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["bootId"] = _bootId;
|
||||||
hello["plugin"] = Version.ToString();
|
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);
|
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;
|
// 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
|
// 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).
|
// erased every zone the previous load of this plugin owned (D92).
|
||||||
|
_worldReady = true;
|
||||||
DropWorldFromOtherWipes();
|
DropWorldFromOtherWipes();
|
||||||
RecreateZones("load");
|
RecreateZones("load");
|
||||||
|
|
||||||
@@ -1309,6 +1314,11 @@ namespace Oxide.Plugins
|
|||||||
if (_cadence == null)
|
if (_cadence == null)
|
||||||
_cadence = timer.Every(BoardCadenceSeconds, Cadence);
|
_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");
|
Dictionary<string, object> frame = Frame("server.initialized", "event");
|
||||||
frame["initial"] = initial;
|
frame["initial"] = initial;
|
||||||
EmitFrame(frame);
|
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>
|
/// <summary>A counter for zone ids, so two zones one run opens in one tick never collide.</summary>
|
||||||
private int _zoneSerial;
|
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 string _lastWorldSummary = "none yet";
|
||||||
|
|
||||||
private Timer _worldTimer;
|
private Timer _worldTimer;
|
||||||
@@ -6179,8 +6205,22 @@ namespace Oxide.Plugins
|
|||||||
return true;
|
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)
|
private bool WorldWritesAllowed(string reqId)
|
||||||
{
|
{
|
||||||
|
if (!WorldReady(reqId))
|
||||||
|
return false;
|
||||||
|
|
||||||
if (_config != null && _config.EventsEnabled)
|
if (_config != null && _config.EventsEnabled)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -6219,6 +6259,9 @@ namespace Oxide.Plugins
|
|||||||
|
|
||||||
private void HandleWorldMonuments(string reqId)
|
private void HandleWorldMonuments(string reqId)
|
||||||
{
|
{
|
||||||
|
if (!WorldReady(reqId))
|
||||||
|
return;
|
||||||
|
|
||||||
var rows = new List<object>();
|
var rows = new List<object>();
|
||||||
|
|
||||||
foreach (KeyValuePair<string, List<MonumentInfo>> kv in MonumentGroups())
|
foreach (KeyValuePair<string, List<MonumentInfo>> kv in MonumentGroups())
|
||||||
@@ -6414,6 +6457,11 @@ namespace Oxide.Plugins
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void RecreateZones(string why)
|
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();
|
Plugin zm = ZoneManagerPlugin();
|
||||||
|
|
||||||
if (zm == null)
|
if (zm == null)
|
||||||
@@ -6593,6 +6641,11 @@ namespace Oxide.Plugins
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void HandleWorldRevert(Dictionary<string, object> cmd, string reqId)
|
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 runId = StringField(cmd, "runId");
|
||||||
string key = StringField(cmd, "key");
|
string key = StringField(cmd, "key");
|
||||||
var ids = new List<string>(Strings(cmd, "ids"));
|
var ids = new List<string>(Strings(cmd, "ids"));
|
||||||
@@ -6693,6 +6746,9 @@ namespace Oxide.Plugins
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void HandleWorldOwned(Dictionary<string, object> cmd, string reqId)
|
private void HandleWorldOwned(Dictionary<string, object> cmd, string reqId)
|
||||||
{
|
{
|
||||||
|
if (!WorldReady(reqId))
|
||||||
|
return;
|
||||||
|
|
||||||
string runId = StringField(cmd, "runId");
|
string runId = StringField(cmd, "runId");
|
||||||
RecreateZones("owned");
|
RecreateZones("owned");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user