using System; using System.Text; using Server.Accounting; using Server.Items; using Server.Mobiles; namespace Server.Custom.Bridge { /// /// Builds the heavy read-models the website consumes: a full character profile, an account /// roster, and a player's vendor holdings. All read live Mobile/Item state, so all must run /// on the Core thread — which the inbound dispatch guarantees (BridgeLink marshals every /// inbound line through Timer.DelayCall before a handler sees it). /// /// A profile is the single most expensive read in the bridge (~0.07 ms + ~2.4 KB at the /// seeded scale, more for a fully-kitted character), so it is built on demand only, never in /// a sweep. See docs/PLAN.md §1. /// public static class BridgeProfile { private static readonly AosAttribute[] AllAttrs = (AosAttribute[])Enum.GetValues(typeof(AosAttribute)); private static readonly AosWeaponAttribute[] AllWeaponAttrs = (AosWeaponAttribute[])Enum.GetValues(typeof(AosWeaponAttribute)); private static readonly AosArmorAttribute[] AllArmorAttrs = (AosArmorAttribute[])Enum.GetValues(typeof(AosArmorAttribute)); // ---- full profile ---- public static string BuildProfile(PlayerMobile m, string reqId) { var sb = BridgeJson.Begin("char.profile"); if (reqId != null) sb.Str("reqId", reqId); sb.Ser("serial", m.Serial); sb.Str("name", m.Name); sb.Str("title", m.Title); sb.Num("body", m.Body.BodyID); sb.Num("hue", m.Hue); sb.Bool("online", m.NetState != null); var acct = m.Account as Account; if (acct != null) sb.Str("acct", acct.Username); // stats sb.Append(",\"stats\":{"); sb.Append("\"str\":").Append(m.Str).Append(",\"dex\":").Append(m.Dex).Append(",\"int\":").Append(m.Int); sb.Append(",\"hits\":").Append(m.Hits).Append(",\"hitsMax\":").Append(m.HitsMax); sb.Append(",\"mana\":").Append(m.Mana).Append(",\"manaMax\":").Append(m.ManaMax); sb.Append(",\"stam\":").Append(m.Stam).Append(",\"stamMax\":").Append(m.StamMax); sb.Append(",\"fame\":").Append(m.Fame).Append(",\"karma\":").Append(m.Karma); sb.Append(",\"luck\":").Append(m.Luck); sb.Append(",\"resist\":{\"phys\":").Append(m.PhysicalResistance); sb.Append(",\"fire\":").Append(m.FireResistance); sb.Append(",\"cold\":").Append(m.ColdResistance); sb.Append(",\"pois\":").Append(m.PoisonResistance); sb.Append(",\"energy\":").Append(m.EnergyResistance).Append("}}"); // skills: trained only (Base > 0), to avoid ~50 zeroes per character sb.Append(",\"skills\":["); bool first = true; for (int i = 0; i < m.Skills.Length; i++) { var s = m.Skills[i]; if (s == null || s.Base <= 0.0) continue; if (!first) sb.Append(','); first = false; sb.Append("{\"n\":\"").Append(s.SkillName).Append('"'); sb.Append(",\"base\":").Append(s.Base.ToString("F1")); sb.Append(",\"value\":").Append(s.Value.ToString("F1")); sb.Append(",\"cap\":").Append(s.Cap.ToString("F1")); sb.Append(",\"lock\":\"").Append(s.Lock).Append("\"}"); } sb.Append(']'); // worn equipment only — not the backpack/bank (see docs/PLAN.md §IV.4) sb.Append(",\"equipment\":["); first = true; foreach (var item in m.Items) { if (item == null || !IsGearLayer(item.Layer)) continue; if (!first) sb.Append(','); first = false; WriteItem(sb, item); } 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) { case Layer.Backpack: case Layer.Bank: case Layer.Hair: case Layer.FacialHair: case Layer.Mount: case Layer.Invalid: return false; default: return true; } } private static void WriteItem(StringBuilder sb, Item item) { sb.Append("{"); sb.Append("\"serial\":\"0x").Append(item.Serial.Value.ToString("X")).Append('"'); sb.Append(",\"layer\":\"").Append(item.Layer).Append('"'); sb.Append(",\"itemId\":").Append(item.ItemID); sb.Append(",\"hue\":").Append(item.Hue); sb.Append(",\"cliloc\":").Append(item.LabelNumber); if (item.Name != null) { sb.Append(",\"name\":"); BridgeJson.Escape(sb, item.Name); } var weapon = item as BaseWeapon; var armor = item as BaseArmor; if (weapon != null) { sb.Append(",\"weapon\":{\"minDamage\":").Append(weapon.MinDamage); sb.Append(",\"maxDamage\":").Append(weapon.MaxDamage).Append('}'); } else if (armor != null) { sb.Append(",\"armor\":{\"baseRating\":").Append(armor.BaseArmorRating).Append('}'); } // flattened union of non-zero mods across every attribute bag sb.Append(",\"mods\":{"); bool first = true; if (weapon != null) { WriteAttrs(sb, weapon.Attributes, ref first); WriteWeaponAttrs(sb, weapon.WeaponAttributes, ref first); } else if (armor != null) { WriteAttrs(sb, armor.Attributes, ref first); WriteArmorAttrs(sb, armor.ArmorAttributes, ref first); } sb.Append("}}"); } private static void WriteAttrs(StringBuilder sb, AosAttributes a, ref bool first) { if (a == null) return; for (int i = 0; i < AllAttrs.Length; i++) { int v = a[AllAttrs[i]]; if (v == 0) continue; if (!first) sb.Append(','); first = false; sb.Append('"').Append(AllAttrs[i]).Append("\":").Append(v); } } private static void WriteWeaponAttrs(StringBuilder sb, AosWeaponAttributes a, ref bool first) { if (a == null) return; for (int i = 0; i < AllWeaponAttrs.Length; i++) { int v = a[AllWeaponAttrs[i]]; if (v == 0) continue; if (!first) sb.Append(','); first = false; sb.Append('"').Append(AllWeaponAttrs[i]).Append("\":").Append(v); } } private static void WriteArmorAttrs(StringBuilder sb, AosArmorAttributes a, ref bool first) { if (a == null) return; for (int i = 0; i < AllArmorAttrs.Length; i++) { int v = a[AllArmorAttrs[i]]; if (v == 0) continue; if (!first) sb.Append(','); first = false; sb.Append('"').Append(AllArmorAttrs[i]).Append("\":").Append(v); } } // ---- account roster ---- /// /// Light per-character summary for an account. Offline characters are included: a /// logged-off mobile stays resident (World.Mobiles) until Delete, so its roster entry is /// always available. /// public static string BuildRoster(Account acct, string reqId) { var sb = BridgeJson.Begin("account.roster"); if (reqId != null) sb.Str("reqId", reqId); sb.Str("acct", acct.Username); sb.Append(",\"chars\":["); bool first = true; for (int i = 0; i < acct.Length; i++) { var m = acct[i]; if (m == null) continue; if (!first) sb.Append(','); first = false; sb.Append("{\"slot\":").Append(i); sb.Append(",\"serial\":\"0x").Append(m.Serial.Value.ToString("X")).Append('"'); sb.Append(",\"name\":"); BridgeJson.Escape(sb, m.Name ?? ""); sb.Append(",\"body\":").Append(m.Body.BodyID); sb.Append(",\"online\":").Append(m.NetState != null ? "true" : "false"); sb.Append('}'); } sb.Append(']'); return sb.End(); } } }