feat(protocol2): titles in char.profile (Part B ph.4)

Overlay BridgeProfile: char.profile gains a titles block (selected index,
fameKarma, skill, and the raw reward-title list) read from PlayerMobile's
public title accessors. No new stream, no sidecar change — it rides the
existing char.profile served by GET /char. Reward entries may be a cliloc
number as a string or a literal; resolve numeric ones website-side like item
names.

Docs: INTEGRATION.md char.profile titles field; PROTOCOL_2 ph.4 built. Part B
phase 5 (Factions/VvV) remains deferred by owner decision.

Verified: overlay compiles in the full ServUO Scripts tree (0 errors, 0
warnings). Live run pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 10:12:01 -05:00
parent b858d526b8
commit e0445d3f94
3 changed files with 52 additions and 3 deletions

View File

@@ -345,7 +345,9 @@ Full character sheet: stats, all trained skills, worn equipment with flattened i
{ "serial":"0x4002B3","layer":"OneHanded","itemId":5046,"hue":0,"cliloc":1023721, { "serial":"0x4002B3","layer":"OneHanded","itemId":5046,"hue":0,"cliloc":1023721,
"weapon":{"minDamage":16,"maxDamage":18}, "weapon":{"minDamage":16,"maxDamage":18},
"mods":{"WeaponDamage":50,"HitLightning":40} } "mods":{"WeaponDamage":50,"HitLightning":40} }
] ],
"titles": { "selected": 0, "fameKarma": "Lord", "skill": "Grandmaster Swordsman",
"reward": ["1154060", "The Bold"] }
} }
``` ```
@@ -353,6 +355,7 @@ Field notes:
- `skills[].base` is trained value, `value` includes item/temp bonuses, `cap` is the cap. **Do not assume `base <= cap`** — GM characters can exceed it. - `skills[].base` is trained value, `value` includes item/temp bonuses, `cap` is the cap. **Do not assume `base <= cap`** — GM characters can exceed it.
- `equipment[].mods` is a flattened map of every non-zero AOS attribute on the item (weapon or armor). Empty `{}` for plain items. - `equipment[].mods` is a flattened map of every non-zero AOS attribute on the item (weapon or armor). Empty `{}` for plain items.
- Item names are usually **clilocs**, not strings: use `name` when present, otherwise resolve `cliloc` against a UO cliloc table on the site. - Item names are usually **clilocs**, not strings: use `name` when present, otherwise resolve `cliloc` against a UO cliloc table on the site.
- `titles` (Protocol 2.0): `selected` is the index into `reward` currently displayed (`-1` if none). `fameKarma`/`skill` are computed display titles, omitted when the character has none. `reward` entries may be a **cliloc number as a string** or a literal string — resolve numeric ones against your cliloc table, same as item names.
- Errors: unknown account → **404** `{"kind":"bridge.error","reason":"unknown account"}`; bad slot → **404**/**400** similarly. - Errors: unknown account → **404** `{"kind":"bridge.error","reason":"unknown account"}`; bad slot → **404**/**400** similarly.
### Account roster ### Account roster

View File

@@ -383,8 +383,8 @@ If the website mirrors rosters/links (it does — `store.record_link`), it must
1. ~~**Guilds + governors.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgeSocial.cs` (guild sweep + `JoinGuild``guild.update`/`guild.remove`/`guild.join`) and `BridgeGovernance.cs` (city sweep → `city.update`, gated on `CityLoyaltySystem.Enabled`), `GuildSweepSeconds` (60s) / `CitySweepSeconds` (300s) config, both wired into `[bridge reload|sweepnow|status`. Sidecar `guilds`/`governors` board tables + `GET /guilds`, `/governors` served from the store (the §12.2 snapshot rule). Shared `BridgeJson.Actor` writer (serial/name/acct/webId/player). **Deviation from the §10 sketch:** the wire uses full-state `guild.update`/`city.update` upserts (website derives "created"/"governor changed" from the board) rather than discrete `guild.created`/`city.governor` events — this avoids a reconnect re-emit looking like a storm of creations, matching the proven `champ.update` model. *Live end-to-end run still pending.* 1. ~~**Guilds + governors.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgeSocial.cs` (guild sweep + `JoinGuild``guild.update`/`guild.remove`/`guild.join`) and `BridgeGovernance.cs` (city sweep → `city.update`, gated on `CityLoyaltySystem.Enabled`), `GuildSweepSeconds` (60s) / `CitySweepSeconds` (300s) config, both wired into `[bridge reload|sweepnow|status`. Sidecar `guilds`/`governors` board tables + `GET /guilds`, `/governors` served from the store (the §12.2 snapshot rule). Shared `BridgeJson.Actor` writer (serial/name/acct/webId/player). **Deviation from the §10 sketch:** the wire uses full-state `guild.update`/`city.update` upserts (website derives "created"/"governor changed" from the board) rather than discrete `guild.created`/`city.governor` events — this avoids a reconnect re-emit looking like a storm of creations, matching the proven `champ.update` model. *Live end-to-end run still pending.*
2. ~~**Presence.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgePresence.cs`: a `presence.online` sweep (total + per-facet + per-region, emitted on change) and real-time `region.enter` (`EventSink.OnEnterRegion`, player-filtered). `PresenceSweepSeconds` (30s), wired into `[bridge`. `GET /online` serves the latest snapshot from the event store (population series via `/history?kind=presence.online`). *Live run pending.* 2. ~~**Presence.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgePresence.cs`: a `presence.online` sweep (total + per-facet + per-region, emitted on change) and real-time `region.enter` (`EventSink.OnEnterRegion`, player-filtered). `PresenceSweepSeconds` (30s), wired into `[bridge`. `GET /online` serves the latest snapshot from the event store (population series via `/history?kind=presence.online`). *Live run pending.*
3. ~~**Housing registry.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgeHousing.cs`: a house sweep over `BaseHouse.AllHouses``house.update`/`house.remove` (owner, region, location, decay, co-owners, friends, price), complementing the existing `house.decay` transition feed. `HousingSweepSeconds` (300s), wired into `[bridge`. Sidecar `houses` board + `GET /houses`. (Stock ServUO has no "for sale" flag, so this is owner→houses; `price` is the placement value, not a listing.) *Live run pending.* 3. ~~**Housing registry.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgeHousing.cs`: a house sweep over `BaseHouse.AllHouses``house.update`/`house.remove` (owner, region, location, decay, co-owners, friends, price), complementing the existing `house.decay` transition feed. `HousingSweepSeconds` (300s), wired into `[bridge`. Sidecar `houses` board + `GET /houses`. (Stock ServUO has no "for sale" flag, so this is owner→houses; `price` is the placement value, not a listing.) *Live run pending.*
4. **Titles.** `char.profile` `titles` block (§10.3) — no new stream, folds into `BridgeProfile`. 4. ~~**Titles.**~~ **Built (2026-07-17), compiles clean.** `char.profile` gains a `titles` block (`selected`, `fameKarma`, `skill`, `reward[]`) from `PlayerMobile` accessors — no new stream, folds into `BridgeProfile`. *Live run pending.*
5. **Factions/VvV** — only after confirming which system the shard runs; stream just the enabled one. 5. **Factions/VvV** **deferred** (owner decision): only after confirming which system the shard runs; stream just the enabled one.
Cross-cutting, lands with Phase 1: the **protocol bump to 2** (§12.1) and the **snapshot-companion rule** (§12.2). The provisioning siblings (§12.3) and mirror-hygiene (§12.4) attach to Part A's phasing since they extend the `account.*` surface. Cross-cutting, lands with Phase 1: the **protocol bump to 2** (§12.1) and the **snapshot-companion rule** (§12.2). The provisioning siblings (§12.3) and mirror-hygiene (§12.4) attach to Part A's phasing since they extend the `account.*` surface.

View File

@@ -98,9 +98,55 @@ namespace Server.Custom.Bridge
} }
sb.Append(']'); sb.Append(']');
WriteTitles(sb, m);
return sb.End(); return sb.End();
} }
/// <summary>
/// The titles a character holds (docs/PROTOCOL_2.md §10.3). `selected` is the index into
/// `reward` currently displayed (-1 if none). `fameKarma` and `skill` are the computed
/// display titles (may be absent). `reward` is the raw reward-title list — an entry may be
/// a cliloc number (as a string) or a literal string; resolve clilocs website-side.
/// </summary>
private static void WriteTitles(StringBuilder sb, PlayerMobile m)
{
sb.Append(",\"titles\":{\"selected\":").Append(m.SelectedTitle);
var fameKarma = m.FameKarmaTitle;
if (!String.IsNullOrEmpty(fameKarma))
{
sb.Append(",\"fameKarma\":");
BridgeJson.Escape(sb, fameKarma);
}
var skill = m.PaperdollSkillTitle;
if (!String.IsNullOrEmpty(skill))
{
sb.Append(",\"skill\":");
BridgeJson.Escape(sb, skill);
}
sb.Append(",\"reward\":[");
var rewards = m.RewardTitles;
if (rewards != null)
{
bool first = true;
for (int i = 0; i < rewards.Count; i++)
{
var r = rewards[i];
if (r == null)
continue;
if (!first) sb.Append(',');
first = false;
BridgeJson.Escape(sb, Convert.ToString(r, System.Globalization.CultureInfo.InvariantCulture));
}
}
sb.Append("]}");
}
private static bool IsGearLayer(Layer layer) private static bool IsGearLayer(Layer layer)
{ {
switch (layer) switch (layer)