Interface SpawneryApi


public interface SpawneryApi
What a plugin can ask the cloud, from either side of the proxy.

The methods that return a CompletionStage ask the operator, and can fail; every other method is answered locally. The operator keeps a mirror current in each agent, so a read -- self() through player(java.util.UUID) -- never crosses a network, blocks, or fails: there is no timeout and no exception to handle. What it returns is the last thing the operator said, which during a reconnect may be a few seconds old and is never wrong about a moment that happened. A proxy is told of a server's lifecycle (created, placed, unschedulable, gone) usually within a second; counts and announcements arrive with the operator's 30 second resync, and a backend's mirror is refreshed by that resync alone. Each stage-returning method says how it can fail; holdReadiness(java.lang.String) is local and throws on a proxy.

Consume this interface; do not implement it. New methods break implementors but no caller. The agent supplies the implementation; a plugin obtains it from Spawnery.api().

Everything is scoped to this pod's own namespace. No method reaches another network: the agent's credentials are a pod-bound ServiceAccount token, so there is nothing to widen.

  • Method Details

    • self

      Self self()
      What this process is. Use instanceof to learn which side.
    • groups

      List<Group> groups()
      Every group in this network, in no particular order.
    • group

      Optional<Group> group(String name)
      One group by name, empty if this network has none.
    • servers

      List<ServerInfo> servers()
      Every server in this network, in no particular order.
    • server

      Optional<ServerInfo> server(String name)
      One server by name, empty if this network has none.
    • proxies

      List<ProxyInfo> proxies()
      Every proxy in this network, in no particular order.
    • proxy

      Optional<ProxyInfo> proxy(String name)
      One proxy by name, empty if this network has none.
    • players

      List<CloudPlayer> players()
      Every player on this network, whichever backend they are on.
    • player

      Optional<CloudPlayer> player(UUID id)
      One player by UUID, empty if they are not on this network.
    • arrivedByTransfer

      boolean arrivedByTransfer(UUID player)
      Whether this player's connection to this proxy is a transfer from another proxy of the network, carrying a cookie this agent verified.

      For a plugin that announces joins: a player who arrived this way was already online a moment ago, and to their friends nothing happened. A transfer intent alone is not enough for that, because any client can claim one; this is true only when the signed cookie checked out.

      Known from LoginEvent on, for a listener that runs after the agent's own (the agent asks for the cookie there and holds the login until it has the answer). Stays true until the player disconnects from this proxy. Always false on a server, and on a proxy whose group does not have spec.update.transfer.

      Agents older than 0.19.0 do not have this method.

    • leavingByTransfer

      boolean leavingByTransfer(UUID player)
      Whether this proxy has sent this player to another proxy of the network, because this proxy is leaving.

      For a plugin that cleans up after a player: the disconnect that follows is not a quit. True from the moment the transfer is sent until every listener of the player's DisconnectEvent on this proxy has run. It says the player was sent, not that they arrived; a client that fails to reconnect is gone like any other, so a plugin that keeps state for them should still let it lapse if they do not turn up elsewhere. Always false on a server.

      Agents older than 0.19.0 do not have this method.

    • connect

      CompletionStage<ConnectResult> connect(UUID player, Target to)
      Asks the operator to move a player.

      Asynchronous on both platforms, although a proxy could answer locally, so that the same code compiles on either side.

      The operator does not know permissions. A player sent to a group whose join permission refuses them is still answered ordered, and is then refused by the proxy or the server.

      The stage fails rather than returning a result when the operator refuses or cannot answer. A request in flight while the stream is renewed is answered on the new stream; one lost on the wire fails at its deadline and is not retried, because only you know whether moving that player twice is safe.

      A player who logged out between your call and the operator reading it is an ordinary failure and not a bug. So is a target that is not routable yet.

    • retire

      CompletionStage<Void> retire(String server)
      Asks that one server stop taking joins and empty out.

      Retiring is not stopping. The server takes no further joins, the players on it finish in their own time, and nobody is moved or disconnected. Emptied, it is taken down by the same rules that take down any server its group no longer needs.

      The stage completes with no value. It fails when the operator refuses, and asking for a server that is already retiring is a failure, so a caller can tell "you retired it" from "somebody had already asked".

    • unretire

      CompletionStage<Void> unretire(String server)
      Takes a server's retirement back. It takes joins again, and nothing automatic removes it any more; it stays until it ends by itself.

      Fails for a server that is already stopping or is not retiring, with the operator's reason.

    • status

      How this network is doing: CPU and memory of its pods against what they asked for, per group, and each server's tick rate. Asks the operator; never anything outside this network's namespace.
    • status

      status() for one server group, proxy group, server or proxy, looked up in that order. Fails when nothing on this network has the name.
    • boost

      @Deprecated CompletionStage<BoostResult> boost(String group, int replicas, Duration forHowLong)
      Deprecated.
      use scale(java.lang.String, int, java.time.Duration), which sets the size rather than adding to it; this keeps working.
      Asks for extra capacity on a group, for a while.

      It adds to what the group tries for and never to what it may reach. The group's own maxReplicas still binds. A request for more than the ceiling leaves is refused rather than trimmed.

      It expires. Pass null for the operator's default, which is an hour. The operator also bounds how long you may ask for; a lasting need belongs in the group's own definition.

      Boosts add rather than replace: two calls make two boosts.

      The stage fails when the operator refuses: a group it does not have, a group sized by a fixed replica count rather than by scaling, more than the ceiling leaves, a negative duration, or longer than it allows. Each says which.

      Parameters:
      forHowLong - how long the boost should run, or null for the operator's default.
    • stopBoosts

      Deprecated.
      use resetScale(java.lang.String); this keeps working.
      Ends every boost on a group and reports how many there were.

      Every one, not the newest. Zero is an ordinary answer -- the group had no boosts -- and not a failure.

    • scale

      CompletionStage<ScaleResult> scale(String group, int replicas, Duration forHowLong)
      Holds an ephemeral group at exactly this many servers for a while.

      Unlike boost(java.lang.String, int, java.time.Duration), which adds to the floor, a pin is both the floor and the ceiling. Empty servers above the number are deleted and occupied ones are retired: no new joins, and the players stay until they leave. 0 is allowed, but a pin below the group's minReplicas is refused unless the caller is a proxy, and a pin above its maxReplicas is always refused. Of several pins on one group the newest wins, and boosts do not count while one holds.

      The stage fails when the operator refuses: a group it does not have, a group that is not ephemeral, more than maxReplicas, fewer than minReplicas from a caller that is not a proxy, a negative duration, or longer than seven days. Each says which.

      Parameters:
      forHowLong - how long the pin holds, or null for the operator's default of an hour.
    • resetScale

      CompletionStage<Integer> resetScale(String group)
      Ends every pin and every boost on a group and reports how many there were. Zero is an ordinary answer.
    • startServer

      CompletionStage<StartedServer> startServer(String group, String key)
      Asks for the private server that carries this key.

      The key names a world and not a server: the operator composes the server's name from the group and the key, and the same key later finds the same world. Only a group of type OnDemand has members to ask for; anything else fails.

      The stage completing means the server was asked for, not that it can take players. It starts out as any server does; watch ServerInfo.phase() or the events for ServerPhase.READY before sending anybody there.

      Asking for one that is already running succeeds with StartedServer.alreadyRunning() set, so a player pressing a button twice needs no lock on your side. That flag says the server exists and nothing about its being ready.

      How it fails. When the operator answers no, the stage fails with an IllegalStateException whose message is <REASON>: <message> -- the operator's reason, then its own sentence. handle, exceptionally and whenComplete called directly on this stage receive that exception itself. A stage derived from it with thenApply or thenCompose delivers it wrapped in a CompletionException, and get() wraps it in an ExecutionException; unwrap with getCause() only when what you caught is one of those wrappers. The reasons:

      • REFUSED for a key that cannot be part of a name, for a name longer than 63 characters once the group and the key are composed, for a group that is not on-demand, for a name that another group's server already has, and for a group that is genuinely at its spec.maxInstances.
      • NOT_FOUND for a group this network does not have.
      • UNAVAILABLE for a member of that key that is still stopping, and for a group that is at its maxInstances only because one of its members is stopping. Neither is a refusal: the same request succeeds once that member is gone, so ask again.

      One failure is not the operator's answer and carries no reason: the stage fails with a TimeoutException when no answer arrives within ten seconds. That includes a request lost while the stream was renewed; it is not retried, because only you know whether asking twice is safe. For a start it is: one that was in fact carried out is answered alreadyRunning the second time.

    • startServer

      CompletionStage<StartedServer> startServer(String group, String key, StartOptions options)
      Asks for the private server that carries this key, with resources of your choosing for it.

      Like startServer(String, String), which uses the group's values. What options sets replaces the group's value for this member. The operator answers REFUSED for values the group does not allow, and for any value at all when the group has no memberResources.

      A member that is already running keeps the values it started with: the stage succeeds with StartedServer.alreadyRunning() set and StartedServer.resources() says what it runs with; compare only the fields you set.

      An operator that predates per-start resources would start the member with the group's values and say nothing. The stage therefore fails with an IllegalStateException when options were set and the operator did not acknowledge them; the member keeps running with the group's values, started that way or already running.

      Agents older than 0.33.0 do not have this method.

    • stopServer

      CompletionStage<Void> stopServer(String server)
      Stops one private server and leaves its world where it is.

      Not retire(java.lang.String): retiring closes a server's door and lets it empty in its own time, while this says its owner is done with it — the players on it are moved through the proxies and the pod goes. The world is on a claim nothing deletes, so the next startServer(java.lang.String, java.lang.String) of the same key finds it.

      The stage completes with no value. Stopping a server that is already on its way out succeeds too, so a second press of the same button is not an error.

      It fails with REFUSED for a server that is not a member of an on-demand group, which is what keeps a wrong name from taking down a lobby, and with NOT_FOUND for a name this network does not have. The failure has the shape startServer(java.lang.String, java.lang.String) describes, the timeout and the renewed stream included. Asking again after either is safe, but a stop that was in fact carried out may answer NOT_FOUND the second time, once the server is gone.

    • deleteServer

      CompletionStage<Void> deleteServer(String group, String key)
      Deletes one private server for good: stops it if it is running, as stopServer(java.lang.String) does, and deletes its world. There is no undo.

      Group and key as startServer(java.lang.String, java.lang.String) takes them, because a stopped server's world is all that is left of it. The stage completes when the deletion is under way; a startServer(java.lang.String, java.lang.String) of the same key answers UNAVAILABLE until the world is gone, and then starts an empty one.

      It fails with NOT_FOUND for a group this network does not have or a key with neither a server nor a world, and with REFUSED for a group that is not on-demand, a key no name can be built from, or a world this operator did not make. The failure has the shape startServer(java.lang.String, java.lang.String) describes, the timeout and the renewed stream included; asking again is safe, and answers NOT_FOUND once the world is gone.

    • restorePoints

      CompletionStage<RestorePoints> restorePoints(String group, String key)
      Lists the generations of a private server's world that restoreWorld(java.lang.String, java.lang.String, long) can go back to, newest first. The first is the world as it stands and says RestorePoint.current().

      RestorePoints.settling() says whether the world may still change. It is true while the member runs, while it is being stopped, and after a stop until its node has uploaded the final state of the world, which can take some seconds more; also while a restore is being written. Until it turns false, the list can miss the newest state and a restoreWorld(java.lang.String, java.lang.String, long) fails with UNAVAILABLE or REFUSED.

      Only for a group whose worlds live in the object store (storage.backend: ObjectStore). Which older generations stay is the group's storage.retention; without one the list holds the current generation alone. A member that runs adds a generation about every five minutes, and one more when it stops.

      It answers while the member runs and while its world is being deleted; it only reads. The list is what the bucket held at that moment: an upload right after it can drop a listed generation, and a restore of that one then fails with NOT_FOUND.

      It fails with NOT_FOUND for a group this network does not have or a key without a world; with REFUSED for a group that is not on-demand or keeps its worlds on claims, a key no name can be built from, and when the operator runs without world sync; and with UNAVAILABLE when the operator could not read the bucket just now. The failure has the shape startServer(java.lang.String, java.lang.String) describes, except that the operator may take up to a minute to answer, so the stage times out after 75 seconds rather than ten. Asking again after a timeout is safe.

    • listRestorePoints

      default CompletionStage<List<RestorePoint>> listRestorePoints(String group, String key)
      The points of restorePoints(java.lang.String, java.lang.String), without whether the world is settling.
    • restoreWorld

      CompletionStage<RestoredWorld> restoreWorld(String group, String key, long generation)
      Makes an older generation of a private server's world its current world, as a new generation. The generation that was current stays a restore point, so a restore can itself be undone before the member runs again; after its next upload, only while the group's retention keeps it. The next startServer(java.lang.String, java.lang.String) of the key starts on the restored world.

      Who may restore, and what a restore means for the players, is your plugin's decision; the operator asks nobody.

      Only while the member is stopped. It fails with:

      • NOT_FOUND for a group this network does not have, a key without a world, or a generation the world does not keep.
      • REFUSED for a group that is not on-demand or keeps its worlds on claims, an operator that runs without world sync, a member that is running, a world that is being deleted, and the current generation.
      • UNAVAILABLE while the member is still stopping, while its world is being written, and when the operator could not reach the bucket just now. Right after a stop it is the final upload of the world: ask again a few seconds later.
      The failure has the shape startServer(java.lang.String, java.lang.String) describes, the renewed stream included. The operator answers once the restore is written, which can take up to a minute. The stage therefore times out after 75 seconds instead of ten, and after a timeout you cannot tell whether the restore happened. Asking again after either is safe: if the restore was carried out and the member has not run since, the answer is the generation it made, even once the world no longer keeps the generation it restored.
    • acceptJoins

      CompletionStage<Void> acceptJoins(boolean accept)
      Opens or closes this server's own door.

      Closing is not retire(java.lang.String). Retiring says the server is finished: it stops taking joins, empties out, and is taken down once it is empty. Closing only stops the joins, for as long as you like, and can be taken back.

      Nobody is moved. The players already here go on playing until they leave on their own. What changes is only whether the proxies send anybody new.

      The phase does not change, and neither does the routing table. A closed server is still ServerPhase.READY — the phase is the operator's account of a server's lifecycle, and shutting a door is not a lifecycle event. The server also stays in the proxies' table, so a spectator sent there by name still arrives. What changes is who is sent there unasked: the closed server's empty seats stop counting as capacity, so the next player looking for a game is placed somewhere else. To leave the table as well, say the round is over — endRound().

      Your group notices. A closed server's empty seats stop counting as the group's free capacity, so a group sized by spare slots builds a replacement rather than sitting at its floor while every server in it has shut its door.

      The stage fails when the operator refuses — most plainly on a proxy, which is not in anybody's routing table but is the routing table.

      Like announce(java.lang.String, java.util.Map<java.lang.String, java.lang.String>), it survives a reconnection without being called again: the agent restates the last door state on every new session, because the operator's default for a session it has never seen is open.

      Parameters:
      accept - false closes the door, true opens it.
    • endRound

      CompletionStage<Void> endRound()
      Says this server's round is over.

      Two things follow, and only the operator can do either: the server leaves the proxies' routing table, so nobody new arrives at a round that has finished, and the pod stopping after this reads as an ending rather than a fault — its group replaces it without counting a failure.

      It does not stop the server. Call it when the round is over and let the process end as it always did.

      The stage fails when the operator refuses — most plainly on a proxy, which is not in anybody's routing table but is the routing table.

      Like acceptJoins(boolean), it survives a reconnection without being called again: the agent restates it on every new session, because the operator's default for a session it has never seen is that the round has not ended. It is restated together with the door, never apart — a round that ended closed the door too, and the two are never told apart on the wire.

    • playableSlots

      void playableSlots(int slots)
      Sets how many of this server's seats count as capacity, from now until changed: what its group's spare slots, free slots and a connect to the group measure. Players beyond it are still admitted up to the server's limit, and make the server full rather than overfull -- unless the group sets enforcePlayableSlots, in which case it is also the door.

      It asks the operator nothing. The next periodic report carries it, and every report after that, so a new session restates it without a second call.

      Servers only; a proxy throws UnsupportedOperationException.

      Parameters:
      slots - the playable seats; 0 hands the decision back to the group's spec.playableSlots. A figure above the server's slots counts as its slots.
      Throws:
      IllegalArgumentException - if slots is negative
    • holdReadiness

      ReadinessHold holdReadiness(String reason)
      Holds this server back from readiness until the returned hold is closed.

      For a plugin whose initialisation continues after the server has finished enabling -- a mapping table loaded on its own executor, a database opened in the background. The agent reports readiness when the last hold is released and the server has finished enabling, whichever comes second, so a server that is not finished stays Starting rather than becoming Ready with nobody able to play on it.

      It cannot lower a readiness already reported. Readiness is a one-way latch; a hold taken after the agent has reported does nothing but log. To stop new players reaching a server that is already ready, use acceptJoins(boolean), which is what that method is for.

      A hold that is never released pins the server in Starting until the operator's startup deadline fails it. That is the intended outcome -- a plugin that never finishes starting is a broken server -- and reason is what names it in the log.

      Servers only. A proxy has no readiness of this kind and this throws UnsupportedOperationException there.

      Parameters:
      reason - what is being waited for, for the log. Required.
    • announce

      CompletionStage<Void> announce(String state, Map<String,String> attributes)
      Publishes what this server is doing, for every other server to read.

      The cloud carries this and never reads it. No operator decision looks at it. It reaches the other agents in this network as ServerInfo.state() and ServerInfo.attributes() and goes no further.

      It is not ServerInfo.phase(). The phase is the operator's account of a server's lifecycle and no plugin can write it. This is the server's own account of what happens while it is READY.

      Each call replaces the last one whole. Attributes are not merged: a call with one attribute leaves this server with one, whatever the call before it said. Publish the whole description each time, which is also the only way an attribute can ever be taken back.

      The stage fails when the operator refuses -- a state or an attribute longer than it carries, more attributes than it carries, or a call from a proxy, which has no per-instance record in the network's picture for an announcement to appear in. Each refusal says which.

      It survives a reconnection without being called again: the agent holds the last description it published and re-publishes it on every new session.

      Repeating the description the operator already holds, or is about to, sends nothing; the stage follows the request already sent. The operator limits how many requests a pod may make, and a repeat that sends nothing leaves that budget to starts, stops and connects.

      Parameters:
      state - what this server is doing, in a word or a short phrase. Empty clears it.
      attributes - anything else worth publishing. Empty clears them.
    • events

      EventBus events()
      Where to hear about things happening in the cloud.

      The same object every time, so a plugin may hold it. See EventBus for what it does and does not promise — most of all that it is a feed and not a ledger.