feat(drill): the staging drill, the week before each forced wipe (stage 9c, D307-D309, D317-D319)

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
This commit is contained in:
2026-10-07 01:29:45 -05:00
parent 9b316c6266
commit c25f73c525
9 changed files with 1058 additions and 8 deletions

View File

@@ -12,12 +12,28 @@
// 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;
}
@@ -96,3 +112,28 @@ 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;
}