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:
@@ -134,6 +134,18 @@ namespace Server.Custom
|
||||
case "election": Election(Arg(parts, 1)); break;
|
||||
case "activate": Activate(Arg(parts, 1)); break;
|
||||
case "password": Password(Arg(parts, 1), Arg(parts, 2)); break;
|
||||
// Phase 11b. Plays the interfering GM a config lease's compare-and-set exists to
|
||||
// catch, and reads a key back the way the game reads it. Both halves are here
|
||||
// rather than only in `[leaseprobe` because a headless rig has no client to type
|
||||
// a command at, and ServUO's own console takes a fixed verb set.
|
||||
case "configset": ConfigSet(Arg(parts, 1), Arg(parts, 2)); break;
|
||||
case "configread": ConfigRead(Arg(parts, 1)); break;
|
||||
// Kill credit inside a participation area. Lives in BridgeParticipationProbe
|
||||
// because it moves mobiles and spawns a creature; reachable from here because a
|
||||
// headless rig has no client to type `[partprobe` at. The two files ship together.
|
||||
case "partprobe":
|
||||
BridgeParticipationProbe.Run(null, Arg(parts, 1), Int(Arg(parts, 2)), Int(Arg(parts, 3)));
|
||||
break;
|
||||
case "save": Say("saving"); Misc.AutoSave.Save(); break;
|
||||
// A clean shutdown, which is the only kind that EMITS. `Stop-Process` drops the
|
||||
// socket and the shard says nothing, so a killed shard is indistinguishable from
|
||||
@@ -149,6 +161,64 @@ namespace Server.Custom
|
||||
return i < parts.Length ? parts[i] : null;
|
||||
}
|
||||
|
||||
private static int Int(string raw)
|
||||
{
|
||||
int n;
|
||||
return Int32.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out n) ? n : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a live config key, so a lease's `drifted` verdict can be produced at all.
|
||||
///
|
||||
/// **`Config.Set` has exactly ONE caller in the whole of ServUO 57.4**
|
||||
/// (`Server/ScriptCompiler.cs`, for `Compiler.Dynamic`). No in-game command, gump or
|
||||
/// console verb writes a config key, so on a stock shard a GM cannot drift a
|
||||
/// configuration lease even deliberately -- and the one safety property a lease has
|
||||
/// that nothing else does would go untested. Written through the same typed setter a
|
||||
/// float lease uses, so what it produces is indistinguishable to the compare-and-set
|
||||
/// from a real interfering write.
|
||||
///
|
||||
/// Deliberately no `Config.Save()`, matching BridgeLeases: nothing about a rig should
|
||||
/// leave a modified .cfg behind for the next boot to inherit.
|
||||
/// </summary>
|
||||
private static void ConfigSet(string key, string raw)
|
||||
{
|
||||
if (key == null || raw == null)
|
||||
{
|
||||
Say("configset <key> <value>");
|
||||
return;
|
||||
}
|
||||
|
||||
double n;
|
||||
|
||||
if (Double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out n))
|
||||
Config.Set(key, n);
|
||||
else
|
||||
Config.Set(key, raw);
|
||||
|
||||
Say("configset " + key + " = " + raw + " (in memory only)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a key back through `Config.Get`, at a moment long after every type
|
||||
/// initialiser has run.
|
||||
///
|
||||
/// This is the check that tells a key which TOOK from one that only appeared to: a
|
||||
/// lease on one of ServUO's ~150 cached call sites applies cleanly and does nothing,
|
||||
/// which is the worst failure this feature has.
|
||||
/// </summary>
|
||||
private static void ConfigRead(string key)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
Say("configread <key>");
|
||||
return;
|
||||
}
|
||||
|
||||
Say("configread " + key + " = " + Config.Get(key, Double.NaN).ToString("R", CultureInfo.InvariantCulture)
|
||||
+ " (double), \"" + Config.Get(key, "<unset>") + "\" (string)");
|
||||
}
|
||||
|
||||
// ---- houses ----
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user