diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index ed397e2..9b2f943 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -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, "weapon":{"minDamage":16,"maxDamage":18}, "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. - `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. +- `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. ### Account roster diff --git a/docs/PROTOCOL_2.md b/docs/PROTOCOL_2.md index c03bb93..2635c11 100644 --- a/docs/PROTOCOL_2.md +++ b/docs/PROTOCOL_2.md @@ -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.* 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.* -4. **Titles.** `char.profile` `titles` block (§10.3) — no new stream, folds into `BridgeProfile`. -5. **Factions/VvV** — only after confirming which system the shard runs; stream just the enabled one. +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** — **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. diff --git a/overlay/Scripts/Custom/Bridge/BridgeProfile.cs b/overlay/Scripts/Custom/Bridge/BridgeProfile.cs index 7e12197..4e08ede 100644 --- a/overlay/Scripts/Custom/Bridge/BridgeProfile.cs +++ b/overlay/Scripts/Custom/Bridge/BridgeProfile.cs @@ -98,9 +98,55 @@ namespace Server.Custom.Bridge } sb.Append(']'); + WriteTitles(sb, m); + return sb.End(); } + /// + /// 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. + /// + 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) { switch (layer)