Files
link/overlay/Scripts/Custom/Bridge/BridgeConfig.cs
Claude b258ee3e60 feat(champ): stream champion-spawn state to the sidecar board
Champion spawns have no ServUO EventSink, so add a fourth polled stream
(BridgeChamps) modeled on BridgeSweeps: enumerate every spawn each tick,
fold to a small record, and emit champ.update only on change. No core
patch — every field used is public.

Covers all three families via a `category` field:
  - champion: ChampionSpawn (type/level/kills/boss/cooldown ETA)
  - mini:     MiniChamp (type/level; auto-restarts, no kill counter)
  - sea:      BaseSeaChampion (a High Seas world-boss mobile, alive only
              while summoned; removed via champ.remove when slain)

Status folds to active/cooldown/dormant. A (re)connection clears the diff
cache so the next sweep re-emits the full board, rebuilding a sidecar that
restarted on its own. Transient entries leave via champ.remove.

Sidecar: a `champs` current-state table (one row per serial) fed by
champ.update (upsert) and champ.remove (delete), exposed at GET /champs as
the live board. New ChampSweepSeconds config (default 10s), wired into
[bridge reload/sweepnow/status. Documented in docs/INTEGRATION.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
2026-07-14 05:46:21 -05:00

104 lines
4.5 KiB
C#

using System;
namespace Server.Custom.Bridge
{
/// <summary>
/// Tunables from Config/Bridge.cfg. Key scope is the filename, so `Port=7788` there
/// reads as "Bridge.Port" here.
///
/// Loaded in Configure(), which ScriptCompiler invokes before World.Load.
/// </summary>
public static class BridgeConfig
{
public static string Host { get; private set; }
public static int Port { get; private set; }
public static int QueueCap { get; private set; }
public static int StatSweepSeconds { get; private set; }
public static int DecaySweepSeconds { get; private set; }
public static int EconomySweepSeconds { get; private set; }
public static int PageSweepSeconds { get; private set; }
public static int ChampSweepSeconds { get; private set; }
public static string LinkUrl { get; private set; }
public static int TownCrierMaxLines { get; private set; }
public static int TownCrierMaxLineLength { get; private set; }
public static int TownCrierMaxActive { get; private set; }
public static int TownCrierMaxDurationSec { get; private set; }
public static bool AdminWriteEnabled { get; private set; }
public static AccessLevel AdminAccessFloor { get; private set; }
public static int AdminBroadcastMaxLength { get; private set; }
public static int AdminReasonMaxLength { get; private set; }
public static int AdminBanMaxDurationSec { get; private set; }
public static bool Enabled { get; private set; }
public static void Configure()
{
Load();
}
/// <summary>Re-readable at runtime via `[bridge reload`.</summary>
public static void Load()
{
Enabled = Config.Get("Bridge.Enabled", true);
Host = Config.Get("Bridge.Host", "127.0.0.1");
Port = Config.Get("Bridge.Port", 7788);
QueueCap = Config.Get("Bridge.QueueCap", 10000);
StatSweepSeconds = Config.Get("Bridge.StatSweepSeconds", 30);
DecaySweepSeconds = Config.Get("Bridge.DecaySweepSeconds", 60);
EconomySweepSeconds = Config.Get("Bridge.EconomySweepSeconds", 300);
PageSweepSeconds = Config.Get("Bridge.PageSweepSeconds", 5);
if (PageSweepSeconds < 1)
PageSweepSeconds = 1;
ChampSweepSeconds = Config.Get("Bridge.ChampSweepSeconds", 10);
if (ChampSweepSeconds < 1)
ChampSweepSeconds = 1;
LinkUrl = Config.Get("Bridge.LinkUrl", "https://yoursite/link");
TownCrierMaxLines = Config.Get("Bridge.TownCrierMaxLines", 6);
TownCrierMaxLineLength = Config.Get("Bridge.TownCrierMaxLineLength", 200);
TownCrierMaxActive = Config.Get("Bridge.TownCrierMaxActive", 20);
TownCrierMaxDurationSec = Config.Get("Bridge.TownCrierMaxDurationSec", 86400);
AdminWriteEnabled = Config.Get("Bridge.AdminWriteEnabled", false);
AdminAccessFloor = ParseAccessLevel(Config.Get("Bridge.AdminAccessFloor", "CoOwner"), AccessLevel.CoOwner);
AdminBroadcastMaxLength = Config.Get("Bridge.AdminBroadcastMaxLength", 300);
AdminReasonMaxLength = Config.Get("Bridge.AdminReasonMaxLength", 400);
AdminBanMaxDurationSec = Config.Get("Bridge.AdminBanMaxDurationSec", 31536000);
if (QueueCap < 16)
QueueCap = 16;
}
/// <summary>
/// Parses an AccessLevel name from config, case-insensitively, falling back to the given
/// default on anything unrecognized so a typo can never open the floor wider than intended.
/// </summary>
private static AccessLevel ParseAccessLevel(string value, AccessLevel fallback)
{
AccessLevel parsed;
if (!String.IsNullOrEmpty(value) && Enum.TryParse(value.Trim(), true, out parsed) &&
Enum.IsDefined(typeof(AccessLevel), parsed))
return parsed;
Console.WriteLine("[Bridge] unrecognized AdminAccessFloor '{0}', using {1}", value, fallback);
return fallback;
}
public static string Describe()
{
return String.Format(
"enabled={0} endpoint={1}:{2} queueCap={3} sweeps(stat={4}s decay={5}s econ={6}s champ={7}s) adminWrite={8}(floor={9})",
Enabled, Host, Port, QueueCap, StatSweepSeconds, DecaySweepSeconds, EconomySweepSeconds,
ChampSweepSeconds, AdminWriteEnabled, AdminAccessFloor);
}
}
}