asobi_world behaviour (asobi v0.84.0)
View SourceBehaviour 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
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.
-callback generate_world(Seed :: integer(), Config :: map()) -> {ok, ZoneStates :: #{{integer(), integer()} => term()}}.
Optional: seed the initial zone states from a world seed.
Optional: project the world to the state one player should see.
-callback handle_input(PlayerId :: binary(), Input :: map(), Entities :: map()) -> {ok, Entities1 :: map()} | {error, Reason :: term()}.
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.
-callback join(PlayerId :: binary(), GameState :: term()) -> {ok, GameState1 :: term()} | {error, Reason :: term()}.
A player joins the world.
-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.
A player leaves the world.
Optional: a phase ended.
-callback on_phase_started(PhaseName :: binary(), GameState :: term()) -> {ok, GameState1 :: term()}.
Optional: a phase started.
-callback on_world_recovered(Snapshots :: map(), GameState :: term()) -> {ok, GameState1 :: term()}.
Optional: the world was recovered from snapshots after a crash.
-callback on_zone_loaded(Coords :: {integer(), integer()}, GameState :: term()) -> {ok, ZoneState :: map(), GameState1 :: term()}.
Optional: a zone was lazily loaded.
-callback on_zone_unloaded(Coords :: {integer(), integer()}, GameState :: term()) -> {ok, GameState1 :: term()}.
Optional: a zone was unloaded.
-callback phases(Config :: map()) -> [asobi_phase:phase_def()].
Optional: declare the world's phases.
-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.
-callback spawn_position(PlayerId :: binary(), GameState :: term()) -> {ok, {X :: number(), Y :: number()}}.
Where a joining player spawns.
-callback spawn_templates(Config :: map()) -> #{binary() => asobi_zone_spawner:spawn_template()}.
Optional: named entity spawn templates for zone spawners.
-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.
Optional: the terrain provider module + args, or none.
-callback zone_tick(Entities :: map(), ZoneState :: term()) -> {Entities1 :: map(), ZoneState1 :: term()}.
Per-zone tick: advance the entities in one zone from its zone_state.