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:
@@ -78,6 +78,26 @@ namespace Server.Custom.Bridge
|
||||
public static int AdminReasonMaxLength { get; private set; }
|
||||
public static int AdminBanMaxDurationSec { get; private set; }
|
||||
|
||||
// ---- the event plane (docs/link/v6.md §8, EVENTS_PLAN.md Phase 11b) ----
|
||||
//
|
||||
// **Its own gate, deliberately not AdminWriteEnabled** (org lead, 2026-09-04). Enabling the
|
||||
// admin plane is an operator consenting to staff moderation driven from the website - a
|
||||
// human pressing kick or ban on a screen. A lease and a participation ledger are the
|
||||
// website changing and watching the world on a SCHEDULE, unattended, at four in the
|
||||
// morning. Those are different consents, and one switch cannot express both.
|
||||
public static bool EventsEnabled { get; private set; }
|
||||
|
||||
public static int LeaseMaxDurationSec { get; private set; }
|
||||
public static int LeaseGraceSec { get; private set; }
|
||||
|
||||
public static int ParticipationSweepSeconds { get; private set; }
|
||||
public static double ParticipationKillWeight { get; private set; }
|
||||
public static int ParticipationMaxRuns { get; private set; }
|
||||
public static int ParticipationMaxMembers { get; private set; }
|
||||
public static int ParticipationMaxRadius { get; private set; }
|
||||
public static int ParticipationGraceSec { get; private set; }
|
||||
public static int ParticipationSnapshotChunk { get; private set; }
|
||||
|
||||
// ---- account provisioning (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md Part A) ----
|
||||
public static SignupMode Signup { get; private set; }
|
||||
public static bool AccountCreateEnabled { get; private set; }
|
||||
@@ -235,6 +255,61 @@ namespace Server.Custom.Bridge
|
||||
AdminReasonMaxLength = Config.Get("Bridge.AdminReasonMaxLength", 400);
|
||||
AdminBanMaxDurationSec = Config.Get("Bridge.AdminBanMaxDurationSec", 31536000);
|
||||
|
||||
// The event plane. Off until an operator says otherwise - see the field block above for
|
||||
// why this is not AdminWriteEnabled.
|
||||
EventsEnabled = Config.Get("Bridge.EventsEnabled", false);
|
||||
|
||||
// Thirty days, matching core's own MAX_LEASE_MS. This is the shard's INDEPENDENT
|
||||
// ceiling rather than a mirror of it: the website bounds what it will ask for, and a
|
||||
// shard that trusted the asking would have no bound of its own at the one moment it
|
||||
// matters, which is when the website is wrong.
|
||||
LeaseMaxDurationSec = Config.Get("Bridge.LeaseMaxDurationSec", 2592000);
|
||||
if (LeaseMaxDurationSec < 1)
|
||||
LeaseMaxDurationSec = 1;
|
||||
|
||||
// How long a finished lease stays listed after its deadline restored it, so teardown
|
||||
// still gets a definite verdict rather than finding nothing and having to guess.
|
||||
LeaseGraceSec = Config.Get("Bridge.LeaseGraceSec", 86400);
|
||||
if (LeaseGraceSec < 0)
|
||||
LeaseGraceSec = 0;
|
||||
|
||||
ParticipationSweepSeconds = Config.Get("Bridge.ParticipationSweepSeconds", 30);
|
||||
if (ParticipationSweepSeconds < 1)
|
||||
ParticipationSweepSeconds = 1;
|
||||
|
||||
// What one kill inside the area is worth against one minute of standing in it. Both
|
||||
// halves live on the shard because the score IS the shard's number: core stores an
|
||||
// opaque decimal it never interprets, so a weight core could edit would be a weight
|
||||
// nobody could explain from either side.
|
||||
ParticipationKillWeight = Config.Get("Bridge.ParticipationKillWeight", 5.0);
|
||||
if (ParticipationKillWeight < 0.0)
|
||||
ParticipationKillWeight = 0.0;
|
||||
|
||||
ParticipationMaxRuns = Config.Get("Bridge.ParticipationMaxRuns", 8);
|
||||
if (ParticipationMaxRuns < 1)
|
||||
ParticipationMaxRuns = 1;
|
||||
|
||||
ParticipationMaxMembers = Config.Get("Bridge.ParticipationMaxMembers", 2000);
|
||||
if (ParticipationMaxMembers < 1)
|
||||
ParticipationMaxMembers = 1;
|
||||
|
||||
// A radius, not a rectangle, and bounded: an area big enough to cover a facet makes
|
||||
// "took part" meaningless and the sweep expensive in the same stroke.
|
||||
ParticipationMaxRadius = Config.Get("Bridge.ParticipationMaxRadius", 300);
|
||||
if (ParticipationMaxRadius < 1)
|
||||
ParticipationMaxRadius = 1;
|
||||
|
||||
ParticipationGraceSec = Config.Get("Bridge.ParticipationGraceSec", 86400);
|
||||
if (ParticipationGraceSec < 0)
|
||||
ParticipationGraceSec = 0;
|
||||
|
||||
// How many members one snapshot resolves before yielding the Core thread. See
|
||||
// BridgeParticipation: this is what makes the handler DEFER, which is what makes
|
||||
// `bridge.busy` reachable at all.
|
||||
ParticipationSnapshotChunk = Config.Get("Bridge.ParticipationSnapshotChunk", 100);
|
||||
if (ParticipationSnapshotChunk < 1)
|
||||
ParticipationSnapshotChunk = 1;
|
||||
|
||||
// Account provisioning. An absent SignupMode defaults to Hybrid; a *present but
|
||||
// unrecognized* value falls back to Game (the safest — no website creation), so a
|
||||
// typo can never accidentally open provisioning.
|
||||
@@ -313,9 +388,10 @@ namespace Server.Custom.Bridge
|
||||
public static string Describe()
|
||||
{
|
||||
return String.Format(
|
||||
"enabled={0} endpoint={1}:{2} queueCap={3} sweeps(stat={4}s decay={5}s econ={6}s champ={7}s) adminWrite={8}(floor={9}) signup={10}(create={11})",
|
||||
"enabled={0} endpoint={1}:{2} queueCap={3} sweeps(stat={4}s decay={5}s econ={6}s champ={7}s) adminWrite={8}(floor={9}) signup={10}(create={11}) events={12}",
|
||||
Enabled, Host, Port, QueueCap, StatSweepSeconds, DecaySweepSeconds, EconomySweepSeconds,
|
||||
ChampSweepSeconds, AdminWriteEnabled, AdminAccessFloor, Signup, AccountCreateEnabled);
|
||||
ChampSweepSeconds, AdminWriteEnabled, AdminAccessFloor, Signup, AccountCreateEnabled,
|
||||
EventsEnabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user