using System; using System.Collections.Generic; using Server.Engines.CityLoyalty; namespace Server.Custom.Bridge { /// /// The town-governor stream (docs/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 and — 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. /// public static class BridgeGovernance { private static Timer _timer; // City enum value -> last-emitted signature. private static readonly Dictionary _last = new Dictionary(); 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(); } /// Stops and recreates the timer from current config. Called by `[bridge reload`. 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); } /// Runs one sweep now. Wired into `[bridge sweepnow`. 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(); } /// Folds the election state into one of: none / nominate / vote / pending. 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"; } } }