using System; using System.Collections.Generic; using System.Text; using Server.Accounting; using Server.Mobiles; using Server.Multis; namespace Server.Custom.Bridge { /// /// The three polled streams, for state that has no EventSink: player vitals, house decay, /// and money supply. All three run on the Core thread via repeating Timers, and the /// measured cost (docs/PLAN.md §1) is why they can: at the seeded scale a full pass of all /// three is well under a millisecond. /// /// Timers do not fire during a world save (Timer.cs:322), so a sweep that would have landed /// mid-save simply happens a few seconds later. That is fine for all three. /// public static class BridgeSweeps { private static Timer _vitals, _decay, _economy; // Last-known decay level per house. In memory, rebuilt from a silent baseline on // ServerStarted, so a restart does not re-announce every house's current stage. private static readonly Dictionary _decayState = new Dictionary(); private static bool _baselined; private static long _vitalsSweeps, _vitalsEmitted, _decaySweeps, _decayTransitions, _economySweeps; public static void Initialize() { if (!BridgeConfig.Enabled) return; EventSink.ServerStarted += OnServerStarted; } private static void OnServerStarted() { BaselineDecay(); Rearm(); } /// Stops and recreates the timers from current config. Called by `[bridge reload`. public static void Rearm() { Stop(); _vitals = Timer.DelayCall( TimeSpan.FromSeconds(BridgeConfig.StatSweepSeconds), TimeSpan.FromSeconds(BridgeConfig.StatSweepSeconds), VitalsSweep); _decay = Timer.DelayCall( TimeSpan.FromSeconds(BridgeConfig.DecaySweepSeconds), TimeSpan.FromSeconds(BridgeConfig.DecaySweepSeconds), DecaySweep); _economy = Timer.DelayCall( TimeSpan.FromSeconds(BridgeConfig.EconomySweepSeconds), TimeSpan.FromSeconds(BridgeConfig.EconomySweepSeconds), EconomySweep); } public static void Stop() { if (_vitals != null) { _vitals.Stop(); _vitals = null; } if (_decay != null) { _decay.Stop(); _decay = null; } if (_economy != null) { _economy.Stop(); _economy = null; } } public static string Status() { return String.Format( "vitals(sweeps={0} emitted={1}) decay(sweeps={2} transitions={3} tracked={4}) economy(sweeps={5})", _vitalsSweeps, _vitalsEmitted, _decaySweeps, _decayTransitions, _decayState.Count, _economySweeps); } // ---- vitals ---- /// /// Online players only. Vitals are small and volatile; the sidecar diffs successive /// snapshots and forwards only changes. Offline characters do not move, so there is /// nothing to sweep — their state is served on demand as a full profile instead. /// private static void VitalsSweep() { try { _vitalsSweeps++; if (!BridgeLink.Connected) return; // nothing is listening; do not fill the queue with perishable snapshots foreach (var m in World.Mobiles.Values) { var pm = m as PlayerMobile; if (pm == null || pm.NetState == null || pm.Deleted) continue; BridgeLink.Emit(WriteVitals(pm)); _vitalsEmitted++; } } catch (Exception ex) { Console.WriteLine("[Bridge] vitals sweep threw: {0}", ex.Message); } } private static string WriteVitals(PlayerMobile m) { return BridgeJson.Begin("char.vitals") .Ser("serial", m.Serial) .Num("hits", m.Hits).Num("hitsMax", m.HitsMax) .Num("mana", m.Mana).Num("manaMax", m.ManaMax) .Num("stam", m.Stam).Num("stamMax", m.StamMax) .Num("str", m.Str).Num("dex", m.Dex).Num("int", m.Int) .Str("map", m.Map == null ? null : m.Map.Name) .Num("x", m.X).Num("y", m.Y) .End(); } // ---- house decay ---- /// /// Populates the last-known level for every house without emitting. Without this, the /// first sweep after a restart would report every house as a fresh transition. /// private static void BaselineDecay() { try { _decayState.Clear(); foreach (var house in BaseHouse.AllHouses) { if (house == null || house.Deleted) continue; _decayState[house.Serial] = house.DecayLevel; } _baselined = true; Console.WriteLine("[Bridge] decay baseline: {0} houses", _decayState.Count); } catch (Exception ex) { Console.WriteLine("[Bridge] decay baseline threw: {0}", ex.Message); } } private static void DecaySweep() { try { _decaySweeps++; if (!_baselined) BaselineDecay(); foreach (var house in BaseHouse.AllHouses) { if (house == null || house.Deleted) continue; var level = house.DecayLevel; // computed getter — read once var serial = house.Serial; DecayLevel prior; bool known = _decayState.TryGetValue(serial, out prior); if (known && prior == level) continue; _decayState[serial] = level; if (!known) continue; // a house that appeared since baseline; record, do not announce _decayTransitions++; if (BridgeLink.Connected) BridgeLink.Emit(WriteDecay(house, prior, level)); } } catch (Exception ex) { Console.WriteLine("[Bridge] decay sweep threw: {0}", ex.Message); } } private static string WriteDecay(BaseHouse house, DecayLevel from, DecayLevel to) { var sb = BridgeJson.Begin("house.decay") .Ser("serial", house.Serial) .Str("from", from.ToString()) .Str("to", to.ToString()) .Str("map", house.Map == null ? null : house.Map.Name) .Num("x", house.X).Num("y", house.Y).Num("z", house.Z); var region = house.Region; if (region != null) sb.Str("region", region.Name); var sign = house.Sign; if (sign != null) sb.Str("name", sign.GetName()); var owner = house.Owner; if (owner != null) { sb.Ser("ownerSerial", owner.Serial); var acct = owner.Account as Account; if (acct != null) sb.Str("ownerAcct", acct.Username); } // Where a player would physically stand to see it. var ban = house.BanLocation; sb.Append(",\"ban\":{\"x\":").Append(ban.X) .Append(",\"y\":").Append(ban.Y) .Append(",\"z\":").Append(ban.Z).Append('}'); sb.Str("builtOn", house.BuiltOn.ToUniversalTime().ToString("o")); sb.Str("lastRefreshed", house.LastRefreshed.ToUniversalTime().ToString("o")); return sb.End(); } // ---- economy supply ---- /// /// Money supply = the sum of every account's currency, as a periodic snapshot. This is /// the level; AccountGoldChange and the vendor events are the flow. The sidecar keeps /// both. /// private static void EconomySweep() { try { _economySweeps++; if (!BridgeLink.Connected) return; double totalCurrency = 0; int accounts = 0; foreach (Account a in Accounting.Accounts.GetAccounts()) { totalCurrency += a.TotalCurrency; accounts++; } BridgeLink.Emit(BridgeJson.Begin("economy.supply") .Num("accounts", accounts) .Num("gold", (long)(totalCurrency * Account.CurrencyThreshold)) .End()); } catch (Exception ex) { Console.WriteLine("[Bridge] economy sweep threw: {0}", ex.Message); } } /// Runs each sweep once, now. For `[bridge sweepnow`. public static void SweepOnce() { VitalsSweep(); DecaySweep(); EconomySweep(); } } }