using System; using System.Collections.Generic; using Server.Mobiles; namespace Server.Custom.Bridge { /// /// Website-published news, pushed into the game's town criers. /// /// Inbound towncrier.add adds a global entry that every town crier announces until it /// expires; towncrier.remove pulls one early. Both run on the Core thread (inbound lines are /// marshaled through Timer.DelayCall before a handler sees them), which is required because /// AddEntry mutates a shared list and the criers send packets. /// /// Loopback is the trust boundary, but the caps here (line count/length, active-entry count, /// duration) are defense in depth: a compromised or buggy sidecar still cannot flood the /// criers or pin a message forever. /// public static class BridgeTownCrier { // Website id -> the entry we created for it, so a later remove can find it. private static readonly Dictionary _entries = new Dictionary(StringComparer.Ordinal); public static void Initialize() { if (!BridgeConfig.Enabled) return; BridgeBoot.RegisterHandler("towncrier.add", OnAdd); BridgeBoot.RegisterHandler("towncrier.remove", OnRemove); } private static void Reply(string kind, string id, string reason) { var sb = BridgeJson.Begin(kind); if (id != null) sb.Str("id", id); if (reason != null) sb.Str("reason", reason); BridgeLink.Emit(sb.End()); } private static void OnAdd(Dictionary o) { var id = BridgeJson.GetString(o, "id"); if (id == null) { Reply("towncrier.error", null, "missing id"); return; } var lines = BridgeJson.GetStringList(o, "lines"); if (lines.Count == 0) { Reply("towncrier.error", id, "no lines"); return; } if (lines.Count > BridgeConfig.TownCrierMaxLines) { Reply("towncrier.error", id, "too many lines"); return; } // Prune expired entries from our map before enforcing the active cap. PruneExpired(); // Replacing an existing id is fine; otherwise enforce the active cap. if (!_entries.ContainsKey(id) && _entries.Count >= BridgeConfig.TownCrierMaxActive) { Reply("towncrier.error", id, "too many active entries"); return; } var clean = new string[lines.Count]; for (int i = 0; i < lines.Count; i++) { var line = lines[i] ?? ""; if (line.Length > BridgeConfig.TownCrierMaxLineLength) line = line.Substring(0, BridgeConfig.TownCrierMaxLineLength); clean[i] = line; } int durationSec = BridgeJson.GetInt(o, "durationSec", 3600); if (durationSec < 1) durationSec = 1; if (durationSec > BridgeConfig.TownCrierMaxDurationSec) durationSec = BridgeConfig.TownCrierMaxDurationSec; try { // If this id already exists, replace it: remove the old entry first. TownCrierEntry old; if (_entries.TryGetValue(id, out old) && old != null) GlobalTownCrierEntryList.Instance.RemoveEntry(old); var entry = GlobalTownCrierEntryList.Instance.AddEntry(clean, TimeSpan.FromSeconds(durationSec)); _entries[id] = entry; Reply("towncrier.ok", id, null); } catch (Exception ex) { Console.WriteLine("[Bridge] towncrier.add threw: {0}", ex.Message); Reply("towncrier.error", id, "internal error"); } } private static void OnRemove(Dictionary o) { var id = BridgeJson.GetString(o, "id"); if (id == null) { Reply("towncrier.error", null, "missing id"); return; } TownCrierEntry entry; if (!_entries.TryGetValue(id, out entry)) { Reply("towncrier.error", id, "unknown id"); return; } _entries.Remove(id); try { if (entry != null) GlobalTownCrierEntryList.Instance.RemoveEntry(entry); Reply("towncrier.ok", id, null); } catch (Exception ex) { Console.WriteLine("[Bridge] towncrier.remove threw: {0}", ex.Message); Reply("towncrier.error", id, "internal error"); } } private static void PruneExpired() { var doomed = new List(); foreach (var kv in _entries) { if (kv.Value == null || kv.Value.Expired) doomed.Add(kv.Key); } foreach (var id in doomed) _entries.Remove(id); } } }