feat(bridge): lease deadlines and the participation ledger (Phase 11b)

Protocol 6 amended in place. Two mechanisms behind one new default-off gate,
`Bridge.EventsEnabled` -- deliberately not `AdminWriteEnabled`, because enabling
the admin plane is consenting to staff moderation from a screen a human is
looking at, and this is consenting to the world being changed and watched on a
schedule, unattended.

BridgeLeases: a live config value held for a bounded time, with the deadline
honoured on the shard whether or not the website is heard from again, and a
compare-and-set restore that reports `drifted` rather than overwriting a GM's
deliberate change. Memory-only -- nothing calls Config.Save() -- so a restart is
a free restore.

BridgeParticipation: presence in a declared area plus kill credit inside it,
keyed by character serial, persisted in the world save. The Bridge's first
persisted state, because a run spans hours and an in-memory tally would regress
every attendee's score after one restart. Its snapshot is also the first handler
that DEFERS, which makes `bridge.busy` reachable for the first time.

And it immediately found a defect in 11a: BridgeIdempotency.Busy built its frame
with Begin("bridge.busy") and then appended a diagnostic `.Str("kind", ...)`, so
the object carried two `kind` fields and every JSON parser takes the last. The
sidecar answered 200 instead of 425. Renamed `busyKind`.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-04 19:31:20 -05:00
parent d2a12c46e2
commit 63a7dc4374
10 changed files with 2181 additions and 7 deletions

View File

@@ -202,6 +202,8 @@ namespace Server.Custom.Bridge
BridgeHousing.Rearm();
BridgePoints.Rearm();
BridgeMarket.Rearm();
BridgeParticipation.Rearm();
BridgeLeases.Rearm();
// Not a sweep, so it has nothing to re-arm — but an operator who just edited a
// .cfg wants the change on the site now, not after a shard restart.
BridgeRuleset.Emit();
@@ -223,6 +225,7 @@ namespace Server.Custom.Bridge
BridgeHousing.SweepOnce();
BridgePoints.SweepOnce();
BridgeMarket.SweepOnce();
BridgeParticipation.SweepOnce();
e.Mobile.SendMessage("Bridge: ran one sweep of each stream.");
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeChamps.Status());
@@ -232,6 +235,7 @@ namespace Server.Custom.Bridge
e.Mobile.SendMessage("Bridge: {0}", BridgeHousing.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgePoints.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeMarket.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeParticipation.Status());
break;
default:
@@ -251,6 +255,8 @@ namespace Server.Custom.Bridge
e.Mobile.SendMessage("Bridge: {0}", BridgePages.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeRuleset.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeIdempotency.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeLeases.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeParticipation.Status());
break;
}
}