feat(bridge): carry guild rank on roster members #13
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/protocol4-guild-rank"
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?
Amends Protocol 4 in place — no bump. The protocol is on
edgeand unreleased, and a bump is owed only once one has reachedmain.PROTOCOL_VERSIONandoverlay.tomlboth stay at 4.Spec: docs #155 · pairs with Module-uo #10
Why
Phase 1 shipped the roster member as the standard actor object, which carries no guild rank. Teams phase 2 found the consequence: the website could learn leadership only from the board's single
leaderfield, sogetTeamLeaders()could name exactly one member — while a UO guild routinely has several at rank 4, andTEAMS.md§2.5 treats multiple leaders as the normal case.What
Roster members carry
rank(0–4, 4 being Leader perRankDefinition.Ranks) plusrankCliloc, orrankNamewhen a custom rank definition uses a literal string.Only the raw rank goes on the wire. ServUO names the five stock ranks with clilocs and ships no text for them, so this shard cannot produce "Warlord" without a client-file table it does not have. The website module does have one, and resolving a game term is its job anyway.
withGuildRankis a parameter onActors()rather than a change to the shared actor writer — rank is a property of a mobile's membership of this guild, not of the mobile, and every other actor this bridge writes is a bystander, a killer or a governor.WriteActoris split into a fields-only writer so both forms share one definition.The trap this found — worth the review time
PlayerMobile.GuildRankreturnsRankDefinition.Leaderfor anyone at GameMaster or above, whatever their actual rank. It is a gameplay convenience so staff can operate a guild stone, and emphatically not a claim about who leads the guild — but it is what the only public accessor returns, and the true value sits in a private field with no getter.Emitting it verbatim would have published every staff member in a guild as a guild leader on a public website. Staff are therefore written with no rank fields at all. A staff account that genuinely leads its guild shows as an unranked member — a visible gap rather than a false claim, which is the right way round given that roster reaches a public page.
Verification
No CI build here, so compiling is not evidence. Run against the local ServUO tree with a throwaway probe that synthesised a guild from real
PlayerMobiles across the rank ladder, with one account promoted to GameMaster. The probe printed stored vs reported rank per member, so the getter's substitution is recorded rather than inferred:The frame that came out the other end, captured off the socket:
Line parsed as valid JSON.
dotnet build Scripts.csprojclean, 0 warnings. Probe deleted, tree rebuilt,deploy.ps1 -Verifyreports 0 changes against the overlay. Shard killed without a world save, so the synthetic guild did not persist (Guilds.binstill 0 bytes).The sidecar needs no change and no store migration — it treats roster members as opaque values and never reads a field inside one. That is the forwarder design doing its job, and it is now written down in the spec for the next person who adds a member field.
AI-assisted: written with Claude Code. Commits carry
Co-Authored-By: Claude <noreply@anthropic.com>.Protocol 4 is still on `edge` and unreleased, so this amends it in place rather than bumping: `PROTOCOL_VERSION` and `overlay.toml` both stay at 4. A bump is only owed once a protocol has reached `main`. Phase 1 shipped the roster member as the standard actor object, which carries no guild rank. The consequence surfaced in Teams phase 2: the website could only learn leadership from the board's single `leader` field, so `getTeamLeaders()` could return exactly one member — while a UO guild routinely has several at rank 4, and TEAMS.md §2.5 treats multiple leaders as the normal case. Roster members now carry `rank` (0-4, 4 being Leader per RankDefinition.Ranks) plus `rankCliloc`, or `rankName` when a custom rank definition uses a literal string instead of a cliloc. Only the raw rank goes on the wire: ServUO names the five standard ranks with cliloc ids and ships no text for them, so this shard cannot produce "Warlord" without a client-file table it does not have. The website module has one, and resolving a game term is its job in any case. `withGuildRank` is a parameter on `Actors()` rather than a change to the shared actor writer. Rank is a property of a mobile's membership of THIS guild, not of the mobile, and every other actor this bridge writes is a bystander, a killer or a governor, where guild rank is meaningless. `WriteActor` is split into a fields-only writer so both forms share one definition of an actor. ## The trap this found **`PlayerMobile.GuildRank` returns `RankDefinition.Leader` for anyone at GameMaster or above, whatever their actual rank.** It is a gameplay convenience so staff can operate a guild stone, and it is emphatically not a claim about who leads the guild -- but it is what the only public accessor returns, and the true value sits in a private field. Emitting it verbatim would have published every staff member in a guild as a guild leader on a public website. Staff are therefore written with no rank fields at all. A staff account that genuinely leads its guild shows as an unranked member, which is a visible gap rather than a false claim -- the right way round, given the name on that roster reaches a public page. ## Verification This repo has no CI build, so compiling is not evidence. Run against the local ServUO tree with a throwaway probe that synthesised a guild from real PlayerMobiles across the rank ladder, with one account promoted to GameMaster. The emitted frame: tester rank 4 cliloc 1062959 (Leader) Seed000A rank 4 cliloc 1062959 (Leader) <- two at once, the point of this Seed000B rank 3 cliloc 1062960 (Warlord) Seed000C rank 2 cliloc 1062961 (Emissary) Seed001A rank 1 cliloc 1062962 (Member) Seed001B no rank fields <- GameMaster, stored rank 0, getter reported rank 4 The probe printed stored vs reported rank per member, so the getter's substitution is recorded rather than inferred: `Seed001B storedRank=0 reportedRank=4 access=GameMaster`. The line parsed as valid JSON. `dotnet build Scripts.csproj` clean, 0 warnings. Probe deleted, tree rebuilt, and `deploy.ps1 -Verify` reports 0 changes against the overlay. The shard was killed without a world save, so the synthetic guild did not persist (Guilds.bin still 0 bytes). **The sidecar needs no change.** It treats roster members as opaque values and never reads a field inside one -- `accumulate_roster` moves them and `upsert_guild_roster` stores them, both by value. That is the forwarder design paying off. Refs docs/link/v4.md §2.3 Co-Authored-By: Claude <noreply@anthropic.com>