New

dot-achievements

What a player has earned, as rules over their numbers — counters, bests, tiers, hidden and secret entries, a catalogue a server validates at boot, a pluggable store and a batched backbone reporter.

dot-achievements states an achievement as data — rules over per-player numbers — rather than as code inside whatever awards it. A condition that lives in the awarding code is one nobody can list, show progress towards, or check a save file against.

Requires dot-core. dot-stats is the natural source of the numbers and dot-auth supplies the backbone client; neither is named in the source.

Usage

var catalogue := DotAchievementCatalogue.of([
    DotAchievement.counter(&"k10",  "First Blood", &"kills", 10),
    DotAchievement.counter(&"k100", "Centurion",   &"kills", 100),
    DotAchievement.make(&"flawless", "Flawless", [
        DotAchievementRule.make(&"round_wins", 1),
        DotAchievementRule.make(&"deaths", 0, DotAchievementRule.Op.AT_MOST),
    ]),
])

var tracker := DotAchievementTracker.new()
tracker.catalogue = catalogue
tracker.store = DotAchievementStoreFile.new("user://achievements")
tracker.unlocked.connect(func(player: String, a: DotAchievement) -> void:
    hud.toast("%s unlocked" % a.display_name))
add_child(tracker)

await tracker.begin(player_key)         # loads their lifetime numbers
tracker.record(player_key, &"kills", 1)
await tracker.end(player_key)           # saves

record() merges by the kind the rule declared, so a counter adds and a best keeps the better. One call serves every stat, and a game cannot get it the wrong way round.

Validating the catalogue loads no mesh, icon or scene, so a dedicated server can decide what somebody has earned and report it — the same rule as dot-loadout.

With dot-stats

var link := DotAchievementStatsLink.new()
link.tracker = tracker
link.stats = stats_tracker
add_child(link)

Showing them

for row in tracker.listing(player_key):
    print(row["name"], row.get("progress", row.get("unlocked")))

Hidden entries are absent until earned and secret ones arrive without their description. The filtering happens here rather than in a UI, because a description withheld by the interface that drew it was still sent to the client.

Reporting to the backbone

Set report_to_backbone and the tracker drives its own DotAchievementReporter: every report_interval seconds (ten by default) it declares the catalogue once — retried on a doubling delay while the backbone is unreachable, dropped after a refusal — and sends the queued unlocks in one batch, so a round’s end that awards a dozen is one request. report_app is the app id sent with the declaration, and await tracker.report() sends on demand, which is what a host does before shutdown. The tracker owns the timer because it is the one node a host already places; before it did, nothing in the family ever flushed the queue and every unlock sat there until the queue limit dropped it.

The site serves the three routes the reporter speaks (define, unlock and player) in the shape the reporter sends. When a 200 names refused unlocks, achievements the site has no definition for, the reporter logs them at ERROR: those unlocks are gone, and before this nothing said so.