docs(link): points.board, the leaderboards API, and what a real shard changed
Protocol 3.0 §7 lands across servuo-plugins, link and website; this is the
matching documentation.
INTEGRATION.md
- points.board in the event catalog: one frame per system, never a delta, no
points.remove (the shard's system set is fixed at startup). Four gotchas
called out, all of them things a consumer gets wrong by default:
1. maxPoints: 0 means UNCAPPED, not "zero allowed" — and on a real shard
it is the COMMON case, so anything rendering points/maxPoints must
special-case it or divide by zero on the happy path.
2. nameString is usually null, with a cliloc in nameNumber — so
humanising the system key is the primary display path, not a fallback.
3. players counts players actually holding points, not table size: ten of
the ~25 systems keep a zero-point row per character ever created, so
the raw count would report the shard's whole census.
4. Entries carry serial + name only, never acct/webId.
- The char.profile `points` block, and why `rank` is absent by default.
- GET /points and /points/:system, including why 404 (never published) and
200-with-empty-top (published, nobody scored) are different answers.
v3.md
- B/2 marked done in both the progress table and §9.
- NEW §7.5, "what the run against a real shard changed" — the same record §6.1
and §6.2 keep. Four corrections the plan could not have anticipated from
reading PointsSystem.cs, the sharpest being that (long)double.MaxValue is an
unchecked conversion yielding long.MinValue, which published
"maxPoints": -9223372036854775808 on the first live sweep. Also records that
GetEntry/GetPoints mutate the world on AutoAdd systems and so cannot be used
in a read model, and the one deliberate deviation from §7.4: the visibility
field rule must key on the wire's `name`, not the descriptive
`characterName`, or it is silently inert.
BACKEND_DESIGN.md — shard_points_boards (including why the top-N list stays in
the payload and why listing orders by COALESCE(name, system)), plus the two new
public routes.
PLAN.md — 3.0 phasing brought current: the spawn atlas and points.board added to
what has shipped, and the Points* keys noted in the config-key paragraph.
PROJECT_TREE.md files are deliberately untouched — they are CI-generated and say
so.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -381,6 +381,74 @@ Absent entirely if the shard runs `Bridge.RulesetEnabled=false` or an older plug
|
||||
This **supersedes the `world.systems` frame** sketched in [`PROTOCOL_2.md`](PROTOCOL_2.md) §10.4 and
|
||||
never implemented; the `systems` block above is what that asked for.
|
||||
|
||||
#### Points / loyalty leaderboards (Protocol 3.0)
|
||||
|
||||
ServUO carries ~25 separate point currencies — Queen's Loyalty, Void Pool, Casino, Clean Up Britannia,
|
||||
the nine city loyalties, Blackthorn, the Doom / Khaldun / Kotl treasure systems — every one a standing
|
||||
players accumulate over months, and none of them visible outside an in-game gump before 3.0.
|
||||
|
||||
A diff sweep (default 300 s), **one frame per system** rather than one large frame for all of them,
|
||||
matching `champ.update` / `guild.update`. A system is emitted only when its top N or its participant
|
||||
count actually changes.
|
||||
|
||||
| kind | fields | notes |
|
||||
|------|--------|-------|
|
||||
| `points.board` | `system`, `nameString`, `nameNumber`, `maxPoints`, `showOnGump`, `players`, `top[]` | One system's complete board — **never a delta**. The latest frame for a `system` replaces the previous one outright. `top[]` entries are `{rank, serial, name, points}`. |
|
||||
|
||||
`system` is the shard's own `PointsType` enum name (`QueensLoyalty`, `CleanUpBritannia`, …) and is the
|
||||
board's stable key. There is deliberately **no `points.remove`**: the set of systems is fixed at startup
|
||||
by `PointsSystem.Configure`, so a system cannot disappear at runtime — the same argument `city.update`
|
||||
makes for cities.
|
||||
|
||||
```json
|
||||
{"kind":"points.board","system":"QueensLoyalty",
|
||||
"nameString":"Queen's Loyalty","nameNumber":1114938,
|
||||
"maxPoints":15000,"showOnGump":true,"players":842,
|
||||
"top":[{"rank":1,"serial":"0x1A2B","name":"Darrow","points":29500},
|
||||
{"rank":2,"serial":"0x1A2C","name":"Mireille","points":21000}],
|
||||
"t":1752489280000}
|
||||
```
|
||||
|
||||
**Four things consumers get wrong.**
|
||||
|
||||
1. **`maxPoints` of `0` means UNCAPPED, not "zero points allowed".** ServUO's idiom for an uncapped
|
||||
system is `double.MaxValue` (`DespiseCrystals`, `ShameCrystals` and `VoidPool` all use it), which
|
||||
the plugin normalises to `0` rather than emitting a nonsense integer. On a real shard **most
|
||||
systems are uncapped**, so a UI that renders `points / maxPoints` must special-case this or it will
|
||||
divide by zero on the common path.
|
||||
2. **`nameString` is usually `null`.** The shard's `Name` is a `TextDefinition`, which may carry a
|
||||
literal *or* a cliloc id, and in practice most systems use the cliloc — so `nameNumber` is set and
|
||||
`nameString` is `null`. Resolve clilocs consumer-side; failing that, humanising the `system` key
|
||||
("CleanUpBritannia" → "Clean Up Britannia") reads better than showing a bare number. This is the
|
||||
same contract `titles.reward` already documents.
|
||||
3. **`players` counts players who actually hold points**, not the size of the system's table. Ten of
|
||||
the ~25 systems have `AutoAdd = true` and therefore keep a zero-point row for every character that
|
||||
has ever logged in, so the raw table size would report the shard's entire character census as that
|
||||
system's participants.
|
||||
4. **Entries carry `serial` and `name` only — never `acct` or `webId`.** A board is the widest-audience
|
||||
surface the bridge has, so the account name of every ranked player deliberately does not cross the
|
||||
wire; resolve serial → site user from your own link mirror if you need it.
|
||||
|
||||
Absent entirely if the shard runs `Bridge.PointsLeaderboardEnabled=false` or an older plugin. Render
|
||||
from `GET /points` (§6) on connect, then keep live with this event.
|
||||
|
||||
##### `char.profile` gains a `points` block
|
||||
|
||||
Read-model enrichment on the existing kind — there is **no** request kind for one character's points,
|
||||
the same precedent `titles` set in [`PROTOCOL_2.md`](PROTOCOL_2.md) §10.3:
|
||||
|
||||
```json
|
||||
"points":[{"system":"QueensLoyalty","nameString":"Queen's Loyalty","nameNumber":1114938,
|
||||
"points":29500,"maxPoints":15000}]
|
||||
```
|
||||
|
||||
Systems where the character has no entry, or an entry at zero, are **omitted** — otherwise every sheet
|
||||
would carry ~25 zeroes. `maxPoints` follows the same `0 == uncapped` rule as the board.
|
||||
|
||||
`rank` is **absent by default** and appears only when the shard runs `Bridge.PointsProfileRank=true`:
|
||||
a points lookup stops at the character's own row, but a rank must count every row that beats them, in
|
||||
every system, on every profile build. Derive rank from `points.board` instead for anyone in the top N.
|
||||
|
||||
---
|
||||
|
||||
## 5. REST — read queries
|
||||
@@ -740,6 +808,27 @@ worse than one that is briefly stale. Keep it current with the `world.ruleset` s
|
||||
`Bridge.RulesetEnabled=false`. That is a real answer distinct from a published ruleset, and worth
|
||||
rendering differently ("not published yet") rather than as an empty ruleset.
|
||||
|
||||
### Points / loyalty leaderboards (Protocol 3.0)
|
||||
|
||||
```
|
||||
GET /points
|
||||
→ { "boards": [ {"kind":"points.board","system":"QueensLoyalty","nameString":"Queen's Loyalty",
|
||||
"nameNumber":1114938,"maxPoints":15000,"showOnGump":true,"players":842,
|
||||
"top":[{"rank":1,"serial":"0x1A2B","name":"Darrow","points":29500}, ...],"t":...}, ... ] }
|
||||
|
||||
GET /points/{system} # e.g. /points/QueensLoyalty
|
||||
→ {"kind":"points.board","system":"QueensLoyalty", ... }
|
||||
```
|
||||
|
||||
Every system's latest board, or one by its `PointsType` name (§4 for the frame and its four gotchas).
|
||||
Served from the sidecar's projection, kept current by the `points.board` stream, ordered by display
|
||||
name. Survives a sidecar restart — which matters more here than for live state, since these are
|
||||
standings built over months and blanking them during a restart reads as data loss.
|
||||
|
||||
`GET /points/{system}` returns **404** for a system the shard has never published (an unknown name, or
|
||||
one excluded by `Bridge.PointsSystems`). That is distinct from a published board nobody has scored in
|
||||
yet, which is **200** with an empty `top[]` — and the two are worth rendering differently.
|
||||
|
||||
---
|
||||
|
||||
## 7. Status codes
|
||||
|
||||
Reference in New Issue
Block a user