asobi_world_lobby (asobi v0.72.6)

View Source

Summary

Functions

Create a new world for the given mode (no owner — anonymous create).

Create a new world for the given mode and tag the player as owner.

Find a running world with capacity for the given mode, or create one.

The non-serialized implementation. Only asobi_world_lobby_server should call this — direct callers race. Exposed because the serializer holds no state and just delegates back here.

List all running worlds.

List running worlds with optional filters: mode, has_capacity, listed, quick_play.

H3 (2026-05-19): cached variant of list_worlds/1 for request paths that do not need a fresh enumeration on every call. Each list_worlds/1 call issues one synchronous gen_server:call per running world (get_info/1); WS world.list at 60 msg/sec x 1000 worlds = 60k calls/sec/attacker. The cache (500 ms TTL, backed by the ETS table owned by asobi_world_lobby_server) absorbs that fan-out without changing the serialization story for find_or_create_unsafe which stays uncached.

Functions

create_world(Mode)

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

Create a new world for the given mode (no owner — anonymous create).

create_world(Mode, PlayerId)

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

Create a new world for the given mode and tag the player as owner.

Refuses with {error, world_capacity_reached} when the global cap (asobi:world_max, default 1000) is hit, and {error, player_world_limit_reached} when the player is already at the per-player cap (asobi:world_max_per_player, default 5). The cap is enforced via a pg group joined on creation so it naturally clears when the world process dies.

find_or_create(Mode)

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

Find a running world with capacity for the given mode, or create one.

Calls go through asobi_world_lobby_server to serialize concurrent requests. The naive list-then-create sequence has a TOCTOU race: two callers both see list_worlds = [], both call create_world, and both clients end up in different worlds despite asking for the same mode. Serializing closes the window.

find_or_create(Mode, PlayerId)

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

find_or_create_unsafe(Mode)

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

The non-serialized implementation. Only asobi_world_lobby_server should call this — direct callers race. Exposed because the serializer holds no state and just delegates back here.

find_or_create_unsafe(Mode, PlayerId)

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

list_worlds()

-spec list_worlds() -> [map()].

List all running worlds.

list_worlds(Filters)

-spec list_worlds(map()) -> [map()].

List running worlds with optional filters: mode, has_capacity, listed, quick_play.

list_worlds_cached()

-spec list_worlds_cached() -> [map()].

H3 (2026-05-19): cached variant of list_worlds/1 for request paths that do not need a fresh enumeration on every call. Each list_worlds/1 call issues one synchronous gen_server:call per running world (get_info/1); WS world.list at 60 msg/sec x 1000 worlds = 60k calls/sec/attacker. The cache (500 ms TTL, backed by the ETS table owned by asobi_world_lobby_server) absorbs that fan-out without changing the serialization story for find_or_create_unsafe which stays uncached.

The cache key is the has_capacity boolean only, never the raw filter map: mode is attacker-controlled and unbounded, so keying on it would let a client cycle distinct modes to force a miss on every request (defeating the cache) and grow the table without bound. Instead the full enumeration is cached under at most two keys and mode is applied in-memory on the cached list.

list_worlds_cached(Filters)

-spec list_worlds_cached(map()) -> [map()].

player_owned_world_count(PlayerId)

-spec player_owned_world_count(binary()) -> non_neg_integer().

world_capacity_state(PlayerId)

-spec world_capacity_state(binary() | undefined) -> map().