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
177 lines
6.3 KiB
C#
177 lines
6.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
using Server.Mobiles;
|
|
using Server.Services.TownCryer;
|
|
|
|
namespace Server.Custom.Bridge
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<string, TownCryerNewsEntry> _ours =
|
|
new Dictionary<string, TownCryerNewsEntry>(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<string, object> 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<string, object> 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");
|
|
}
|
|
}
|
|
|
|
/// <summary>Proclaims a single line — the article title — through the town criers.</summary>
|
|
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());
|
|
}
|
|
}
|
|
}
|