A board is an ordering over one number per player, scoped by string keys — so “fastest time on surf_beginner, main track, normal style”, “most kills this week” and “highest arena score” are one thing with three configurations.
Requires dot-core. dot-auth is optional and is never imported.
var boards := DotLeaderboardManager.new()
boards.store = DotLeaderboardStoreMemory.new()
add_child(boards)
var fastest := DotLeaderboardDef.make(&"fastest", DotLeaderboardDef.Kind.TIME)
fastest.display_name = "Fastest time"
fastest.publish = true # opt in to the site leaderboard
boards.define(fastest)
# one definition, one board per (map, track, style)
await boards.submit(&"fastest",
{"map": "surf_beginner", "track": "0", "style": "normal"},
player_id, player_name, run.time())
var page := await boards.page(&"fastest", {"map": "surf_beginner", ...})
Four orderings — TIME, SCORE, POINTS, PENALTY — and rendering that knows
a time from a score. The in-memory store materialises ranks on write, because “am I first”
is asked far more often than a board is written to; the SQL store computes them on read (ties share a rank), since a stored rank costs a write per player on the board for every submission.
Statistics
var session := DotStatSet.new()
session.add(&"jumps", stats.jumps)
session.add(&"distance", metres)
session.set_best(&"top_speed", stats.max_speed)
await boards.add_stats(player_id, session)
await boards.publish_stat(&"most_jumps", {}, player_id, player_name, &"jumps")
Any counter can become a board. Boards and statistics are different problems —
one number ordered, versus many numbers accumulated — which is why
dot-stats exists beside this for the second
one.
Where a game plugs in
- DotLeaderboardStore subclass
put,page,entry_for,count_on,remove,add_stats,stats_for. Two implementations ship:DotLeaderboardStoreMemory, andDotLeaderboardStoreSql, which keeps boards and statistics in real tables through adot-sqldriver (held duck-typed, sodot-leaderboarddoes not depend ondot-sql). Every SQL call suspends, so with it usesubmit_async,page_asyncandentry_for_async; the plain forms refuse it. Swap between them and nothing above changes. The manager sorts, the store does not have to.- DotLeaderboardDef
Kind, display name, scope keys, value formatting, and
publish— the opt-in to the site leaderboard.- DotLeaderboardReporter
Batches submissions to the backbone, keeps its queue through an outage, and is bounded so a backbone down for a day cannot exhaust the server.
boards.reporter.clienttakesdot-auth’sDotBackboneClient.- Signals
entry_accepted,entry_refused,stats_changed.
Reporting to the site
boards.report_to_backbone = true
boards.reporter.client = backbone_client
Needs the LEADERBOARD_WRITE scope on a server- or app-scoped integration.
Nothing is sent per event: entries are queued and flushed in batches.
Pseudonymous, deliberately
A server asserting “this player is that member” is a server that can put
anybody’s name on anybody’s score, so the link has to be proved by the player
— which is what the device-code and ticket flows in
dot-auth are for.
`to_dictionary()` must not hand out its own dictionary
A Dictionary is a reference in GDScript, and DotLeaderboardDef.scoped()
round-trips through one — so every scoped board and the template it came from
were one object, and every board on the server ended up with the last scope
anybody asked for. The same aliasing was then found in three other places.
Scope keys sort by text
scope_key() builds the board’s address from the scope’s keys in order. Array.sort() orders StringNames by where each was interned, not by their text, so the same scope could address a different board (a store row, a site address) in each process. It sorts by text now, and StringName and mixed String / StringName keys give the same board.
