asobi_world behaviour (asobi v0.75.1)

View Source

Behaviour for large-session world game modules.

Unlike asobi_match, world games are spatially partitioned into zones. The game module provides zone-level tick logic and global post-tick events. Only init/1, join/2, leave/2, spawn_position/2, zone_tick/2, handle_input/3, post_tick/2, init_zone_state/2, and dump_zone_state/1 are required; the rest are optional hooks (see -optional_callbacks).

Summary

Callbacks

Reduce zone_state to a JSON-safe map for snapshotting: drop any live runtime (e.g. a VM that cannot be serialised) and decode engine-held gameplay state to plain terms. The inverse of init_zone_state's restore path.

Optional: seed the initial zone states from a world seed.

Optional: project the world to the state one player should see.

Apply a player input to a zone's entities.

Initialise global game state from the match config.

Build this zone's zone_state in the zone process, from the zone Config and any plain gameplay state restored from a snapshot. This is where a game module that holds a per-zone runtime (e.g. a Lua VM) constructs it, bound to the zone process. Runs once, after init, via handle_continue.

A player joins the world.

Optional. Same as join/2, but also receives the join context the client supplied — a flat map of binaries, bounded by the server, that asobi does not interpret.

A player leaves the world.

Optional: a phase ended.

Optional: a phase started.

Optional: the world was recovered from snapshots after a crash.

Optional: a zone was lazily loaded.

Optional: a zone was unloaded.

Optional: declare the world's phases.

Global post-tick hook: continue, trigger a vote, or finish the world.

Where a joining player spawns.

Optional: named entity spawn templates for zone spawners.

Optional (asobi#253): a per-tick, cheap hint that spawn templates may have changed since zone creation - e.g. a script hot-reload adding/renaming a template. spawn_templates/1 is only ever called once, at zone creation; without this, a long-running zone never learns about a template added later and every spawn attempt against it surfaces as unknown_spawn_template indefinitely, not just until the next reload.

Optional: the terrain provider module + args, or none.

Per-zone tick: advance the entities in one zone from its zone_state.

Callbacks

dump_zone_state(ZoneState)

(optional)
-callback dump_zone_state(ZoneState :: map()) -> map().

Reduce zone_state to a JSON-safe map for snapshotting: drop any live runtime (e.g. a VM that cannot be serialised) and decode engine-held gameplay state to plain terms. The inverse of init_zone_state's restore path.

generate_world(Seed, Config)

(optional)
-callback generate_world(Seed :: integer(), Config :: map()) ->
                            {ok, ZoneStates :: #{{integer(), integer()} => term()}}.

Optional: seed the initial zone states from a world seed.

get_state(PlayerId, GameState)

(optional)
-callback get_state(PlayerId :: binary(), GameState :: term()) -> StateForPlayer :: map().

Optional: project the world to the state one player should see.

handle_input(PlayerId, Input, Entities)

-callback handle_input(PlayerId :: binary(), Input :: map(), Entities :: map()) ->
                          {ok, Entities1 :: map()} | {error, Reason :: term()}.

Apply a player input to a zone's entities.

init(Config)

-callback init(Config :: map()) -> {ok, GameState :: term()}.

Initialise global game state from the match config.

init_zone_state(Config, ZoneState)

(optional)
-callback init_zone_state(Config :: map(), ZoneState :: map()) -> map().

Build this zone's zone_state in the zone process, from the zone Config and any plain gameplay state restored from a snapshot. This is where a game module that holds a per-zone runtime (e.g. a Lua VM) constructs it, bound to the zone process. Runs once, after init, via handle_continue.

join(PlayerId, GameState)

-callback join(PlayerId :: binary(), GameState :: term()) ->
                  {ok, GameState1 :: term()} | {error, Reason :: term()}.

A player joins the world.

join(PlayerId, Ctx, GameState)

(optional)
-callback join(PlayerId :: binary(), Ctx :: map(), GameState :: term()) ->
                  {ok, GameState1 :: term()} | {error, Reason :: term()}.

Optional. Same as join/2, but also receives the join context the client supplied — a flat map of binaries, bounded by the server, that asobi does not interpret.

Implement this to gate entry on something the client presents: a join code, an invite token, a party id, a password. Without it there is no channel from a client to your game before membership exists, so join/2 can implement an allowlist but never a code.

Export join/3 and it is used instead of join/2. asobi never reads the context; validate it against your own GameState and return {error, Reason} to refuse. The context is #{} when there is no client behind the join.

leave(PlayerId, GameState)

-callback leave(PlayerId :: binary(), GameState :: term()) -> {ok, GameState1 :: term()}.

A player leaves the world.

on_phase_ended(PhaseName, GameState)

(optional)
-callback on_phase_ended(PhaseName :: binary(), GameState :: term()) -> {ok, GameState1 :: term()}.

Optional: a phase ended.

on_phase_started(PhaseName, GameState)

(optional)
-callback on_phase_started(PhaseName :: binary(), GameState :: term()) -> {ok, GameState1 :: term()}.

Optional: a phase started.

on_world_recovered(Snapshots, GameState)

(optional)
-callback on_world_recovered(Snapshots :: map(), GameState :: term()) -> {ok, GameState1 :: term()}.

Optional: the world was recovered from snapshots after a crash.

on_zone_loaded(Coords, GameState)

(optional)
-callback on_zone_loaded(Coords :: {integer(), integer()}, GameState :: term()) ->
                            {ok, ZoneState :: map(), GameState1 :: term()}.

Optional: a zone was lazily loaded.

on_zone_unloaded(Coords, GameState)

(optional)
-callback on_zone_unloaded(Coords :: {integer(), integer()}, GameState :: term()) ->
                              {ok, GameState1 :: term()}.

Optional: a zone was unloaded.

phases(Config)

(optional)
-callback phases(Config :: map()) -> [asobi_phase:phase_def()].

Optional: declare the world's phases.

post_tick(Tick, GameState)

-callback post_tick(Tick :: non_neg_integer(), GameState :: term()) ->
                       {ok, GameState1 :: term()} |
                       {vote, VoteConfig :: map(), GameState1 :: term()} |
                       {finished, Result :: map(), GameState1 :: term()}.

Global post-tick hook: continue, trigger a vote, or finish the world.

spawn_position(PlayerId, GameState)

-callback spawn_position(PlayerId :: binary(), GameState :: term()) -> {ok, {X :: number(), Y :: number()}}.

Where a joining player spawns.

spawn_templates(Config)

(optional)
-callback spawn_templates(Config :: map()) -> #{binary() => asobi_zone_spawner:spawn_template()}.

Optional: named entity spawn templates for zone spawners.

spawn_templates_hint(ZoneState)

(optional)
-callback spawn_templates_hint(ZoneState :: term()) ->
                                  unchanged | {changed, #{binary() => asobi_zone_spawner:spawn_template()}}.

Optional (asobi#253): a per-tick, cheap hint that spawn templates may have changed since zone creation - e.g. a script hot-reload adding/renaming a template. spawn_templates/1 is only ever called once, at zone creation; without this, a long-running zone never learns about a template added later and every spawn attempt against it surfaces as unknown_spawn_template indefinitely, not just until the next reload.

Called every tick if exported, so implementations MUST be cheap in the common case - return unchanged immediately unless something already indicates a real change happened this tick (e.g. a hot-reload just ran). Do not unconditionally rebuild/re-read a template source here.

{changed, NewTemplates} REPLACES the zone's entire template set, the same as spawn_templates/1's result does at creation - it is not a delta. An implementation built from a partial reload that reconstructs only the templates it knows changed will silently drop every other template; make sure NewTemplates includes every template that should still be spawnable, not only the ones that changed.

terrain_provider(Config)

(optional)
-callback terrain_provider(Config :: map()) -> {Module :: module(), ProviderArgs :: map()} | none.

Optional: the terrain provider module + args, or none.

zone_tick(Entities, ZoneState)

-callback zone_tick(Entities :: map(), ZoneState :: term()) -> {Entities1 :: map(), ZoneState1 :: term()}.

Per-zone tick: advance the entities in one zone from its zone_state.