From f9210e3a4fce604268b16967c645eb65b444dabc Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 9 Oct 2026 02:07:14 -0500 Subject: [PATCH] fix(examples): a Kits reload owed for the example kits survives an interrupted load (D329) 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 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- plugin/RunicNPC.cs | 52 ++++++++++++++++++++++++++++++++++++++----- tools/RunicNpcTest.cs | 5 ++++- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/plugin/RunicNPC.cs b/plugin/RunicNPC.cs index 5b0de53..67b29a8 100644 --- a/plugin/RunicNPC.cs +++ b/plugin/RunicNPC.cs @@ -4121,8 +4121,16 @@ namespace Oxide.Plugins { /// D284: set on the first load that wrote them, and never cleared. [JsonProperty("examplesWritten")] public bool ExamplesWritten; + + /// D329: the example kits written that Kits has not loaded yet. The reload is owed until it has. + [JsonProperty("kitsReloadOwed")] public List KitsReloadOwed = new List(); + + /// D329: the loads that have asked for that reload. At most three, so a kit Kits never takes cannot loop. + [JsonProperty("kitsReloadTries")] public int KitsReloadTries; } + private const int KitsReloadTriesMax = 3; + /// Placements wait while Kits reloads with the example kits (D283 reloads RunicNPC with it). private bool _holdForKits; @@ -4206,8 +4214,8 @@ namespace Oxide.Plugins /// /// 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). /// 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); + } + + /// + /// 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. + /// + private void SettleKitsReload() + { + StateFile state = ReadData(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(); + 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(); } diff --git a/tools/RunicNpcTest.cs b/tools/RunicNpcTest.cs index ed7b7ba..a16a4a2 100644 --- a/tools/RunicNpcTest.cs +++ b/tools/RunicNpcTest.cs @@ -45,7 +45,7 @@ namespace Oxide.Plugins /// a made-up user id (11400000001 and up), so IsNpc is false. They take no damage. /// /// - [Info("RunicNpcTest", "Runic Gateway", "0.7.2")] + [Info("RunicNpcTest", "Runic Gateway", "0.7.3")] [Description("RunicNPC stage 2 to 7 tests, the stage 6 spike and stage 9's cost check. A developer tool: never ship it.")] internal class RunicNpcTest : RustPlugin { @@ -2422,6 +2422,9 @@ namespace Oxide.Plugins "each needs kits.runicnpc, which nobody is granted"); JObject state = Interface.Oxide.DataFileSystem.ReadObject("RunicNPC/state"); Check("s6.examples.once", state?["examplesWritten"]?.Type == JTokenType.Boolean && (bool)state["examplesWritten"], "state.json: " + state?.ToString(Formatting.None)); + // D329: once Kits knows the examples, no reload is owed (a state from before D329 has no list at all). + JArray owed = state?["kitsReloadOwed"] as JArray; + Check("s6.examples.reloadSettled", owed == null || owed.Count == 0, "kitsReloadOwed: " + (owed?.ToString(Formatting.None) ?? "absent")); JObject items = kitFile?["_kits"]?["rnpc_juggernaut"] as JObject; int itemCount = items == null ? 0 : ((JArray)items["MainItems"]).Count + ((JArray)items["WearItems"]).Count + ((JArray)items["BeltItems"]).Count; Check("s6.examples.items", itemCount == 6, $"rnpc_juggernaut carries {itemCount} of its 6 items (a short name this Rust lacks would be left out)"); -- 2.49.1