using System; using System.Collections.Generic; using Server.Mobiles; using Server.Services.TownCryer; namespace Server.Custom.Bridge { /// /// Website news articles pushed into the modern Town Cryer News gump /// (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §16). Distinct from BridgeTownCrier, which drives the scrolling-crier /// announcement lines (GlobalTownCrierEntryList). Here the full article — title, body (HTML), /// image, and a "more info" URL — becomes a TownCryerNewsEntry in TownCryerSystem.NewsEntries, /// which the stock news gumps already render (they branch on TextDefinition.Number, so string /// content needs no gump change). /// /// No stock edit: NewsEntries is a public mutable list, so we insert/remove directly and keep /// our own id -> entry map, leaving the stock entries untouched. On add we also proclaim just /// the title through the existing crier say path (default on), so players hear it in-world. /// /// Everything runs on the Core thread (inbound lines are marshaled through Timer.DelayCall), /// which is required to touch the shared news list and to send crier packets. /// public static class BridgeNews { // A neutral scroll gump when the website supplies no image. private const int DefaultImage = 0x64E; // Website id -> the news entry we created for it, so a later remove/replace can find it. private static readonly Dictionary _ours = new Dictionary(StringComparer.Ordinal); public static void Initialize() { if (!BridgeConfig.Enabled) return; BridgeBoot.RegisterHandler("news.add", OnAdd); BridgeBoot.RegisterHandler("news.remove", OnRemove); } private static void OnAdd(Dictionary o) { var id = BridgeJson.GetString(o, "id"); if (id == null) { Reply("news.error", null, "missing id"); return; } var list = TownCryerSystem.NewsEntries; if (list == null) { Reply("news.error", id, "town cryer unavailable"); return; } var title = BridgeJson.GetString(o, "title"); if (String.IsNullOrEmpty(title)) { Reply("news.error", id, "missing title"); return; } var body = BridgeJson.GetString(o, "body") ?? ""; var url = BridgeJson.GetString(o, "url"); int image = BridgeJson.GetInt(o, "image", DefaultImage); // announce defaults to true (proclaim the title in-world); "announce":false suppresses it. bool announce = true; object rawAnnounce; if (o.TryGetValue("announce", out rawAnnounce) && rawAnnounce is bool) announce = (bool)rawAnnounce; if (title.Length > BridgeConfig.NewsMaxTitleLength) title = title.Substring(0, BridgeConfig.NewsMaxTitleLength); if (body.Length > BridgeConfig.NewsMaxBodyLength) body = body.Substring(0, BridgeConfig.NewsMaxBodyLength); try { // Replace an existing id in place: drop the old entry first. TownCryerNewsEntry old; if (_ours.TryGetValue(id, out old) && old != null) { list.Remove(old); _ours.Remove(id); } else if (_ours.Count >= BridgeConfig.NewsMaxExternal) { Reply("news.error", id, "too many news entries"); return; } var entry = new TownCryerNewsEntry( new TextDefinition(title), new TextDefinition(body), image, null, url); list.Insert(0, entry); // newest first, as the gump reads top-down _ours[id] = entry; if (announce) Announce(title); Reply("news.ok", id, null); } catch (Exception ex) { Console.WriteLine("[Bridge] news.add threw: {0}", ex.Message); Reply("news.error", id, "internal error"); } } private static void OnRemove(Dictionary o) { var id = BridgeJson.GetString(o, "id"); if (id == null) { Reply("news.error", null, "missing id"); return; } TownCryerNewsEntry entry; if (!_ours.TryGetValue(id, out entry)) { Reply("news.error", id, "unknown id"); return; } _ours.Remove(id); try { var list = TownCryerSystem.NewsEntries; if (list != null && entry != null) list.Remove(entry); Reply("news.ok", id, null); } catch (Exception ex) { Console.WriteLine("[Bridge] news.remove threw: {0}", ex.Message); Reply("news.error", id, "internal error"); } } /// Proclaims a single line — the article title — through the town criers. private static void Announce(string title) { try { GlobalTownCrierEntryList.Instance.AddEntry( new[] { title }, TimeSpan.FromSeconds(BridgeConfig.NewsAnnounceDurationSec)); } catch (Exception ex) { // A failed proclamation must not fail the news add — the article is already posted. Console.WriteLine("[Bridge] news announce threw: {0}", ex.Message); } } 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()); } } }