fix(examples): a Kits reload owed for the example kits survives an interrupted load (D329)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m52s

The first load writes the example kits into Kits' file and asked for a Kits
reload on a one-second timer. A timer dies with its plugin, so a second load of
RunicNPC inside that second (the 9d session's, on Carbon) lost the reload, and
examplesWritten was already set: the examples stayed refused ("no kit
rnpc_juggernaut") until Kits was reloaded by hand.

The kits written are now owed a reload in data/RunicNPC/state.json
(kitsReloadOwed). SettleKitsReload, on every load and when Kits comes back,
clears the debt once Kits knows every kit owed, and otherwise asks for the
reload again, at most three times, then warns with the command to run.

Oxide never showed it: loading RunicNPC recompiles and reloads Kits there
(// Requires: Kits). Carbon does not.

Walked on both rigs (Rust 25797215, Oxide 2.0.7815, Carbon 2.0.262): a first
install landing on a running server, the interrupted state, and a debt for a
kit that never exists (three reloads, the warning, no loop). The harness gains
s6.examples.reloadSettled (0.7.3; compiled on the Carbon rig).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-09 02:07:14 -05:00
parent 00be8d8d74
commit f9210e3a4f
2 changed files with 51 additions and 6 deletions

View File

@@ -4121,8 +4121,16 @@ namespace Oxide.Plugins
{
/// <summary>D284: set on the first load that wrote them, and never cleared.</summary>
[JsonProperty("examplesWritten")] public bool ExamplesWritten;
/// <summary>D329: the example kits written that Kits has not loaded yet. The reload is owed until it has.</summary>
[JsonProperty("kitsReloadOwed")] public List<string> KitsReloadOwed = new List<string>();
/// <summary>D329: the loads that have asked for that reload. At most three, so a kit Kits never takes cannot loop.</summary>
[JsonProperty("kitsReloadTries")] public int KitsReloadTries;
}
private const int KitsReloadTriesMax = 3;
/// <summary>Placements wait while Kits reloads with the example kits (D283 reloads RunicNPC with it).</summary>
private bool _holdForKits;
@@ -4206,8 +4214,8 @@ namespace Oxide.Plugins
/// <summary>
/// D284: on the first load only, the four example kits go into Kits' own data file (Kits has no
/// API that creates one, D280) and the four profiles into ours, each only where its name is free.
/// The flag is set before anything is written, so this runs once whatever happens. Kits is then
/// reloaded, which reloads RunicNPC too (D283); placements wait for it.
/// The flag is set before anything is written, so this runs once whatever happens. The kits it wrote
/// are then owed a Kits reload (D329), which SettleKitsReload asks for; it reloads RunicNPC too (D283).
/// </summary>
private void WriteExamplesOnce()
{
@@ -4286,11 +4294,43 @@ namespace Oxide.Plugins
if (addedKits.Count == 0)
return;
// Kits reads its file only when it loads (stage 6 spike, finding 5). The reload takes RunicNPC down and
// back with it (D283); if this framework does not, OnPluginLoaded("Kits") picks the profiles up.
// Kits reads its file only when it loads (stage 6 spike, finding 5), so a reload is owed; SettleKitsReload asks for it.
state.KitsReloadOwed = addedKits;
state.KitsReloadTries = 0;
Interface.Oxide.DataFileSystem.WriteObject(DataState, state);
}
/// <summary>
/// D329: asks Kits to reload while the example kits written are still owed, and clears the debt once Kits
/// knows them all. The reload runs on a timer, which dies with RunicNPC: in the 9d session a second load of
/// RunicNPC within that second lost it, and the examples stayed refused until Kits was reloaded by hand.
/// Owing it in the state file lets any later load (or Kits coming back) finish the job.
/// </summary>
private void SettleKitsReload()
{
StateFile state = ReadData<StateFile>(DataState);
if (state.KitsReloadOwed == null || state.KitsReloadOwed.Count == 0)
return;
string command = Framework() == "carbon" ? "c.reload" : "oxide.reload";
if (state.KitsReloadOwed.All(KitExists) || state.KitsReloadTries >= KitsReloadTriesMax)
{
if (state.KitsReloadTries >= KitsReloadTriesMax && !state.KitsReloadOwed.All(KitExists))
PrintWarning($"Kits has still not loaded the example kits ({string.Join(", ", state.KitsReloadOwed.ToArray())}) after " +
$"{KitsReloadTriesMax} reloads. Run `{command} Kits` once.");
state.KitsReloadOwed = new List<string>();
state.KitsReloadTries = 0;
Interface.Oxide.DataFileSystem.WriteObject(DataState, state);
return;
}
state.KitsReloadTries++;
Interface.Oxide.DataFileSystem.WriteObject(DataState, state);
// The reload takes RunicNPC down and back with it (D283); if this framework does not, OnPluginLoaded("Kits")
// picks the profiles up. Placements wait for it.
_holdForKits = true;
timer.Once(60f, () => _holdForKits = false);
string command = Framework() == "carbon" ? "c.reload" : "oxide.reload";
timer.Once(1f, () => ConsoleSystem.Run(ConsoleSystem.Option.Server, command, "Kits"));
}
@@ -4475,6 +4515,7 @@ namespace Oxide.Plugins
ResolveFields();
LoadData();
WriteExamplesOnce();
SettleKitsReload();
RevalidateProfiles();
RebuildPlacements();
TrackWorld();
@@ -4526,6 +4567,7 @@ namespace Oxide.Plugins
if (plugin == null || plugin.Name != "Kits" || !_ready)
return;
_holdForKits = false;
SettleKitsReload();
RevalidateProfiles();
TickPlacements();
}