Phase 6: town-crier news (website -> game)

BridgeTownCrier handles inbound towncrier.add / towncrier.remove, pushing
website-published news into GlobalTownCrierEntryList so every town crier
announces it until it expires. Both run on the Core thread (required: AddEntry
mutates a shared list and the criers send packets). An id maps to the created
TownCrierEntry so a later remove can pull it, and re-adding an id replaces the
prior entry.

Caps are enforced before touching the shared list -- line count, line length,
active-entry count, duration -- as defense in depth on top of the loopback trust
boundary: a buggy or compromised sidecar still cannot flood the criers or pin a
message forever. Config: Bridge.TownCrierMax{Lines,LineLength,Active,DurationSec}.

Verified with a sending stub and a probe that logs the actual crier list. Replies
and game state agree: add n1 -> towncrier.ok and the entry appears with the exact
lines; add n2 (8 lines over the cap of 6) -> towncrier.error and never enters the
list; remove n1 -> towncrier.ok and the entry is gone; remove unknown ->
towncrier.error. Evidence in docs/PLAN.md §16.

Adds BridgeJson.GetStringList for JSON string arrays, tools/stub_sidecar_crier.ps1,
and tools/scaffolding/BridgeCrierProbe.cs. This closes the pure-plugin inbound
work; only the PlayerVendorSale core edit (Phase 7) remains on the ServUO side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 11:56:22 -05:00
parent be05ea912c
commit b8058094f0
9 changed files with 351 additions and 0 deletions

View File

@@ -149,6 +149,35 @@ namespace Server.Custom.Bridge
return v as string ?? Convert.ToString(v, CultureInfo.InvariantCulture);
}
/// <summary>
/// Extracts a JSON array of strings. JavaScriptSerializer materializes JSON arrays as
/// object[] (or ArrayList) when the target is object, so handle both and stringify each
/// element. Returns an empty list for a missing or non-array value, never null.
/// </summary>
public static List<string> GetStringList(Dictionary<string, object> o, string key)
{
var result = new List<string>();
object v;
if (o == null || !o.TryGetValue(key, out v) || v == null)
return result;
var enumerable = v as System.Collections.IEnumerable;
if (enumerable == null || v is string)
return result;
foreach (var item in enumerable)
{
if (item == null)
continue;
result.Add(item as string ?? Convert.ToString(item, CultureInfo.InvariantCulture));
}
return result;
}
public static int GetInt(Dictionary<string, object> o, string key, int fallback)
{
object v;