Files
runicnpc-rust/tools/fieldlist/Program.cs
wtclaude f8084955e8
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m38s
feat: stage 2, the NPC and its API (API 2)
RunicNPC now spawns its own NPC: Rust's scientist with its ScientistNPC and
ScientistBrain swapped for ours, copying a fixed field list generated from
Rust's unmodified assembly (D232, tools/fieldlist). Profiles live in
data/RunicNPC/profiles.json in the approved shape (D238); placements persist
with each/group respawn (D236) and wait for a missing profile or route
(D237); roamers wander, follow Rust's monument paths, or walk a route (D233,
D234), with our own chase where Rust's needs an AI zone; sentries hold their
spot; NPCs walk home and sleep past 160 m of any player (D235). Spawns wait
for the navmesh and are spread over frames; caps are off by default and the
cost warning is shown instead (D227). The whole PLAN.md section 4 API and its
hooks are in, documented in docs/runicnpc/API.md.

tools/RunicNpcTest.cs is the stage 2 harness; every group passed on both
rigs, and after a plugin reload and a server restart. checkPlugin no longer
counts an override (RunicNpcPlayer.OnDied) as a hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-30 01:31:04 -05:00

99 lines
4.0 KiB
C#

// Writes the list of fields RunicNPC's swap copies from Rust's scientist to ours (D232).
//
// node tools/managed.js carbon <dir> (once per Rust update)
// dotnet run --project tools/fieldlist -- <dir> [plugin/RunicNPC.cs]
//
// <dir> must hold the CARBON rig's managed assemblies: Oxide's patcher makes Rust's private fields
// public, so its Assembly-CSharp.dll no longer says what Rust itself serialises.
//
// The rule is stage 1's, which is Unity's own for a component's authored data: every instance
// field from the component's type down to (not including) MonoBehaviour that is public and not
// [NonSerialized], or carries [SerializeField]; never readonly, const or a delegate. Applied here to
// the unmodified assembly, it picks the same fields on both frameworks. The result replaces the
// block between the two marker lines in the plugin, and the plugin resolves each name at load.
using System.Reflection;
using System.Text;
if (args.Length < 1)
{
Console.Error.WriteLine("usage: dotnet run --project tools/fieldlist -- <managed dir> [plugin/RunicNPC.cs]");
return 2;
}
string managed = args[0];
string plugin = args.Length > 1 ? args[1] : Path.Combine("plugin", "RunicNPC.cs");
string[] dlls = Directory.GetFiles(managed, "*.dll");
var resolver = new PathAssemblyResolver(dlls);
using var context = new MetadataLoadContext(resolver, "mscorlib");
Assembly rust = context.LoadFromAssemblyPath(Path.Combine(managed, "Assembly-CSharp.dll"));
List<string> Fields(string typeName)
{
Type type = rust.GetType(typeName, throwOnError: true)!;
var names = new List<string>();
for (Type? t = type; t != null && t.FullName != "UnityEngine.MonoBehaviour"; t = t.BaseType)
{
foreach (FieldInfo f in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
{
if (f.IsInitOnly || f.IsLiteral)
continue;
bool serialised = (f.IsPublic && (f.Attributes & FieldAttributes.NotSerialized) == 0) ||
f.GetCustomAttributesData().Any(a => a.AttributeType.FullName == "UnityEngine.SerializeField");
if (!serialised || IsDelegate(f.FieldType))
continue;
names.Add($"{t.Name}.{f.Name}");
}
}
return names;
}
static bool IsDelegate(Type t)
{
for (Type? b = t; b != null; b = b.BaseType)
if (b.FullName == "System.Delegate")
return true;
return false;
}
List<string> npc = Fields("ScientistNPC");
List<string> brain = Fields("ScientistBrain");
Guid mvid = rust.ManifestModule.ModuleVersionId;
const string Open = " // <fieldlist>";
const string Close = " // </fieldlist>";
var block = new StringBuilder();
block.AppendLine(Open);
block.AppendLine($" // Generated by tools/fieldlist from Rust's Assembly-CSharp.dll, module {mvid}.");
block.AppendLine(" // Do not edit by hand: regenerate after a Rust update (D232).");
Append(block, "SwapNpcFields", npc);
Append(block, "SwapBrainFields", brain);
block.Append(Close);
static void Append(StringBuilder sb, string name, List<string> fields)
{
sb.AppendLine($" private static readonly string[] {name} =");
sb.AppendLine(" {");
foreach (string f in fields)
sb.AppendLine($" \"{f}\",");
sb.AppendLine(" };");
}
string text = File.ReadAllText(plugin);
string nl = text.Contains("\r\n") ? "\r\n" : "\n";
int start = text.IndexOf(Open, StringComparison.Ordinal);
int end = text.IndexOf(Close, StringComparison.Ordinal);
if (start < 0 || end < start)
{
Console.Error.WriteLine($"{plugin}: the marker lines '{Open.Trim()}' and '{Close.Trim()}' were not found.");
return 1;
}
string replacement = block.ToString().Replace("\r\n", "\n").Replace("\n", nl);
File.WriteAllText(plugin, text[..start] + replacement + text[(end + Close.Length)..]);
Console.WriteLine($"{plugin}: {npc.Count} NPC fields, {brain.Count} brain fields (Assembly-CSharp module {mvid}).");
return 0;