feat: stage 0, an empty RunicNPC that releases through CI
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m44s
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m44s
The repository RunicNPC is built in (docs/runicnpc/PLAN.md §9, stage 0): - plugin/RunicNPC.cs: `// Requires: Kits` (D217), `[Info]` with the 0.0.0 placeholder the release stamps, `RunicNpc_ApiVersion()` (API 1), and `rnpc.status`, which reports the version and which hooks have fired. It spawns nothing. - plugin.toml: the API version, the framework floors it was loaded on (Oxide 2.0.7726, Carbon 2.0.259) and requires_plugins = ["Kits"]. - scripts/checkPlugin.js, adapted from Rust-Plugins': every hook listed and void unless written down; chat-command signatures; every RunicNpc_ call reachable by Call (the HumanNPC trap, PLAN.md §1.2); ApiVersion, `// Requires:` and [Info] agreeing with plugin.toml. 23 self-tests, including the real plugin and a CRLF checkout. - PR Checks on PRs into main and edge; the release workflow on main, with Rust-Plugins' release engine unchanged and an adapter that ships runicnpc-<ver>.tar.gz (runicnpc/RunicNPC.cs + manifest.json) and SHA256SUMS. No bundle dispatch until stage 4. - tools/: the rig panel scripts, with the panel and server ids moved into a git-ignored tools/rigs.json. `con.js` became `console.js`: CON is a reserved device name on Windows, and git there cannot open the file. - README, CONTRIBUTING (edge-based flow, AI disclosure, borrow-not-copy), SECURITY, the code of conduct, issue and PR templates. `feat:` so the cutover to main cuts the first release, 0.1.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
123
plugin/RunicNPC.cs
Normal file
123
plugin/RunicNPC.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
// Requires: Kits
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Oxide.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// RunicNPC — Runic Gateway's own NPC plugin for Rust, on Oxide and Carbon.
|
||||
///
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Kits is required</b> (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 <c>requires_plugins</c> in <c>plugin.toml</c> are two statements of one fact, and the
|
||||
/// PR check holds them equal.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Info("RunicNPC", "Runic Gateway", "0.0.0")]
|
||||
[Description("Runic Gateway's NPCs: profiles, placements and an API for other plugins.")]
|
||||
internal class RunicNPC : RustPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// The version of the API other plugins call (<c>RunicNpc_*</c>, PLAN.md §4). Declared
|
||||
/// twice, here and as <c>api</c> in <c>plugin.toml</c>, 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.
|
||||
///
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private const int ApiVersion = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 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: <c>rnpc.status</c> 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.
|
||||
/// </summary>
|
||||
private static readonly string[] ExpectedHooks =
|
||||
{
|
||||
"OnServerInitialized"
|
||||
};
|
||||
|
||||
private readonly Dictionary<string, long> _hookCounts = new Dictionary<string, long>();
|
||||
|
||||
// ---- 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.
|
||||
|
||||
/// <summary>The API's version. A caller that needs more refuses this RunicNPC and says so.</summary>
|
||||
private int RunicNpc_ApiVersion() => ApiVersion;
|
||||
|
||||
// ---- console ----
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[ConsoleCommand("rnpc.status")]
|
||||
private void CmdStatus(ConsoleSystem.Arg arg)
|
||||
{
|
||||
if (arg.Connection != null && !arg.IsAdmin)
|
||||
return;
|
||||
|
||||
var fired = new List<string>();
|
||||
var silent = new List<string>();
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user