feat(bridge): publish points/loyalty leaderboards as points.board #4
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/points-board"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
Protocol 3.0 §7 (
docs/link/v3.md), order 4 of 6. ServUO carries ~25 separate point currencies — Queen's Loyalty, Void Pool, Casino, Clean Up Britannia, the nine city loyalties, the Doom/Khaldun/Kotl treasure systems — every one a standing players build over months, and none of them visible outside an in-game gump until now.Part of a four-repo change: servuo-plugins (this) → link #18 → website #114 → docs #69.
BridgePoints.cs— the board sweepA diff sweep shaped like
BridgeHousing:ServerStartedarms the timer, a sidecar connect clears the diff state so a fresh sidecar gets every board, and each pass emits only the systems whose top N or participant count actually moved. One ~600 B frame per system rather than one 12 KB frame, matchingchamp.update/guild.update.No
points.remove— the system set is fixed at startup byPointsSystem.Configure, the same argumentcity.updatealready makes for cities.The perf trap, and why the selection looks like it does.
PlayerTableis a plainList<PointsEntry>, and ten of the ~25 systems haveAutoAdd = true, so they hold a row for every character that has ever logged in. The obvious.OrderByDescending().Take(N)is a full sort per system on the Core thread — whichBRIDGE_PLUGIN_PLAN.md§1 measured as the second thing in the whole bridge capable of blowing a frame budget. Instead: a single bounded pass into a fixed N-element array kept sorted by insertion, one allocation for the whole sweep, and the common case is one comparison against the running Nth place before a row is rejected.Which systems publish defaults to the shard's own answer —
ShowOnLoyaltyGump— rather than a list invented here that would drift every time a subsystem is added.Bridge.cfg PointsSystems=overrides it; an unrecognised name is logged, never silently dropped.Entries are written inline as
{serial, name}, never viaBridgeJson.Actor. A board is the widest-audience surface the bridge has, soacct/webIddeliberately do not cross the wire; the site resolves serial → user from its own link mirror.char.profilegains apointsblockRead-model enrichment on an existing kind — the
titlesprecedent fromPROTOCOL_2.md§10.3, no new request kind.⚠️ This code must never call
PointsSystem.GetEntry/GetPoints, and the reason is worth a reviewer's attention. Both look like read accessors and both mutate the world:GetEntry(from, create: false)still callsAddEntrywhen the system hasAutoAdd(PointsSystem.cs:207), appending a row and firingOnPlayerAdded. Using them would have grown the points save file by up to ten rows every time anyone opened a character sheet. Hand-rolled read-only scan instead.rankis off by default (PointsProfileRank): 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.How it was tested
Verified by running it, not by reading it.
Compile — the whole
Scriptstree (6,207 files) compiles clean against real ServUO 57.4 assemblies:dotnet build Scripts/Scripts.csproj -c Release→ Build succeeded, 0 Warning(s), 0 Error(s). (Note the trapBRIDGE_PLUGIN_PLAN.md§1 warns about: the shard's dynamic rebuild can't overwriteServUO.exewhile it is running, so the first boot after a script change silently reloads the previousScripts.dll. Confirmed the compile banner and the new code being live —/rulesetanswering — rather than trusting the absence of errors.)Runtime, against the real shard — booted the local ServUO tree with a 43,011-mobile / 209,116-item world against the Rust sidecar, and let a sweep run. Five live boards emitted:
QueensLoyalty,VoidPool,DespiseCrystals,ShameCrystals,ViceVsVirtue.That run caught a bug no fake-shard test could. ServUO's idiom for an uncapped system is
MaxPoints = double.MaxValue(DespiseCrystals,ShameCrystals,VoidPoolall use it), and(long)on that in C# is an unchecked conversion — it doesn't throw, it yieldslong.MinValue. The first live sweep published:Fixed with
Cap()/Score()converters that normalise anything unrepresentable to0, now the wire's documented "uncapped" value. Re-deployed, rebuilt, re-booted, re-swept:Worth flagging for review because it inverts the obvious reading: on a real shard
maxPoints: 0is the common case, not an edge case. The same run also showed four of five boards sendingnameString: nullwith only a cliloc — so the consumer-side "humanise the system key" fallback is the primary display path, not a defensive nicety. Both are documented in docs #69.Checklist
AI-assisted contributions (required)
Claude Code (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with aCo-Authored-Bytrailer.License
Protocol 3.0 §7 (docs/link/v3.md). ServUO carries ~25 separate point currencies — Queen's Loyalty, Void Pool, Casino, Clean Up Britannia, the nine city loyalties, the Doom/Khaldun/Kotl treasure systems — every one a standing players build over months, and none of them visible outside an in-game gump until now. BridgePoints.cs - A diff sweep shaped like BridgeHousing: ServerStarted arms the timer, a sidecar connect clears the diff state so a fresh sidecar gets every board, and each pass emits only the systems whose top N or participant count moved. One ~600 B frame per system rather than one 12 KB frame, matching champ.update / guild.update. No points.remove — the system set is fixed at startup by PointsSystem.Configure, the same argument city.update makes. - Selection is a single bounded pass into a fixed N-element array kept sorted by insertion, NOT OrderByDescending().Take(N). PlayerTable is a plain List and ten of the ~25 systems have AutoAdd = true, so they hold a row for every character ever created: the naive version is ~25 full sorts on the Core thread, which BRIDGE_PLUGIN_PLAN.md §1 measured as the second thing in the bridge capable of blowing a frame budget. - Which systems publish defaults to the shard's OWN answer — ShowOnLoyaltyGump — rather than a list here that would drift; Bridge.cfg PointsSystems= overrides it, and an unrecognised name is logged rather than dropped. - Entries are written inline as {serial, name}, never via BridgeJson.Actor. A board is the widest-audience surface the bridge has, so acct/webId deliberately do not cross the wire; the site resolves serial → user from its own link mirror. char.profile gains a points block, the titles precedent from PROTOCOL_2.md §10.3 - Never uses PointsSystem.GetEntry/GetPoints: both MUTATE THE WORLD, since GetEntry(create: false) still calls AddEntry when the system has AutoAdd (PointsSystem.cs:207). Using them would have appended up to ten rows to the points save file every time anyone opened a character sheet. Hand-rolled read-only scan instead. - rank is off by default (PointsProfileRank). A points lookup stops at the character's own row; a rank must count every row that beats them, in every system, on every profile build. Verified by running it, not by reading it: the whole Scripts tree (6,207 files) compiles clean against real ServUO 57.4 assemblies, and a boot against the local shard with a 43,011-mobile world emitted five live boards. That run caught a bug no fake shard could — ServUO's uncapped idiom is MaxPoints = double.MaxValue, and (long) on it is an UNCHECKED conversion yielding long.MinValue, so the first sweep published "maxPoints": -9223372036854775808 for three of the five boards. Cap()/Score() now normalise anything unrepresentable, and maxPoints: 0 is the documented "uncapped" value — which on a real shard is the common case, not an edge case. Re-verified after the fix: 0 for the uncapped systems, 15000 and 10000 for the two that genuinely cap. Co-Authored-By: Claude <noreply@anthropic.com>