asobi_world_server (asobi v0.72.6)

View Source

A persistent, zoned world: the gen_statem behind large session games (game_type = "world"). It partitions space into a grid of asobi_zone processes, moves players between zones as they travel, runs a world-level post_tick, and drives voting. Use it for MMO-style shared spaces; for transient matches use asobi_match_server instead.

Summary

Types

What a non-member may see. Every key is optional: maps:with/2 keeps only what the world set.

Functions

As join/3, plus an opaque join context from the client. asobi does not interpret it; it reaches the game module's join/3 if it exports one.

Projection of get_info/1 for callers who are not members of the world.

As move_player/3, but for a caller that already holds the entity's full state (asobi_zone's resolve_zone_crossings/1). Entity is written into the target zone as-is instead of being reconstructed as #{x, y, type} only, so fields a game script keeps beyond position - health, inventory, whatever - survive the zone boundary. See widgrensit/asobi#248.

As pos_to_zone/2, additionally clamped to 0 .. GridSize - 1 on both axes. Use this wherever a player's current zone is derived from position. See widgrensit/asobi#248.

Tell the world server a zone was just created by something other than a player join or crossing - asobi_zone creating a neighbour to receive a crossing NPC (widgrensit/asobi#271). The world server owns player_zones, so only it can subscribe the already-connected players whose interest ring already covered those coords (widgrensit/asobi#275).

Types

listing()

-type listing() ::
          #{world_id => binary(),
            status => atom(),
            player_count => non_neg_integer(),
            max_players => pos_integer(),
            mode => binary() | undefined,
            grid_size => non_neg_integer(),
            started_at => integer() | undefined,
            phase => map()}.

What a non-member may see. Every key is optional: maps:with/2 keeps only what the world set.

Functions

callback_mode()

-spec callback_mode() -> gen_statem:callback_mode_result().

cancel(Pid)

-spec cancel(pid()) -> ok.

cast_vote(Pid, PlayerId, VoteId, OptionId)

-spec cast_vote(pid(), binary(), binary(), binary()) -> ok | {error, term()}.

finished/3

-spec finished(gen_statem:event_type() | enter, term(), map()) -> gen_statem:state_enter_result(atom()).

get_info(Pid)

-spec get_info(pid()) -> map().

get_info/2

-spec get_info(pid(), listing) -> map().

init(Config)

-spec init(map()) -> {ok, atom(), map()}.

join(Pid, PlayerId)

-spec join(pid(), binary()) -> ok | {error, term()}.

join(Pid, PlayerId, SessionPid)

-spec join(pid(), binary(), pid()) -> ok | {error, term()}.

join(Pid, PlayerId, SessionPid, Ctx)

-spec join(pid(), binary(), pid(), map()) -> ok | {error, term()}.

As join/3, plus an opaque join context from the client. asobi does not interpret it; it reaches the game module's join/3 if it exports one.

leave(Pid, PlayerId)

-spec leave(pid(), binary()) -> ok.

listing_info(Info)

-spec listing_info(map()) -> listing().

Projection of get_info/1 for callers who are not members of the world.

get_info/1 carries the full players roster. Discovery and the join reply are open to any authenticated player, so they get identity, mode, status, capacity and phase only.

The projection descends into phase: asobi_phase:info/1 evolves independently and its config is game-authored, so allowlisting only the top level would leak whatever a future phase field happens to carry.

loading/3

move_player(Pid, PlayerId, NewPos)

-spec move_player(pid(), binary(), {number(), number()}) -> ok.

move_player(Pid, PlayerId, NewPos, Entity)

-spec move_player(pid(), binary(), {number(), number()}, map()) -> ok.

As move_player/3, but for a caller that already holds the entity's full state (asobi_zone's resolve_zone_crossings/1). Entity is written into the target zone as-is instead of being reconstructed as #{x, y, type} only, so fields a game script keeps beyond position - health, inventory, whatever - survive the zone boundary. See widgrensit/asobi#248.

pos_to_zone(Pos, ZoneSize, GridSize)

-spec pos_to_zone({number(), number()}, non_neg_integer(), pos_integer()) ->
                     {non_neg_integer(), non_neg_integer()}.

As pos_to_zone/2, additionally clamped to 0 .. GridSize - 1 on both axes. Use this wherever a player's current zone is derived from position. See widgrensit/asobi#248.

post_tick(Pid, TickN)

-spec post_tick(pid(), non_neg_integer()) -> ok.

reconnect(Pid, PlayerId)

-spec reconnect(pid(), binary()) -> ok | {error, term()}.

running/3

-spec running(gen_statem:event_type() | enter, term(), map()) -> gen_statem:state_enter_result(atom()).

spawn_at(Pid, TemplateId, Pos)

-spec spawn_at(pid(), binary(), {number(), number()}) -> ok.

spawn_at(Pid, TemplateId, Pos, Overrides)

-spec spawn_at(pid(), binary(), {number(), number()}, map()) -> ok.

start_link(Config)

-spec start_link(map()) -> gen_statem:start_ret().

start_vote(Pid, VoteConfig)

-spec start_vote(pid(), map()) -> {ok, pid()} | {error, term()}.

terminate/3

-spec terminate(term(), atom(), map()) -> ok.

use_veto(Pid, PlayerId, VoteId)

-spec use_veto(pid(), binary(), binary()) -> ok | {error, term()}.

whereis(WorldId)

-spec whereis(binary()) -> {ok, pid()} | error.

zone_created(Pid, Coords, ZonePid)

-spec zone_created(pid(), {integer(), integer()}, pid()) -> ok.

Tell the world server a zone was just created by something other than a player join or crossing - asobi_zone creating a neighbour to receive a crossing NPC (widgrensit/asobi#271). The world server owns player_zones, so only it can subscribe the already-connected players whose interest ring already covered those coords (widgrensit/asobi#275).