Phase 3: polled sweeps (vitals, house decay, economy)
BridgeSweeps runs three repeating Core-thread timers for the state that has no
EventSink. Cost measured in Phase 1 is why they can run on the main thread: a
full pass of all three is well under a millisecond at the seeded scale.
- Vitals: online players only. Small and volatile; the sidecar diffs snapshots.
Offline characters do not move, so they are served on demand as full profiles
instead, not swept.
- House decay: emits only on a level transition. A silent baseline on
ServerStarted records every house's current stage, so a restart does not
re-announce them. Payload carries from/to, coords, nested ban location,
region, sign name, owner serial+account, and built/refreshed timestamps, all
null-guarded.
- Economy supply: periodic sum of every account's currency as a snapshot. The
level; AccountGoldChange and the vendor events are the flow.
All three re-arm on `[bridge reload`; `[bridge sweepnow` runs one of each on
demand; `[bridge status` reports sweep counters. Sweeps skip emitting while the
sidecar is disconnected, since their state is perishable and re-emitted next
tick anyway (unlike events, which queue through an outage).
Verified on the seeded world with 8s intervals: baseline recorded 29 houses
silently, a probe bumped one Somewhat->Fairly, and the next sweep emitted exactly
one house.decay and none for the other 28. Economy emitted a supply snapshot per
interval. Vitals emitted nothing, correctly, since all seeded characters are
offline. Evidence in docs/PLAN.md §13.
Adds tools/scaffolding/BridgeSweepProbe.cs (never deployed) to force a decay
transition on demand.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
279
overlay/Scripts/Custom/Bridge/BridgeSweeps.cs
Normal file
279
overlay/Scripts/Custom/Bridge/BridgeSweeps.cs
Normal file
@@ -0,0 +1,279 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
using Server.Accounting;
|
||||
using Server.Mobiles;
|
||||
using Server.Multis;
|
||||
|
||||
namespace Server.Custom.Bridge
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<Serial, DecayLevel> _decayState =
|
||||
new Dictionary<Serial, DecayLevel>();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Stops and recreates the timers from current config. Called by `[bridge reload`.</summary>
|
||||
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 ----
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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 ----
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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 ----
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Runs each sweep once, now. For `[bridge sweepnow`.</summary>
|
||||
public static void SweepOnce()
|
||||
{
|
||||
VitalsSweep();
|
||||
DecaySweep();
|
||||
EconomySweep();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user