// Requires: Kits using System; using System.Collections.Generic; namespace Oxide.Plugins { /// /// RunicNPC — Runic Gateway's own NPC plugin for Rust, on Oxide and Carbon. /// /// /// This is the stage 0 plugin (docs/runicnpc/PLAN.md §9): it loads, answers its API version, /// and reports on itself. It spawns nothing. Everything it will do is planned in that /// document, stage by stage. /// /// /// /// Kits is required (D217): it is how every RunicNPC NPC is equipped, so the plugin /// declares it on the first line and neither framework loads RunicNPC without it. That line /// and requires_plugins in plugin.toml are two statements of one fact, and the /// PR check holds them equal. /// /// [Info("RunicNPC", "Runic Gateway", "0.0.0")] [Description("Runic Gateway's NPCs: profiles, placements and an API for other plugins.")] internal class RunicNPC : RustPlugin { /// /// The version of the API other plugins call (RunicNpc_*, PLAN.md §4). Declared /// twice, here and as api in plugin.toml, which the release copies into /// the tarball's manifest so the installer and the bridge can refuse a RunicNPC too old /// for them before it is loaded. The PR check holds the two equal. /// /// /// It moves when a call or a raised hook changes shape, not on every release: the /// release version says what was built, this says what a caller can rely on. /// /// private const int ApiVersion = 1; /// /// Every hook this plugin declares. Both frameworks bind a hook by name and arity /// through reflection, and neither says a word when a name matches nothing, so the /// plugin counts its own: rnpc.status lists the ones that have never fired. The /// list is seeded at zero, because a hook that never fired is exactly the one a /// dictionary that learns names as they arrive could never report. /// private static readonly string[] ExpectedHooks = { "OnServerInitialized" }; private readonly Dictionary _hookCounts = new Dictionary(); // ---- lifecycle ---- private void Init() { foreach (string hook in ExpectedHooks) _hookCounts[hook] = 0L; } private void OnServerInitialized() { MarkHook("OnServerInitialized"); Puts($"RunicNPC {Version} loaded, API {ApiVersion}. Stage 0: it spawns nothing yet."); } private void MarkHook(string name) { long count; _hookCounts.TryGetValue(name, out count); _hookCounts[name] = count + 1; } // ---- the API (PLAN.md §4) ---- // // Every call is private and prefixed `RunicNpc_`. Private because Oxide's `Call` reaches a // non-public method by name, and a PUBLIC one only when it carries [HookMethod] — the trap // HumanNPC's RefreshNPC and RemoveNPC fell into (PLAN.md §1.2). Prefixed because `Call` // matches by name alone, and no hook of any other plugin may ever match one of ours. /// The API's version. A caller that needs more refuses this RunicNPC and says so. private int RunicNpc_ApiVersion() => ApiVersion; // ---- console ---- /// /// What this RunicNPC is and which of its hooks have fired. Answers over RCON as well /// as at the console, and is the first thing to ask for when a server says it has no /// RunicNPC. /// [ConsoleCommand("rnpc.status")] private void CmdStatus(ConsoleSystem.Arg arg) { if (arg.Connection != null && !arg.IsAdmin) return; var fired = new List(); var silent = new List(); foreach (string hook in ExpectedHooks) { long count; _hookCounts.TryGetValue(hook, out count); if (count > 0L) fired.Add($"{hook}={count}"); else silent.Add(hook); } string firedText = fired.Count > 0 ? string.Join(" ", fired.ToArray()) : "(none)"; string silentText = silent.Count > 0 ? string.Join(" ", silent.ToArray()) : "(none)"; arg.ReplyWith( $"RunicNPC {Version} api={ApiVersion} hooks={ExpectedHooks.Length} " + $"fired={fired.Count} silent={silent.Count}" + Environment.NewLine + $"fired: {firedText}" + Environment.NewLine + $"silent: {silentText}"); } } }