fix(bridge): refuse world commands until the world has loaded
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m29s
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m29s
The link starts in Init, before the save loads. The phase 13a walk restarted the rig mid-run: the website saw the new boot id at once, asked world.owned into an empty world, and the plugin — finding no entity behind any net id — pruned three live crates from its own registry. They stayed in the world, owned by nothing, and a revert would have refused them. world.owned, world.revert, world.monuments and every world write now answer `not-ready` until OnServerInitialized, zones are not re-created before it, and server.hello carries `worldReady` so the website can hold its reconcile until the world it would be asking about exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -981,6 +981,10 @@ namespace Oxide.Plugins
|
||||
hello["bootId"] = _bootId;
|
||||
hello["plugin"] = Version.ToString();
|
||||
|
||||
// The website compares boot ids to decide when to reconcile the events ledger, and
|
||||
// must not do it until the world it would be asking about exists (§28.6).
|
||||
hello["worldReady"] = _worldReady;
|
||||
|
||||
EmitFrame(hello);
|
||||
}
|
||||
|
||||
@@ -1300,6 +1304,7 @@ namespace Oxide.Plugins
|
||||
// Only now is the wipe known, so only now can what an earlier map owned be dropped;
|
||||
// and this hook also fires on a hotload, which is exactly when ZoneManager has just
|
||||
// erased every zone the previous load of this plugin owned (D92).
|
||||
_worldReady = true;
|
||||
DropWorldFromOtherWipes();
|
||||
RecreateZones("load");
|
||||
|
||||
@@ -1309,6 +1314,11 @@ namespace Oxide.Plugins
|
||||
if (_cadence == null)
|
||||
_cadence = timer.Every(BoardCadenceSeconds, Cadence);
|
||||
|
||||
// Say the world is ready now rather than at the next tick: the website holds its
|
||||
// reconcile until it hears this.
|
||||
if (_connected)
|
||||
SendHello();
|
||||
|
||||
Dictionary<string, object> frame = Frame("server.initialized", "event");
|
||||
frame["initial"] = initial;
|
||||
EmitFrame(frame);
|
||||
@@ -5977,6 +5987,22 @@ namespace Oxide.Plugins
|
||||
/// <summary>A counter for zone ids, so two zones one run opens in one tick never collide.</summary>
|
||||
private int _zoneSerial;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the world is loaded for THIS load of the plugin — set at
|
||||
/// <c>OnServerInitialized</c>, which fires at the end of a real start and at once on a
|
||||
/// hotload. Until then every world command is refused <c>not-ready</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// Found by the phase 13a walk, and it is the worst kind of wrong: the link starts in
|
||||
/// <c>Init</c>, BEFORE the save loads, so a website that saw the new boot id asked
|
||||
/// <c>world.owned</c> into an empty world — and the plugin, finding no entity behind
|
||||
/// any net id, pruned three live crates from its own registry. They stayed in the
|
||||
/// world owned by nothing, and a revert would have refused them. An empty world at
|
||||
/// boot is not an answer; it is the question arriving too early.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private bool _worldReady;
|
||||
|
||||
private string _lastWorldSummary = "none yet";
|
||||
|
||||
private Timer _worldTimer;
|
||||
@@ -6179,8 +6205,22 @@ namespace Oxide.Plugins
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Main thread. Refuses a world command until the world is loaded (see <c>_worldReady</c>).</summary>
|
||||
private bool WorldReady(string reqId)
|
||||
{
|
||||
if (_worldReady)
|
||||
return true;
|
||||
|
||||
EmitWorldError(reqId, "not-ready",
|
||||
"the server is still loading its world; ask again once it has started");
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool WorldWritesAllowed(string reqId)
|
||||
{
|
||||
if (!WorldReady(reqId))
|
||||
return false;
|
||||
|
||||
if (_config != null && _config.EventsEnabled)
|
||||
return true;
|
||||
|
||||
@@ -6219,6 +6259,9 @@ namespace Oxide.Plugins
|
||||
|
||||
private void HandleWorldMonuments(string reqId)
|
||||
{
|
||||
if (!WorldReady(reqId))
|
||||
return;
|
||||
|
||||
var rows = new List<object>();
|
||||
|
||||
foreach (KeyValuePair<string, List<MonumentInfo>> kv in MonumentGroups())
|
||||
@@ -6414,6 +6457,11 @@ namespace Oxide.Plugins
|
||||
/// </summary>
|
||||
private void RecreateZones(string why)
|
||||
{
|
||||
// At a real start ZoneManager loads before the world does and initialises its own
|
||||
// zones at OnTerrainInitialized; ours wait for OnServerInitialized, which calls here.
|
||||
if (!_worldReady)
|
||||
return;
|
||||
|
||||
Plugin zm = ZoneManagerPlugin();
|
||||
|
||||
if (zm == null)
|
||||
@@ -6593,6 +6641,11 @@ namespace Oxide.Plugins
|
||||
/// </summary>
|
||||
private void HandleWorldRevert(Dictionary<string, object> cmd, string reqId)
|
||||
{
|
||||
// Before the world loads, every entity looks gone, and a revert would forget things
|
||||
// that are about to exist again.
|
||||
if (!WorldReady(reqId))
|
||||
return;
|
||||
|
||||
string runId = StringField(cmd, "runId");
|
||||
string key = StringField(cmd, "key");
|
||||
var ids = new List<string>(Strings(cmd, "ids"));
|
||||
@@ -6693,6 +6746,9 @@ namespace Oxide.Plugins
|
||||
/// </summary>
|
||||
private void HandleWorldOwned(Dictionary<string, object> cmd, string reqId)
|
||||
{
|
||||
if (!WorldReady(reqId))
|
||||
return;
|
||||
|
||||
string runId = StringField(cmd, "runId");
|
||||
RecreateZones("owned");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user