asobi_quests (asobi_quests v0.2.2)
View SourceThe domain. Plain Erlang over asobi_repo, knowing nothing about Lua, RPC or
HTTP - each of those is a thin adapter over this module.
The counter
asobi has no counter primitive, so quests ships one. A quest names a counter
and a target; a game script reports raw events ("kills", +1) and never
mentions a quest. progress/3 fans one report out to every active quest
listening for that counter, in the caller's current period.
The increment is a single INSERT ... ON CONFLICT DO UPDATE SET counter = counter + EXCLUDED.counter statement, so two match processes reporting a kill
in the same millisecond cannot lose one. It cannot be expressed through
asobi_repo: update_all/2 sets literal values and kura's on_conflict
replaces rather than accumulates, so the statement goes through
kura_repo_worker:query/3. See the gap note in the README.
Completion and claiming
completed_at is stamped by the same statement that crosses the target, and
COALESCE keeps the first stamp: a quest completes once even if the counter
keeps climbing.
claim/2 is a guarded UPDATE ... WHERE completed_at IS NOT NULL AND claimed_at IS NULL. Postgres decides the winner of a double-claim race, so
the reward is granted by whichever call updated a row and the other is told
already_claimed without reading anything first.
Summary
Functions
Every active definition.
Every active definition listening for Counter.
Claim a completed quest's reward.
Deactivate a definition. Progress rows survive; a deactivated quest simply stops advancing.
Create or update a definition, keyed on quest_key.
One definition by key. Reads the cache, falling back to the database.
Report Amount of Counter for a player, advancing every quest listening.
Delete progress rows from periods that closed more than Days ago.
Every active quest's state for a player, in the player's current period.
Types
-type advance() :: #{quest_key := binary(), counter := non_neg_integer(), target := pos_integer(), completed := boolean(), newly_completed := boolean(), claimed := boolean()}.
-type quest() :: map().
-type state() :: #{quest_key := binary(), title := binary(), counter_name := binary(), period := asobi_quests_period:period(), period_key := binary(), counter := non_neg_integer(), target := pos_integer(), completed := boolean(), claimed := boolean(), reward_currency := binary() | undefined, reward_amount := non_neg_integer()}.
Functions
Every active definition.
Every active definition listening for Counter.
-spec claim(binary(), binary()) -> {ok, #{quest_key := binary(), currency := binary() | undefined, amount := non_neg_integer()}} | {error, quest_not_found | not_completed | already_claimed | term()}.
Claim a completed quest's reward.
The whole thing is one transaction: the guarded claim update and the economy grant commit together or not at all, so a grant that fails cannot leave a quest marked claimed and unpaid.
Deactivate a definition. Progress rows survive; a deactivated quest simply stops advancing.
Create or update a definition, keyed on quest_key.
Idempotent on purpose: a game script declares its quests from init(), which
runs on every match or world boot, so calling this a thousand times has to
converge on one row.
One definition by key. Reads the cache, falling back to the database.
-spec progress(binary(), binary(), pos_integer()) -> {ok, [advance()]} | {error, term()}.
Report Amount of Counter for a player, advancing every quest listening.
Returns one entry per quest advanced, so a caller that wants to announce a
completion reads newly_completed rather than diffing anything itself. An
unknown counter is not an error: it means no quest cares yet.
-spec purge_expired(non_neg_integer()) -> {ok, non_neg_integer()} | {error, term()}.
Delete progress rows from periods that closed more than Days ago.
Only recurring quests produce dead rows: a none quest's single row is its
lifetime record and is never expired. Called by
asobi_quests_rollover_worker.
Every active quest's state for a player, in the player's current period.
One query for the definitions and one for the progress rows, composed in Erlang. A join would have to express "the row whose period_key is the one this quest's period resolves to right now", which is per-quest and not a constant.