A Gitea job that checks each new Rust staging build in the seven days before the monthly forced wipe: - tools/drill/static.sh: staging's managed assemblies (DepotDownloader) and Oxide's staging build; whether Oxide has caught up (fieldlist --oxide-behind), the swap's field list compared, and the plugin compiled. - tools/drill/drill.js: window (D318), gate (one check per depot manifest), live (rust-staging runs rnt.run all; rust-carbon is stopped and restarted when both 6000-map rigs run, D309) and report (one issue per build, state on the drill branch; a build waits up to three days for Oxide, D317). - tools/drill/rig-start.sh: the rig's startup; installs the branch and Oxide's build for it, and refuses to start a mismatched pair. - RunicNpcTest 0.7.2: stage 5's five fights look for a field with room for their 280 m line (D319), and the spawn check names what did not spawn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
140 lines
6.2 KiB
C#
140 lines
6.2 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.
|
|
|
|
//
|
|
// The staging drill (D308) also asks it whether Oxide's build has caught up with Rust's:
|
|
//
|
|
// dotnet run --project tools/fieldlist -- --oxide-behind <managed dir> <Oxide's Assembly-CSharp.dll>
|
|
//
|
|
// Oxide ships its own patched Assembly-CSharp.dll, which replaces Rust's. Built from an older Rust,
|
|
// it lacks what Rust has added since, and the server dies at boot ("The referenced script
|
|
// (ItemModFoodVisual) on this Behaviour is missing!"). Oxide's patcher only ever adds, so every
|
|
// type and member Rust's own assembly declares, compiler-generated ones aside, must be in Oxide's.
|
|
// It prints "behind <n>" and the first of them; 0 means caught up. Measured 2026-10-06: public Rust
|
|
// and public Oxide, 0; staging Rust 25766353 and Oxide's staging build of 2026-10-05, 482.
|
|
|
|
using System.Reflection;
|
|
using System.Text;
|
|
|
|
if (args.Length == 3 && args[0] == "--oxide-behind")
|
|
return OxideBehind(args[1], args[2]);
|
|
|
|
if (args.Length < 1)
|
|
{
|
|
Console.Error.WriteLine("usage: dotnet run --project tools/fieldlist -- <managed dir> [plugin/RunicNPC.cs]");
|
|
Console.Error.WriteLine(" dotnet run --project tools/fieldlist -- --oxide-behind <managed dir> <Oxide's Assembly-CSharp.dll>");
|
|
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;
|
|
|
|
static int OxideBehind(string managed, string oxideAssembly)
|
|
{
|
|
HashSet<string> Declared(string assembly)
|
|
{
|
|
IEnumerable<string> others = Directory.GetFiles(managed, "*.dll").Where(f => Path.GetFileName(f) != "Assembly-CSharp.dll");
|
|
using var context = new MetadataLoadContext(new PathAssemblyResolver(others.Append(assembly)), "mscorlib");
|
|
var names = new HashSet<string>();
|
|
foreach (Type t in context.LoadFromAssemblyPath(assembly).GetTypes())
|
|
{
|
|
names.Add(t.FullName!);
|
|
foreach (MemberInfo m in t.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
|
|
names.Add($"{t.FullName}::{m.Name}");
|
|
}
|
|
// Compiler-generated names (lambdas, closures, iterators) are renumbered by every build.
|
|
names.RemoveWhere(n => n.Contains('<'));
|
|
return names;
|
|
}
|
|
|
|
List<string> missing = Declared(Path.Combine(managed, "Assembly-CSharp.dll")).Except(Declared(oxideAssembly)).OrderBy(n => n).ToList();
|
|
Console.WriteLine($"behind {missing.Count}");
|
|
foreach (string n in missing.Take(20))
|
|
Console.WriteLine(" " + n);
|
|
return 0;
|
|
}
|