using System; using System.Collections.Generic; using Server.Accounting; using Server.Mobiles; using Server.Multis; namespace Server.Custom.Bridge { /// /// Inbound request/response. The sidecar asks; the shard answers. Every handler runs on the /// Core thread (BridgeBoot dispatches inbound lines through Timer.DelayCall first), so all of /// these may read live world state freely. /// /// A request carries an optional "reqId" the shard echoes back, so the sidecar can correlate /// the reply with the request it sent. A malformed or unresolvable request gets a /// "bridge.error" reply rather than silence, so the website can show a real failure instead /// of hanging. /// public static class BridgeRequests { public static void Initialize() { if (!BridgeConfig.Enabled) return; BridgeBoot.RegisterHandler("char.request", OnCharRequest); BridgeBoot.RegisterHandler("account.roster", OnRosterRequest); BridgeBoot.RegisterHandler("vendor.snapshot", OnVendorSnapshotRequest); } private static void Fail(string reqId, string reason) { var sb = BridgeJson.Begin("bridge.error"); if (reqId != null) sb.Str("reqId", reqId); sb.Str("reason", reason); BridgeLink.Emit(sb.End()); } // ---- char.request ---- /// /// Resolve a character by serial, or by account + slot, and reply with a full profile. /// Works for offline characters too: a logged-off mobile is still resident. /// private static void OnCharRequest(Dictionary o) { var reqId = BridgeJson.GetString(o, "reqId"); PlayerMobile pm = null; var serialStr = BridgeJson.GetString(o, "serial"); if (serialStr != null) { pm = ResolveSerial(serialStr) as PlayerMobile; if (pm == null) { Fail(reqId, "no player with serial " + serialStr); return; } } else { var acctName = BridgeJson.GetString(o, "account"); var acct = acctName == null ? null : Accounting.Accounts.GetAccount(acctName) as Account; if (acct == null) { Fail(reqId, "unknown account"); return; } int slot = BridgeJson.GetInt(o, "slot", 0); if (slot < 0 || slot >= acct.Length) { Fail(reqId, "slot out of range"); return; } pm = acct[slot] as PlayerMobile; if (pm == null) { Fail(reqId, "no character in slot " + slot); return; } } BridgeLink.Emit(BridgeProfile.BuildProfile(pm, reqId)); } // ---- account.roster ---- private static void OnRosterRequest(Dictionary o) { var reqId = BridgeJson.GetString(o, "reqId"); var acctName = BridgeJson.GetString(o, "account"); var acct = acctName == null ? null : Accounting.Accounts.GetAccount(acctName) as Account; if (acct == null) { Fail(reqId, "unknown account"); return; } BridgeLink.Emit(BridgeProfile.BuildRoster(acct, reqId)); } // ---- vendor.snapshot ---- /// /// Every player vendor owned by any character on an account, with its held gold and /// priced listings. Enumerates PlayerVendor.PlayerVendors and matches by owner account. /// private static void OnVendorSnapshotRequest(Dictionary o) { var reqId = BridgeJson.GetString(o, "reqId"); var acctName = BridgeJson.GetString(o, "account"); var acct = acctName == null ? null : Accounting.Accounts.GetAccount(acctName) as Account; if (acct == null) { Fail(reqId, "unknown account"); return; } var sb = BridgeJson.Begin("vendor.snapshot"); if (reqId != null) sb.Str("reqId", reqId); sb.Str("acct", acct.Username); sb.Append(",\"vendors\":["); bool firstVendor = true; var all = PlayerVendor.PlayerVendors; if (all != null) { foreach (var v in all) { if (v == null || v.Deleted || v.Owner == null) continue; if (!(v.Owner.Account is Account ownerAcct) || ownerAcct != acct) continue; if (!firstVendor) sb.Append(','); firstVendor = false; sb.Append("{\"serial\":\"0x").Append(v.Serial.Value.ToString("X")).Append('"'); sb.Append(",\"shopName\":"); BridgeJson.Escape(sb, v.ShopName ?? ""); sb.Append(",\"holdGold\":").Append(v.HoldGold); sb.Append(",\"ownerSerial\":\"0x").Append(v.Owner.Serial.Value.ToString("X")).Append('"'); var house = v.Map; sb.Append(",\"map\":"); BridgeJson.Escape(sb, v.Map == null ? "" : v.Map.Name); sb.Append(",\"x\":").Append(v.X).Append(",\"y\":").Append(v.Y); sb.Append(",\"listings\":["); bool firstItem = true; var pack = v.Backpack; if (pack != null) { foreach (var item in pack.Items) { var vi = v.GetVendorItem(item); if (vi == null) continue; if (!firstItem) sb.Append(','); firstItem = false; sb.Append("{\"serial\":\"0x").Append(item.Serial.Value.ToString("X")).Append('"'); sb.Append(",\"itemId\":").Append(item.ItemID); sb.Append(",\"amount\":").Append(item.Amount); sb.Append(",\"price\":").Append(vi.Price); sb.Append(",\"forSale\":").Append(vi.IsForSale ? "true" : "false").Append('}'); } } sb.Append("]}"); } } sb.Append(']'); BridgeLink.Emit(sb.End()); } // ---- helpers ---- private static Mobile ResolveSerial(string serialStr) { try { var s = serialStr.Trim(); int value; if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) value = Convert.ToInt32(s.Substring(2), 16); else value = Convert.ToInt32(s, 10); return World.FindMobile(value); } catch { return null; } } } }