Extracted docs/ (ADMIN_CONTROLS, INTEGRATION, PLAN, PROTOCOL_2, RESEARCH, SHARD_PREREQS) into the central RunicGateway/docs repo under link/, with full commit history preserved via git filter-repo. The source cites these design docs by section throughout, so every in-repo reference (C# + Rust comments, Bridge.cfg, and the READMEs) is repointed at the new docs-repo URL. README references are rendered as markdown links; a Documentation pointer section is added to the top-level README. Docs repo: https://gitea.whitlocktech.com/RunicGateway/docs
292 lines
11 KiB
C#
292 lines
11 KiB
C#
using System;
|
|
using System.Text;
|
|
|
|
using Server.Accounting;
|
|
using Server.Items;
|
|
using Server.Mobiles;
|
|
|
|
namespace Server.Custom.Bridge
|
|
{
|
|
/// <summary>
|
|
/// 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 https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md §1.
|
|
/// </summary>
|
|
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 https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The titles a character holds (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/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)
|
|
{
|
|
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 ----
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
}
|