// Writes the list of fields RunicNPC's swap copies from Rust's scientist to ours (D232). // // node tools/managed.js carbon (once per Rust update) // dotnet run --project tools/fieldlist -- [plugin/RunicNPC.cs] // // 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 -- [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 Fields(string typeName) { Type type = rust.GetType(typeName, throwOnError: true)!; var names = new List(); 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 npc = Fields("ScientistNPC"); List brain = Fields("ScientistBrain"); Guid mvid = rust.ManifestModule.ModuleVersionId; const string Open = " // "; const string Close = " // "; 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 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;