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:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user