Skip to content

Game state loader

This module drives the game-loading and update loop: it talks to com.stratkit.server-communication to post requests, hands the response to com.stratkit.client-api's walker to deserialize, and applies the result to the world. It is also where every other module registers the handlers that read the deserialized state.

Configuration

This module is configured through a GameConfig object:

  • ServerAddress: The game server URL (scheme, host, port).
  • UserAuth: User authentication token or password for alpha servers.
  • UserId: The user account id.
  • GameId: The game id.
  • GameStateUpdateInterval: The game state polling interval in seconds (optional, default is 60 seconds).

Registering a handler

ClientApiPassSetupSystem creates the ClientApiRegistry singleton (ClientApiRegistryContainer) before any other module's setup system runs, and builds the ClientApiWalker from it only once every module has had the chance to register — after AllModulesInitialisedTag is set. A module's own setup system fetches the registry and registers its handlers there, typically disabling itself once done:

[UpdateInGroup(typeof(InitializationSystemGroup))]
internal sealed partial class AIStateLoaderSetupSystem : SystemBase {
    protected override void OnCreate() => RequireForUpdate<ClientApiRegistryContainer>();

    protected override void OnUpdate() {
        ClientApiRegistry registry = SystemAPI.ManagedAPI.GetSingleton<ClientApiRegistryContainer>().Value;
        registry.AddAIStateHandler(ClientApiPaths.AIState.Self, new AIStateDataHandler());

        Enabled = false;
    }
}

A handler registered after the walker is built is never dispatched to, so registration must happen before that point — see com.stratkit.client-api for how a handler itself is implemented.

Talking to the game server

GameServerClient, held on ClientApiPassContainer, speaks the Client API: it builds requests, writes them into its native buffer, and posts them through com.stratkit.server-communication's GameServerTransport. It sits next to the walker because a state response's envelope can only be read while the response is walked.

  • Loading a game. LoadGameAsync() activates the game and requests the first state in one round trip, so it hands back both the activation result and an open GameStateResponse. Call it once at the start of a game session. It retries the whole sequence GameServerClientSettings.LoadGameAttempts times.
  • Updating a game. RequestGameStateAsync(versions, actions) polls for changes since the given StateVersions, with user actions along for the ride. RequestGameStateSingleAsync asks for one state. Stratkit.ClientApi.SupTypeVersioning.UsesTimeStamp(SupType) says which states also send their timestamp.
  • Buffer sizes. GameStateLoaderModule's ClientApiCodecSettings sizes the request writer, the state reader and the per-activation reader. All three are created in ClientApiPassSetupSystem or by the client it builds.
  • Server switches. A ClientSwitchServerException points the client at the named host and repeats the request unchanged, up to three responses per request. That applies to the activation and to state requests alike.

Running a pass

GameStatePass.Start starts one deserialization pass — request, read, dispatch — against the ClientApiPassContainer singleton (the client, reader, context and walker that live for the world's lifetime) and moves the game state entity to GameUpdateState.ApplyingUpdate. ApplyGameStateSystem picks up the resulting GameStatePassResult once the pass completes and plays its command buffer into the world.

Every request path — the initial game load, the periodic poll, queued user actions and a single-state fetch — funnels through this one pass.

Issue user actions

When the user interacts with the game in a way that affects the state, the server needs to process and verify this "user action", sending an updated game state in response. User actions are typed as Stratkit.ClientApi.Actions.IClientUserAction, generated from the Client-API schema — one DTO per action, constructed with an object initializer. To issue one, create a new entity with a ServerCommandRequest component wrapping it; RequestServerCommandSystem picks it up, sends it along with the next poll, and destroys the entity.

Issue user action example

IClientUserAction command = new ClientArmyAction {
    Armies = new List<ClientArmy> { army },
};

entityCommandBuffer.AddComponent(
    entityCommandBuffer.CreateEntity(),
    new ServerCommandRequest { Value = command }
);