// 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}");
}
}
}