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());
}
}
}