feat(bridge): the world verbs an event owns (protocol 7, Phase 12a)
Five verbs an author sees -- creatures, an enhanced "boss", an oracle NPC, a temporary gate, decoration -- and ONE command family underneath them, because every one of them ends in the same sentence: an object exists, and this run owns it. `world.spawn` / `world.despawn` / `world.owned` carry a `what` discriminator, and the per-verb differences are fields rather than kinds. The ownership registry is PERSISTED, and that is forced rather than chosen. A spawned creature is in the world save, so it survives the restart that proves a town-crier line gone -- which already rules out reconcile-by-boot-stamp. But the record of which run owns which serial has nowhere else to live: in memory it is lost in the restart the creatures survive, and only in the website's ledger it is not held here at all, so `world.despawn` would delete whatever serial it was handed and "never touches a creature it did not create" would have no mechanism behind it. So the Bridge gains its second persisted file beside `Participation.bin` -- written by the same world save as the objects it describes, so the two cannot get out of step. The oracle is ours rather than `XmlSpawner2.XmlDialog`'s, and that engine is the reason for both halves of the decision. Its `SpeechEntry` is the evidence the shape is right -- `Text` plus comma-separated `Keywords`, a keyword-less entry as the greeting, a proximity range, a conversation lock. It is also why not to build on it: `SpeechEntry` carries an `Action` string, XmlSpawner's command-scripting language, which would leave an arbitrary-command field one step from an event author. `Mobile.OnMovement` (delivered to every mobile in range -- the `HandlesOnMovement` filter applies only to Items) and `Mobile.HandlesOnSpeech`/`OnSpeech` are native virtuals and are all it needs. Every `Bridge.EventsMax*` REFUSES rather than clamps, on `LeaseMaxDurationSec`'s argument from 11b: the shard's bound exists for the case where the website is wrong. `Bridge.EventsEnabled` gates all of it -- spawning is the same consent 11b introduced that switch for, not a third one. Decoration carries an `itemId`, because `Static` accounts for 5031 of the tree's decoration placements under 1992 different graphics: for that class the graphic IS the identity. Never applied to a `BaseAddon`, whose own ItemID is not what a player sees. Containers are refused outright -- teardown would delete whatever a player had left inside. `tools/scaffolding` gains `worldgone <serial>`, which deletes an object behind the registry's back. It is the one outcome the rig cannot reach by asking the bridge -- every bridge verb that removes an object also drops its row -- and it is what a player's sword does every time they kill an event creature. Verified on a real ServUO 57.4 world (206k items, 42k mobiles) against the release sidecar: all five verbs place; every ceiling refuses; a container and an unknown type refuse; one run cannot despawn another's object; the registry and its objects both survive a save and a clean restart (`pruned: 0`); a creature deleted behind the registry's back comes back `gone` rather than `removed`; and a five-second gate is collected by the shard's own deadline with `world.expired` on the wire. Refs: docs/link/v7.md, docs/website/EVENTS_PLAN.md Phase 12a Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -39,6 +39,8 @@ namespace Server.Custom
|
||||
/// activate <account> clear an account's inactivity, so its houses stop
|
||||
/// being Condemned and CAN be refreshed
|
||||
/// password <account> <pw> set a game account's password (for a login probe)
|
||||
/// worldgone <serial> delete an object BEHIND the ownership registry's
|
||||
/// back, playing the player who killed it
|
||||
/// save a world save
|
||||
/// shutdown a CLEAN shutdown, so the bridge emits server.shutdown
|
||||
///
|
||||
@@ -146,6 +148,12 @@ namespace Server.Custom
|
||||
case "partprobe":
|
||||
BridgeParticipationProbe.Run(null, Arg(parts, 1), Int(Arg(parts, 2)), Int(Arg(parts, 3)));
|
||||
break;
|
||||
// Phase 12a. `world.despawn` answering `gone` rather than `removed` is the
|
||||
// path a player takes every time they kill an event creature, and it is the one
|
||||
// outcome the rig cannot reach by asking the bridge: every bridge verb that
|
||||
// removes an object also drops its registry row, so the two never disagree.
|
||||
// This deletes the object and leaves the row, which is exactly what a sword does.
|
||||
case "worldgone": WorldGone(Arg(parts, 1)); 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
|
||||
@@ -156,6 +164,44 @@ namespace Server.Custom
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an object by serial, without telling anything.
|
||||
///
|
||||
/// Accepts the `0x…` form the bridge writes serials in, so a serial can be pasted
|
||||
/// straight out of a `world.owned` reply.
|
||||
/// </summary>
|
||||
private static void WorldGone(string raw)
|
||||
{
|
||||
if (String.IsNullOrEmpty(raw))
|
||||
{
|
||||
Say("worldgone <serial>");
|
||||
return;
|
||||
}
|
||||
|
||||
var text = raw.Trim();
|
||||
uint parsed;
|
||||
var ok = text.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? UInt32.TryParse(text.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsed)
|
||||
: UInt32.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
Say("worldgone: \"" + raw + "\" is not a serial");
|
||||
return;
|
||||
}
|
||||
|
||||
var entity = World.FindEntity((Serial)unchecked((int)parsed));
|
||||
|
||||
if (entity == null || entity.Deleted)
|
||||
{
|
||||
Say("worldgone: nothing at " + text);
|
||||
return;
|
||||
}
|
||||
|
||||
entity.Delete();
|
||||
Say("worldgone: deleted " + text + " and told nobody");
|
||||
}
|
||||
|
||||
private static string Arg(string[] parts, int i)
|
||||
{
|
||||
return i < parts.Length ? parts[i] : null;
|
||||
|
||||
Reference in New Issue
Block a user