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:
2026-07-10 10:59:21 -05:00
parent 261ae6bc5c
commit 7ea7570e5a
4 changed files with 360 additions and 2 deletions

View File

@@ -148,7 +148,7 @@ namespace Server.Custom.Bridge
BridgeLink.Emit(sb.End());
}
[Usage("bridge [status | reload | ping]")]
[Usage("bridge [status | reload | ping | sweepnow]")]
[Description("Inspects and controls the sidecar link.")]
private static void Bridge_OnCommand(CommandEventArgs e)
{
@@ -158,8 +158,9 @@ namespace Server.Custom.Bridge
{
case "reload":
BridgeConfig.Load();
BridgeSweeps.Rearm();
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
e.Mobile.SendMessage("Bridge: endpoint changes take effect on reconnect.");
e.Mobile.SendMessage("Bridge: sweeps re-armed; endpoint changes take effect on reconnect.");
break;
case "ping":
@@ -167,12 +168,19 @@ namespace Server.Custom.Bridge
e.Mobile.SendMessage("Bridge: ping queued.");
break;
case "sweepnow":
BridgeSweeps.SweepOnce();
e.Mobile.SendMessage("Bridge: ran one sweep of each stream.");
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
break;
default:
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
e.Mobile.SendMessage(
"Bridge: connected={0} depth={1} sent={2} dropped={3} received={4} connects={5} writeErrors={6}",
BridgeLink.Connected, BridgeLink.Depth, BridgeLink.Sent, BridgeLink.Dropped,
BridgeLink.Received, BridgeLink.Connects, BridgeLink.WriteErrors);
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
break;
}
}