feat(bridge)!: protocol 4 — guild rosters and per-member leaves (Teams cutover 1/6) #14
Reference in New Issue
Block a user
No description provided.
Delete Branch "edge"
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?
Cutover 1 of 6. The shard half of the Teams bet reaching
main.Two commits, both from Teams phase 1:
guild.roster— a new kind carrying the actual member set, which the sweep already had to compute a signature over. Emitted when the set changes and on the reconnect baseline.acct/webIdonly for members whose game account is linked.guild.leave— the missing counterpart to the existingguild.join, computable from the same set diff. Before this, a leave surfaced only as the member count dropping in the nextguild.update.guild.updateis unchanged, so nothing that reads it today has to change.overlay.tomldeclaresprotocol = 4andlink'sPROTOCOL_VERSIONis 4 on itsedge— the pair CI publishes as a bundle only composes if both land. Merge this withRunicGateway/link(cutover 2/6), before the installer.Spec of record:
docs/link/v4.md.Verification
Phase 0 was a throwaway spike run against the real ServUO tree and the real Rust sidecar precisely because this repo has no CI build and a dynamic rebuild can silently reload a stale
Scripts.dll. The roster landed in the store, survived a sidecar restart and came back out ofGET /guilds. Phase 11's rig walk exercised the same path again through the website.AI disclosure
Written with Claude Code (Opus 5). Commits carry the
Co-Authored-Bytrailer.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>