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
176 lines
6.0 KiB
C#
176 lines
6.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
using Server.Engines.CityLoyalty;
|
|
|
|
namespace Server.Custom.Bridge
|
|
{
|
|
/// <summary>
|
|
/// The town-governor stream (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §10.2). In modern ServUO the "mayor of a
|
|
/// town" is the Governor in the City Loyalty System (King Blackthorn's governance): each of
|
|
/// the governed cities has a Governor, a GovernorElect, and an Election. None of these raises
|
|
/// an EventSink, so — like <see cref="BridgeChamps"/> and <see cref="BridgeSocial"/> — the set
|
|
/// is polled and each city emits `city.update` only when its signature changes. Governors turn
|
|
/// over on the order of weeks, so a slow sweep (default 5 min) is ample.
|
|
///
|
|
/// The wire model is uniform with the rest of Part B: a full-state `city.update` upsert, with
|
|
/// "the governor changed" derived sidecar-side by comparing to the stored board — rather than a
|
|
/// discrete from→to event, which a sidecar reconnect (cache cleared, full re-emit) would
|
|
/// otherwise fire spuriously for every city.
|
|
///
|
|
/// Gated on CityLoyaltySystem.Enabled: a shard running its own town system emits nothing here.
|
|
/// </summary>
|
|
public static class BridgeGovernance
|
|
{
|
|
private static Timer _timer;
|
|
|
|
// City enum value -> last-emitted signature.
|
|
private static readonly Dictionary<int, string> _last = new Dictionary<int, string>();
|
|
|
|
private static long _sweeps, _emitted;
|
|
private static bool _warnedDisabled;
|
|
|
|
public static void Initialize()
|
|
{
|
|
if (!BridgeConfig.Enabled)
|
|
return;
|
|
|
|
EventSink.ServerStarted += OnServerStarted;
|
|
}
|
|
|
|
private static void OnServerStarted()
|
|
{
|
|
BridgeLink.Connected_Core += OnConnected;
|
|
Rearm();
|
|
}
|
|
|
|
private static void OnConnected()
|
|
{
|
|
_last.Clear();
|
|
}
|
|
|
|
/// <summary>Stops and recreates the timer from current config. Called by `[bridge reload`.</summary>
|
|
public static void Rearm()
|
|
{
|
|
Stop();
|
|
|
|
_timer = Timer.DelayCall(
|
|
TimeSpan.FromSeconds(BridgeConfig.CitySweepSeconds),
|
|
TimeSpan.FromSeconds(BridgeConfig.CitySweepSeconds),
|
|
CitySweep);
|
|
}
|
|
|
|
public static void Stop()
|
|
{
|
|
if (_timer != null) { _timer.Stop(); _timer = null; }
|
|
}
|
|
|
|
public static string Status()
|
|
{
|
|
return String.Format("cities(enabled={0} sweeps={1} emitted={2} tracked={3})",
|
|
CityLoyaltySystem.Enabled, _sweeps, _emitted, _last.Count);
|
|
}
|
|
|
|
/// <summary>Runs one sweep now. Wired into `[bridge sweepnow`.</summary>
|
|
public static void SweepOnce()
|
|
{
|
|
CitySweep();
|
|
}
|
|
|
|
private static void CitySweep()
|
|
{
|
|
try
|
|
{
|
|
_sweeps++;
|
|
|
|
if (!CityLoyaltySystem.Enabled || CityLoyaltySystem.Cities == null)
|
|
{
|
|
if (!_warnedDisabled)
|
|
{
|
|
Console.WriteLine("[Bridge] city loyalty disabled; governor stream idle.");
|
|
_warnedDisabled = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!BridgeLink.Connected)
|
|
return; // nothing is listening; do not fill the queue with perishable snapshots
|
|
|
|
foreach (var city in CityLoyaltySystem.Cities)
|
|
{
|
|
if (city == null)
|
|
continue;
|
|
|
|
var sig = Signature(city);
|
|
|
|
int key = (int)city.City;
|
|
|
|
string prior;
|
|
if (_last.TryGetValue(key, out prior) && prior == sig)
|
|
continue; // unchanged since last emit
|
|
|
|
_last[key] = sig;
|
|
BridgeLink.Emit(WriteCity(city));
|
|
_emitted++;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("[Bridge] city sweep threw: {0}", ex.Message);
|
|
}
|
|
}
|
|
|
|
// The volatile fields: governor, governor-elect, and the election phase / candidate count.
|
|
private static string Signature(CityLoyaltySystem city)
|
|
{
|
|
var gov = city.Governor == null ? 0 : city.Governor.Serial.Value;
|
|
var elect = city.GovernorElect == null ? 0 : city.GovernorElect.Serial.Value;
|
|
|
|
var e = city.Election;
|
|
var phase = ElectionPhase(e);
|
|
var candidates = (e == null || e.Candidates == null) ? 0 : e.Candidates.Count;
|
|
|
|
return String.Concat(
|
|
gov.ToString(), "|", elect.ToString(), "|", phase, "|", candidates.ToString());
|
|
}
|
|
|
|
private static string WriteCity(CityLoyaltySystem city)
|
|
{
|
|
var e = city.Election;
|
|
var phase = ElectionPhase(e);
|
|
var candidates = (e == null || e.Candidates == null) ? 0 : e.Candidates.Count;
|
|
|
|
var sb = BridgeJson.Begin("city.update")
|
|
.Str("city", city.City.ToString())
|
|
.Str("electionPhase", phase)
|
|
.Num("candidates", candidates);
|
|
|
|
sb.Actor("governor", city.Governor);
|
|
sb.Actor("governorElect", city.GovernorElect);
|
|
|
|
if (e != null && e.Ongoing)
|
|
sb.Str("autoPickAt", e.AutoPickGovernor.ToUniversalTime().ToString("o"));
|
|
|
|
return sb.End();
|
|
}
|
|
|
|
/// <summary>Folds the election state into one of: none / nominate / vote / pending.</summary>
|
|
private static string ElectionPhase(CityElection e)
|
|
{
|
|
if (e == null)
|
|
return "none";
|
|
|
|
if (e.CanNominate())
|
|
return "nominate";
|
|
|
|
if (e.CanVote())
|
|
return "vote";
|
|
|
|
if (e.Ongoing)
|
|
return "pending";
|
|
|
|
return "none";
|
|
}
|
|
}
|
|
}
|