Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of SandboxTools v1.0.0
BepInEx\plugins\SandboxTools.dll
Decompiled 2 weeks agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("SandboxTools")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+5a7893f063ebc01dc2fdd45d41f21693bbde6510")] [assembly: AssemblyProduct("SandboxTools")] [assembly: AssemblyTitle("SandboxTools")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace SandboxTools { internal static class DiscoverActions { internal static bool MuteUnlockMessages; public static void Run(ConsoleEventArgs args) { if (args.Length < 3) { throw new InvalidOperationException("Usage: sandbox discover all|materials|stations|recipes"); } switch (args[2].ToLowerInvariant()) { case "all": DiscoverAll(args); break; case "materials": DiscoverMaterialsOnly(args); break; case "stations": DiscoverStationsOnly(args); break; case "recipes": DiscoverRecipes(args); break; default: throw new InvalidOperationException("Unknown discover target '" + args[2] + "'. Use all, materials, stations, or recipes."); } } private static void DiscoverAll(ConsoleEventArgs args) { Player player = RequirePlayer(); ObjectDB odb = RequireObjectDB(); MuteUnlockMessages = true; try { int num = DiscoverMaterials(player, odb); int num2 = DiscoverStations(player, odb); GameAccess.RefreshPlayerLists(player); string msg = $"Discovered {num} materials, {num2} stations. Known recipes: {GameAccess.KnownRecipes(player).Count}."; Reply(args, player, msg); } finally { MuteUnlockMessages = false; } } private static void DiscoverMaterialsOnly(ConsoleEventArgs args) { Player player = RequirePlayer(); ObjectDB odb = RequireObjectDB(); MuteUnlockMessages = true; try { int num = DiscoverMaterials(player, odb); GameAccess.RefreshPlayerLists(player); string msg = $"Discovered {num} materials. Known materials: {GameAccess.KnownMaterials(player).Count}."; Reply(args, player, msg); } finally { MuteUnlockMessages = false; } } private static void DiscoverStationsOnly(ConsoleEventArgs args) { Player player = RequirePlayer(); ObjectDB odb = RequireObjectDB(); MuteUnlockMessages = true; try { int num = DiscoverStations(player, odb); GameAccess.RefreshPlayerLists(player); string msg = $"Discovered {num} stations. Known stations: {GameAccess.KnownStations(player).Count}."; Reply(args, player, msg); } finally { MuteUnlockMessages = false; } } private static void DiscoverRecipes(ConsoleEventArgs args) { Player player = RequirePlayer(); ObjectDB odb = RequireObjectDB(); MuteUnlockMessages = true; try { HashSet<string> hashSet = GameAccess.KnownRecipes(player); int count = hashSet.Count; int num = 0; foreach (Recipe item in GameAccess.Recipes(odb)) { if (!((Object)(object)item == (Object)null) && Object.op_Implicit((Object)(object)item.m_item)) { num++; AddKnownRecipe(player, item); } } GameAccess.RefreshPlayerLists(player); int num2 = hashSet.Count - count; string msg = $"Unlocked {num2} recipes ({num} scanned, {hashSet.Count} known)."; Reply(args, player, msg); } finally { MuteUnlockMessages = false; } } private static int DiscoverMaterials(Player player, ObjectDB odb) { HashSet<string> hashSet = GameAccess.KnownMaterials(player); int count = hashSet.Count; foreach (GameObject item in GameAccess.Items(odb)) { if (!Object.op_Implicit((Object)(object)item)) { continue; } ItemDrop component = item.GetComponent<ItemDrop>(); if (Object.op_Implicit((Object)(object)component) && component.m_itemData?.m_shared != null) { string name = component.m_itemData.m_shared.m_name; if (!string.IsNullOrEmpty(name)) { hashSet.Add(name); } } } return hashSet.Count - count; } private static int DiscoverStations(Player player, ObjectDB odb) { HashSet<string> names = new HashSet<string>(); foreach (Recipe item in GameAccess.Recipes(odb)) { if (!((Object)(object)item == (Object)null)) { AddStation(item.m_craftingStation); } } foreach (GameObject item2 in GameAccess.Items(odb)) { if (!Object.op_Implicit((Object)(object)item2)) { continue; } AddStation(item2.GetComponent<CraftingStation>()); PieceTable val = item2.GetComponent<ItemDrop>()?.m_itemData?.m_shared?.m_buildPieces; if ((Object)(object)val == (Object)null) { continue; } foreach (GameObject item3 in GameAccess.PieceTablePieces(val)) { if (Object.op_Implicit((Object)(object)item3)) { AddStation(item3.GetComponent<CraftingStation>()); AddFromPiece(item3.GetComponent<Piece>()); } } } foreach (GameObject item4 in GameAccess.BuildPieces(odb)) { if (Object.op_Implicit((Object)(object)item4)) { AddStation(item4.GetComponent<CraftingStation>()); AddFromPiece(item4.GetComponent<Piece>()); } } Dictionary<string, int> dictionary = GameAccess.KnownStations(player); foreach (string item5 in names) { if (dictionary.TryGetValue(item5, out var value)) { if (value < 100) { dictionary[item5] = 100; } } else { dictionary.Add(item5, 100); } } return names.Count; void AddFromPiece(Piece? piece) { if (Object.op_Implicit((Object)(object)piece)) { AddStation(piece.m_craftingStation); } } void AddStation(CraftingStation? station) { if (Object.op_Implicit((Object)(object)station) && !string.IsNullOrEmpty(station.m_name)) { names.Add(station.m_name); } } } private static void AddKnownRecipe(Player player, Recipe recipe) { SharedData val = recipe.m_item?.m_itemData?.m_shared; if (val != null) { string name = val.m_name; if (!string.IsNullOrEmpty(name)) { GameAccess.KnownRecipes(player).Add(name); } } } private static Player RequirePlayer() { Player localPlayer = Player.m_localPlayer; if (!Object.op_Implicit((Object)(object)localPlayer)) { throw new InvalidOperationException("No player."); } return localPlayer; } private static ObjectDB RequireObjectDB() { ObjectDB instance = ObjectDB.instance; if (!Object.op_Implicit((Object)(object)instance)) { throw new InvalidOperationException("ObjectDB not ready."); } return instance; } private static void Reply(ConsoleEventArgs args, Player player, string msg) { args.Context.AddString(msg); ((Character)player).Message((MessageType)1, msg, 0, (Sprite)null, false); } } [HarmonyPatch(typeof(MessageHud), "QueueUnlockMsg")] internal static class MuteUnlockMessagesPatch { private static bool Prefix() { return !DiscoverActions.MuteUnlockMessages; } } internal static class ExportActions { public static void Run(ConsoleEventArgs args, ManualLogSource log) { ExportKind exportKind = ExportKind.All; if (args.Length >= 3) { string text = args[2].ToLowerInvariant(); if (!(text == "items")) { if (!(text == "recipes")) { throw new InvalidOperationException("Usage: sandbox export [items|recipes]"); } exportKind = ExportKind.Recipes; } else { exportKind = ExportKind.Items; } } ExportService.Run(log, exportKind); string text2 = ((exportKind == ExportKind.All) ? "full" : exportKind.ToString().ToLowerInvariant()); args.Context.AddString("SandboxTools: " + text2 + " export complete."); } } public static class SandboxCommand { [CompilerGenerated] private static class <>O { public static ConsoleOptionsFetcher <0>__AutocompleteOptions; } private static readonly string[] Groups = new string[4] { "discover", "skills", "export", "help" }; private static readonly string[] DiscoverTargets = new string[4] { "all", "materials", "stations", "recipes" }; private static readonly string[] ExportTargets = new string[2] { "items", "recipes" }; private static readonly string[] SkillLevels = new string[3] { "100", "50", "0" }; private const string Help = "sandbox <group> ...\n discover all|materials|stations|recipes\n skills [level=100]\n export [items|recipes]"; public static void Register(ManualLogSource log) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown ConsoleEvent val = delegate(ConsoleEventArgs args) { try { Dispatch(args, log); } catch (InvalidOperationException ex) { args.Context.AddString(ex.Message); } catch (Exception ex2) { log.LogError((object)$"sandbox command failed: {ex2}"); args.Context.AddString("sandbox failed: " + ex2.Message); } }; object obj = <>O.<0>__AutocompleteOptions; if (obj == null) { ConsoleOptionsFetcher val2 = AutocompleteOptions; <>O.<0>__AutocompleteOptions = val2; obj = (object)val2; } new ConsoleCommand("sandbox", "SandboxTools cheat helpers. Try: sandbox", val, true, false, false, false, false, false, (ConsoleOptionsFetcher)obj, false, false, false); } private static void Dispatch(ConsoleEventArgs args, ManualLogSource log) { if (args.Length < 2) { args.Context.AddString("sandbox <group> ...\n discover all|materials|stations|recipes\n skills [level=100]\n export [items|recipes]"); return; } switch (args[1].ToLowerInvariant()) { case "discover": DiscoverActions.Run(args); break; case "skills": SkillsActions.Run(args); break; case "export": ExportActions.Run(args, log); break; case "help": case "?": args.Context.AddString("sandbox <group> ...\n discover all|materials|stations|recipes\n skills [level=100]\n export [items|recipes]"); break; default: throw new InvalidOperationException("Unknown sandbox group '" + args[1] + "'.\nsandbox <group> ...\n discover all|materials|stations|recipes\n skills [level=100]\n export [items|recipes]"); } } private static List<string> AutocompleteOptions() { string text = ReadConsoleInput().TrimStart(Array.Empty<char>()); string[] array = SplitKeepingTrailing(text); if (array.Length <= 1) { return Groups.ToList(); } if (array.Length == 2 && !text.EndsWith(" ")) { return FilterPrefix(Groups, array[1]); } if (array.Length == 2 && text.EndsWith(" ")) { return OptionsForGroup(array[1]).ToList(); } if (array.Length >= 3) { string prefix = (text.EndsWith(" ") ? "" : array[^1]); return FilterPrefix(OptionsForGroup(array[1]), prefix); } return Groups.ToList(); } private static string[] OptionsForGroup(string group) { return group.ToLowerInvariant() switch { "discover" => DiscoverTargets, "export" => ExportTargets, "skills" => SkillLevels, _ => Array.Empty<string>(), }; } private static List<string> FilterPrefix(IEnumerable<string> options, string prefix) { if (string.IsNullOrEmpty(prefix)) { return options.ToList(); } return options.Where((string o) => o.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList(); } private static string[] SplitKeepingTrailing(string input) { if (string.IsNullOrEmpty(input)) { return Array.Empty<string>(); } List<string> list = new List<string>(); int num = 0; for (int i = 0; i < input.Length; i++) { if (input[i] == ' ') { list.Add(input.Substring(num, i - num)); num = i + 1; } } list.Add(input.Substring(num)); return list.ToArray(); } private static string ReadConsoleInput() { try { if (Object.op_Implicit((Object)(object)Console.instance)) { string text = ReadTerminalInput((Terminal)(object)Console.instance); if (!string.IsNullOrEmpty(text)) { return text; } } } catch { } try { if (Object.op_Implicit((Object)(object)Chat.instance)) { string text2 = ReadTerminalInput((Terminal)(object)Chat.instance); if (!string.IsNullOrEmpty(text2)) { return text2; } } } catch { } return ""; } private static string? ReadTerminalInput(Terminal terminal) { object value = Traverse.Create((object)terminal).Field("m_input").GetValue(); if (value == null) { return null; } return Traverse.Create(value).Property("text", (object[])null).GetValue<string>() ?? Traverse.Create(value).Field("m_Text").GetValue<string>(); } } internal static class SkillsActions { public static void Run(ConsoleEventArgs args) { //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Invalid comparison between Unknown and I4 //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if (!Object.op_Implicit((Object)(object)localPlayer)) { throw new InvalidOperationException("No player."); } float result = 100f; if (args.Length >= 3 && !float.TryParse(args[2], out result)) { throw new InvalidOperationException("Usage: sandbox skills [level=100]"); } if (result < 0f || result > 100f) { throw new InvalidOperationException($"Level must be between 0 and {100f}."); } Skills skills = ((Character)localPlayer).GetSkills(); int num = 0; foreach (SkillType item in GameAccess.AllSkillTypes()) { if ((int)item != 0 && (int)item != 999 && GameAccess.IsSkillValid(item)) { Skill skill = GameAccess.GetSkill(skills, item); if (skill != null) { GameAccess.SetSkillLevel(skill, result); num++; } } } string text = $"Set {num} skills to {result}."; args.Context.AddString(text); ((Character)localPlayer).Message((MessageType)1, text, 0, (Sprite)null, false); } } internal static class BiomeGuess { private static readonly (string Needle, string Biome)[] NameHints = new(string, string)[61] { ("ashland", "Ashlands"), ("asksvin", "Ashlands"), ("flametal", "Ashlands"), ("charred", "Ashlands"), ("fallen", "Ashlands"), ("mistland", "Mistlands"), ("carapace", "Mistlands"), ("seeker", "Mistlands"), ("gjall", "Mistlands"), ("mage", "Mistlands"), ("eitr", "Mistlands"), ("softtissue", "Mistlands"), ("blackmetal", "Plains"), ("lox", "Plains"), ("flax", "Plains"), ("barley", "Plains"), ("tar", "Plains"), ("deathsquito", "Plains"), ("padded", "Plains"), ("silver", "Mountain"), ("wolf", "Mountain"), ("fenring", "Mountain"), ("freeze", "Mountain"), ("obsidian", "Mountain"), ("onion", "Mountain"), ("crystal", "Mountain"), ("iron", "Swamp"), ("ancientbark", "Swamp"), ("guck", "Swamp"), ("chain", "Swamp"), ("bloodbag", "Swamp"), ("entrails", "Swamp"), ("root", "Swamp"), ("ooze", "Swamp"), ("bronze", "BlackForest"), ("troll", "BlackForest"), ("copper", "BlackForest"), ("tin", "BlackForest"), ("thistle", "BlackForest"), ("blueberry", "BlackForest"), ("surtling", "BlackForest"), ("antler", "BlackForest"), ("deer", "Meadows"), ("boar", "Meadows"), ("neck", "Meadows"), ("flint", "Meadows"), ("raspberry", "Meadows"), ("mushroom", "Meadows"), ("honey", "Meadows"), ("leatherscraps", "Meadows"), ("wood", "Meadows"), ("stone", "Meadows"), ("queen", "Mistlands"), ("goblin", "Plains"), ("yagluth", "Plains"), ("moder", "Mountain"), ("bonemass", "Swamp"), ("elder", "BlackForest"), ("eikthyr", "Meadows"), ("fader", "Ashlands"), ("deepnorth", "DeepNorth") }; private static readonly Dictionary<string, string> StationBiomes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "piece_workbench", "Meadows" }, { "piece_cauldron", "Meadows" }, { "forge", "BlackForest" }, { "piece_artisanstation", "Mistlands" }, { "blackforge", "Mistlands" }, { "piece_magetable", "Mistlands" }, { "piece_oven", "Plains" }, { "piece_spinningwheel", "Plains" }, { "piece_stonecutter", "Mountain" }, { "piece_meadcauldron", "Meadows" }, { "galdrtable", "Mistlands" } }; public static string Guess(string internalName, string itemName, string? stationInternal, string? stationName, IEnumerable<string> materialInternalNames) { foreach (string materialInternalName in materialInternalNames) { string text = FromText(materialInternalName); if (text != "Unknown") { return text; } } string text2 = FromStation(stationInternal, stationName); if (text2 != "Unknown") { return text2; } string text3 = FromText(internalName); if (text3 != "Unknown") { return text3; } return FromText(itemName); } private static string FromStation(string? stationInternal, string? stationName) { if (!string.IsNullOrEmpty(stationInternal)) { foreach (KeyValuePair<string, string> stationBiome in StationBiomes) { if (stationInternal.IndexOf(stationBiome.Key, StringComparison.OrdinalIgnoreCase) >= 0) { return stationBiome.Value; } } } return FromText(stationInternal + " " + stationName); } private static string FromText(string? text) { if (string.IsNullOrEmpty(text)) { return "Unknown"; } string text2 = text.ToLowerInvariant(); for (int i = 0; i < NameHints.Length; i++) { if (text2.IndexOf(NameHints[i].Needle, StringComparison.Ordinal) >= 0) { return NameHints[i].Biome; } } if (text2.IndexOf("ashland", StringComparison.Ordinal) >= 0) { return "Ashlands"; } if (text2.IndexOf("mistland", StringComparison.Ordinal) >= 0) { return "Mistlands"; } if (text2.IndexOf("blackforest", StringComparison.Ordinal) >= 0) { return "BlackForest"; } if (text2.IndexOf("deepnorth", StringComparison.Ordinal) >= 0) { return "DeepNorth"; } if (text2.IndexOf("meadow", StringComparison.Ordinal) >= 0) { return "Meadows"; } if (text2.IndexOf("swamp", StringComparison.Ordinal) >= 0) { return "Swamp"; } if (text2.IndexOf("mountain", StringComparison.Ordinal) >= 0) { return "Mountain"; } if (text2.IndexOf("plain", StringComparison.Ordinal) >= 0) { return "Plains"; } return "Unknown"; } } internal static class Csv { public static string Escape(string? value) { if (value == null) { value = ""; } if (value.IndexOfAny(new char[4] { ',', '"', '\r', '\n' }) < 0) { return value; } return "\"" + value.Replace("\"", "\"\"") + "\""; } public static void Write(string path, IReadOnlyList<string> headers, IEnumerable<IReadOnlyList<string>> rows) { string directoryName = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } StringBuilder stringBuilder = new StringBuilder(); WriteLine(stringBuilder, headers); foreach (IReadOnlyList<string> row in rows) { WriteLine(stringBuilder, row); } UTF8Encoding encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); File.WriteAllText(path, stringBuilder.ToString(), encoding); } private static void WriteLine(StringBuilder sb, IReadOnlyList<string> cells) { for (int i = 0; i < cells.Count; i++) { if (i > 0) { sb.Append(','); } sb.Append(Escape(cells[i])); } sb.Append("\r\n"); } } internal enum ExportKind { All, Items, Recipes } internal static class ExportService { private readonly struct Flags { public readonly bool IsFood; public readonly bool IsWeapon; public readonly bool IsArmor; public readonly bool IsShield; public readonly bool IsTool; public readonly bool IsMaterial; public readonly bool IsPlaceable; public Flags(bool isFood, bool isWeapon, bool isArmor, bool isShield, bool isTool, bool isMaterial, bool isPlaceable) { IsFood = isFood; IsWeapon = isWeapon; IsArmor = isArmor; IsShield = isShield; IsTool = isTool; IsMaterial = isMaterial; IsPlaceable = isPlaceable; } } private sealed class ItemRow { public string ItemName = ""; public string InternalName = ""; public string ItemType = ""; public string Biome = "Unknown"; public float Weight; public int MaxStackSize; public float Durability; public float FoodHealth; public float FoodStamina; public float FoodEitr; public float FoodDuration; public float FoodRegen; public float Armor; public float BlockPower; public float ParryBonus; public string Damage = "0"; public bool IsFood; public bool IsWeapon; public bool IsArmor; public bool IsShield; public bool IsTool; public bool IsMaterial; public bool IsPlaceable; public bool HasRecipe; } private sealed class RecipeRow { public string ItemName = ""; public string InternalName = ""; public string ItemType = ""; public string Biome = "Unknown"; public string CraftingStation = ""; public int MinStationLevel; public int AmountProduced; public float Weight; public int MaxStackSize; public float Durability; public float FoodHealth; public float FoodStamina; public float FoodEitr; public float FoodDuration; public float FoodRegen; public bool IsFood; public bool IsWeapon; public bool IsArmor; public bool IsShield; public bool IsTool; public bool IsMaterial; public bool IsPlaceable; public bool IsCraftable; public List<(string Name, int Amount)> Materials = new List<(string, int)>(); } private static readonly string[] ItemHeaders = new string[24] { "ItemName", "InternalName", "ItemType", "Biome", "Weight", "MaxStackSize", "Durability", "FoodHealth", "FoodStamina", "FoodEitr", "FoodDuration", "FoodRegen", "Armor", "BlockPower", "ParryBonus", "Damage", "IsFood", "IsWeapon", "IsArmor", "IsShield", "IsTool", "IsMaterial", "IsPlaceable", "HasRecipe" }; private static readonly string[] RecipeFixedHeaders = new string[23] { "ItemName", "InternalName", "ItemType", "Biome", "CraftingStation", "MinStationLevel", "AmountProduced", "Weight", "MaxStackSize", "Durability", "FoodHealth", "FoodStamina", "FoodEitr", "FoodDuration", "FoodRegen", "IsFood", "IsWeapon", "IsArmor", "IsShield", "IsTool", "IsMaterial", "IsPlaceable", "IsCraftable" }; public static void Run(ManualLogSource log, ExportKind kind = ExportKind.All) { Stopwatch stopwatch = Stopwatch.StartNew(); ObjectDB instance = ObjectDB.instance; if (!Object.op_Implicit((Object)(object)instance)) { throw new InvalidOperationException("ObjectDB not ready."); } if (!Object.op_Implicit((Object)(object)ZNetScene.instance)) { throw new InvalidOperationException("ZNetScene not ready."); } HashSet<string> craftable = BuildCraftableSet(instance); Dictionary<string, Recipe> recipeByItem = IndexRecipes(instance); List<ItemRow> source = CollectItems(instance, craftable, recipeByItem); List<RecipeRow> source2 = CollectRecipes(instance, craftable); source = source.OrderBy<ItemRow, string>((ItemRow i) => i.InternalName, StringComparer.OrdinalIgnoreCase).ToList(); source2 = source2.OrderBy<RecipeRow, string>((RecipeRow r) => r.InternalName, StringComparer.OrdinalIgnoreCase).ToList(); string text = Path.Combine(Paths.BepInExRootPath, "exports"); Directory.CreateDirectory(text); bool num = kind == ExportKind.All || kind == ExportKind.Items; bool flag = kind == ExportKind.All || kind == ExportKind.Recipes; if (num) { WriteItems(Path.Combine(text, "items.csv"), source); WriteFiltered(text, source); } if (flag) { WriteRecipes(Path.Combine(text, "recipes.csv"), source2); } stopwatch.Stop(); LogSummary(log, source, source2, stopwatch.Elapsed, text, kind); } private static HashSet<string> BuildCraftableSet(ObjectDB odb) { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (Recipe item in GameAccess.Recipes(odb)) { if (!((Object)(object)item == (Object)null) && Object.op_Implicit((Object)(object)item.m_item)) { hashSet.Add(PrefabName(((Component)item.m_item).gameObject)); } } return hashSet; } private static Dictionary<string, Recipe> IndexRecipes(ObjectDB odb) { Dictionary<string, Recipe> dictionary = new Dictionary<string, Recipe>(StringComparer.OrdinalIgnoreCase); foreach (Recipe item in GameAccess.Recipes(odb)) { if (!((Object)(object)item == (Object)null) && Object.op_Implicit((Object)(object)item.m_item)) { string key = PrefabName(((Component)item.m_item).gameObject); if (!dictionary.ContainsKey(key)) { dictionary[key] = item; } } } return dictionary; } private static List<ItemRow> CollectItems(ObjectDB odb, HashSet<string> craftable, Dictionary<string, Recipe> recipeByItem) { HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); List<ItemRow> rows = new List<ItemRow>(); foreach (GameObject item in GameAccess.Items(odb)) { TryAdd(item); } ZNetScene instance = ZNetScene.instance; if (Object.op_Implicit((Object)(object)instance)) { foreach (GameObject item2 in GameAccess.Prefabs(instance)) { TryAdd(item2); } } return rows; void TryAdd(GameObject? go) { if (Object.op_Implicit((Object)(object)go)) { ItemDrop component = go.GetComponent<ItemDrop>(); if (Object.op_Implicit((Object)(object)component) && component.m_itemData?.m_shared != null) { string text = PrefabName(go); if (seen.Add(text)) { recipeByItem.TryGetValue(text, out Recipe value); rows.Add(BuildItemRow(component, text, craftable.Contains(text), value)); } } } } } private static List<RecipeRow> CollectRecipes(ObjectDB odb, HashSet<string> craftable) { List<RecipeRow> list = new List<RecipeRow>(); foreach (Recipe item2 in GameAccess.Recipes(odb)) { if ((Object)(object)item2 == (Object)null || !Object.op_Implicit((Object)(object)item2.m_item)) { continue; } ItemDrop item = item2.m_item; if (item.m_itemData?.m_shared == null) { continue; } string text = PrefabName(((Component)item).gameObject); SharedData shared = item.m_itemData.m_shared; string itemName = Localize(shared.m_name); Flags flags = Classify(shared); CraftingStation craftingStation = item2.m_craftingStation; string text2 = (Object.op_Implicit((Object)(object)craftingStation) ? PrefabName(((Component)craftingStation).gameObject) : ""); string text3 = (Object.op_Implicit((Object)(object)craftingStation) ? Localize(craftingStation.m_name) : ""); List<(string, int)> list2 = new List<(string, int)>(); if (item2.m_resources != null) { Requirement[] resources = item2.m_resources; foreach (Requirement val in resources) { if (val != null && Object.op_Implicit((Object)(object)val.m_resItem)) { string text4 = PrefabName(((Component)val.m_resItem).gameObject); if (!string.IsNullOrEmpty(text4)) { list2.Add((text4, val.m_amount)); } } } } string biome = BiomeGuess.Guess(text, itemName, text2, text3, list2.Select<(string, int), string>(((string Name, int Amount) m) => m.Name)); list.Add(new RecipeRow { ItemName = itemName, InternalName = text, ItemType = ((object)Unsafe.As<ItemType, ItemType>(ref shared.m_itemType)/*cast due to .constrained prefix*/).ToString(), Biome = biome, CraftingStation = (string.IsNullOrEmpty(text3) ? text2 : text3), MinStationLevel = item2.m_minStationLevel, AmountProduced = item2.m_amount, Weight = shared.m_weight, MaxStackSize = shared.m_maxStackSize, Durability = (shared.m_useDurability ? shared.m_maxDurability : 0f), FoodHealth = shared.m_food, FoodStamina = shared.m_foodStamina, FoodEitr = shared.m_foodEitr, FoodDuration = shared.m_foodBurnTime, FoodRegen = shared.m_foodRegen, IsFood = flags.IsFood, IsWeapon = flags.IsWeapon, IsArmor = flags.IsArmor, IsShield = flags.IsShield, IsTool = flags.IsTool, IsMaterial = flags.IsMaterial, IsPlaceable = flags.IsPlaceable, IsCraftable = craftable.Contains(text), Materials = list2 }); } return list; } private static ItemRow BuildItemRow(ItemDrop drop, string internalName, bool hasRecipe, Recipe? recipe) { //IL_018c: Unknown result type (might be due to invalid IL or missing references) SharedData shared = drop.m_itemData.m_shared; string itemName = Localize(shared.m_name); Flags flags = Classify(shared); string stationInternal = null; string stationName = null; IEnumerable<string> materialInternalNames = Array.Empty<string>(); if ((Object)(object)recipe != (Object)null) { CraftingStation craftingStation = recipe.m_craftingStation; if (Object.op_Implicit((Object)(object)craftingStation)) { stationInternal = PrefabName(((Component)craftingStation).gameObject); stationName = Localize(craftingStation.m_name); } if (recipe.m_resources != null) { materialInternalNames = from r in recipe.m_resources where r != null && Object.op_Implicit((Object)(object)r.m_resItem) select PrefabName(((Component)r.m_resItem).gameObject); } } return new ItemRow { ItemName = itemName, InternalName = internalName, ItemType = ((object)Unsafe.As<ItemType, ItemType>(ref shared.m_itemType)/*cast due to .constrained prefix*/).ToString(), Biome = BiomeGuess.Guess(internalName, itemName, stationInternal, stationName, materialInternalNames), Weight = shared.m_weight, MaxStackSize = shared.m_maxStackSize, Durability = (shared.m_useDurability ? shared.m_maxDurability : 0f), FoodHealth = shared.m_food, FoodStamina = shared.m_foodStamina, FoodEitr = shared.m_foodEitr, FoodDuration = shared.m_foodBurnTime, FoodRegen = shared.m_foodRegen, Armor = shared.m_armor, BlockPower = shared.m_blockPower, ParryBonus = shared.m_timedBlockBonus, Damage = FormatDamage(shared.m_damages), IsFood = flags.IsFood, IsWeapon = flags.IsWeapon, IsArmor = flags.IsArmor, IsShield = flags.IsShield, IsTool = flags.IsTool, IsMaterial = flags.IsMaterial, IsPlaceable = flags.IsPlaceable, HasRecipe = hasRecipe }; } private static Flags Classify(SharedData shared) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Invalid comparison between Unknown and I4 //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Invalid comparison between Unknown and I4 //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Invalid comparison between Unknown and I4 //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Invalid comparison between Unknown and I4 //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Invalid comparison between Unknown and I4 //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Invalid comparison between Unknown and I4 //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Invalid comparison between Unknown and I4 //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Invalid comparison between Unknown and I4 //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Invalid comparison between Unknown and I4 //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Invalid comparison between Unknown and I4 //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Invalid comparison between Unknown and I4 //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Invalid comparison between Unknown and I4 //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Invalid comparison between Unknown and I4 //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Invalid comparison between Unknown and I4 ItemType itemType = shared.m_itemType; bool isFood = (int)itemType == 2 && (shared.m_food > 0f || shared.m_foodStamina > 0f || shared.m_foodEitr > 0f); bool isWeapon = (int)itemType == 3 || (int)itemType == 14 || (int)itemType == 22 || (int)itemType == 4 || (int)itemType == 15 || (int)itemType == 20; bool isArmor = (int)itemType == 6 || (int)itemType == 7 || (int)itemType == 11 || (int)itemType == 17 || (int)itemType == 12; bool isShield = (int)itemType == 5; bool isTool = (int)itemType == 19; bool isMaterial = (int)itemType == 1; bool isPlaceable = (Object)(object)shared.m_buildPieces != (Object)null; return new Flags(isFood, isWeapon, isArmor, isShield, isTool, isMaterial, isPlaceable); } private static string FormatDamage(DamageTypes dmg) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) float num; try { num = ((DamageTypes)(ref dmg)).GetTotalDamage(); } catch { num = dmg.m_damage + dmg.m_blunt + dmg.m_slash + dmg.m_pierce + dmg.m_chop + dmg.m_pickaxe + dmg.m_fire + dmg.m_frost + dmg.m_lightning + dmg.m_poison + dmg.m_spirit; } if (num <= 0f) { return "0"; } List<string> parts = new List<string>(); Add("slash", dmg.m_slash); Add("blunt", dmg.m_blunt); Add("pierce", dmg.m_pierce); Add("chop", dmg.m_chop); Add("pickaxe", dmg.m_pickaxe); Add("fire", dmg.m_fire); Add("frost", dmg.m_frost); Add("lightning", dmg.m_lightning); Add("poison", dmg.m_poison); Add("spirit", dmg.m_spirit); Add("damage", dmg.m_damage); if (parts.Count != 0) { return F(num) + " (" + string.Join(" ", parts) + ")"; } return F(num); void Add(string label, float v) { if (v > 0f) { parts.Add(label + ":" + F(v)); } } } private static void WriteItems(string path, List<ItemRow> items) { List<IReadOnlyList<string>> rows = ((IEnumerable<ItemRow>)items).Select((Func<ItemRow, IReadOnlyList<string>>)((ItemRow i) => new string[24] { i.ItemName, i.InternalName, i.ItemType, i.Biome, F(i.Weight), i.MaxStackSize.ToString(CultureInfo.InvariantCulture), F(i.Durability), F(i.FoodHealth), F(i.FoodStamina), F(i.FoodEitr), F(i.FoodDuration), F(i.FoodRegen), F(i.Armor), F(i.BlockPower), F(i.ParryBonus), i.Damage, Bool(i.IsFood), Bool(i.IsWeapon), Bool(i.IsArmor), Bool(i.IsShield), Bool(i.IsTool), Bool(i.IsMaterial), Bool(i.IsPlaceable), Bool(i.HasRecipe) })).ToList(); Csv.Write(path, ItemHeaders, rows); } private static void WriteRecipes(string path, List<RecipeRow> recipes) { int num = 0; foreach (RecipeRow recipe in recipes) { if (recipe.Materials.Count > num) { num = recipe.Materials.Count; } } List<string> list = new List<string>(RecipeFixedHeaders); for (int i = 1; i <= num; i++) { list.Add($"Material{i}"); list.Add($"Material{i}Amount"); } List<IReadOnlyList<string>> list2 = new List<IReadOnlyList<string>>(recipes.Count); foreach (RecipeRow recipe2 in recipes) { List<string> list3 = new List<string>(list.Count) { recipe2.ItemName, recipe2.InternalName, recipe2.ItemType, recipe2.Biome, recipe2.CraftingStation, recipe2.MinStationLevel.ToString(CultureInfo.InvariantCulture), recipe2.AmountProduced.ToString(CultureInfo.InvariantCulture), F(recipe2.Weight), recipe2.MaxStackSize.ToString(CultureInfo.InvariantCulture), F(recipe2.Durability), F(recipe2.FoodHealth), F(recipe2.FoodStamina), F(recipe2.FoodEitr), F(recipe2.FoodDuration), F(recipe2.FoodRegen), Bool(recipe2.IsFood), Bool(recipe2.IsWeapon), Bool(recipe2.IsArmor), Bool(recipe2.IsShield), Bool(recipe2.IsTool), Bool(recipe2.IsMaterial), Bool(recipe2.IsPlaceable), Bool(recipe2.IsCraftable) }; for (int j = 0; j < num; j++) { if (j < recipe2.Materials.Count) { list3.Add(recipe2.Materials[j].Name); list3.Add(recipe2.Materials[j].Amount.ToString(CultureInfo.InvariantCulture)); } else { list3.Add(""); list3.Add(""); } } list2.Add(list3); } Csv.Write(path, list, list2); } private static void WriteFiltered(string exportDir, List<ItemRow> items) { WriteSubset(Path.Combine(exportDir, "foods.csv"), (from i in items where i.IsFood orderby i.FoodHealth + i.FoodStamina + i.FoodEitr descending select i).ThenBy<ItemRow, string>((ItemRow i) => i.InternalName, StringComparer.OrdinalIgnoreCase)); WriteSubset(Path.Combine(exportDir, "weapons.csv"), items.Where((ItemRow i) => i.IsWeapon).OrderBy<ItemRow, string>((ItemRow i) => i.InternalName, StringComparer.OrdinalIgnoreCase)); WriteSubset(Path.Combine(exportDir, "armor.csv"), items.Where((ItemRow i) => i.IsArmor).OrderBy<ItemRow, string>((ItemRow i) => i.InternalName, StringComparer.OrdinalIgnoreCase)); WriteSubset(Path.Combine(exportDir, "materials.csv"), items.Where((ItemRow i) => i.IsMaterial).OrderBy<ItemRow, string>((ItemRow i) => i.InternalName, StringComparer.OrdinalIgnoreCase)); } private static void WriteSubset(string path, IEnumerable<ItemRow> items) { WriteItems(path, items.ToList()); } private static void LogSummary(ManualLogSource log, List<ItemRow> items, List<RecipeRow> recipes, TimeSpan elapsed, string exportDir, ExportKind kind) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"SandboxTools {kind.ToString().ToLowerInvariant()} export finished in {elapsed.TotalMilliseconds:0} ms -> {exportDir}"); if (kind == ExportKind.All || kind == ExportKind.Items) { stringBuilder.AppendLine($"Total Items: {items.Count}"); stringBuilder.AppendLine($"Food Count: {items.Count((ItemRow i) => i.IsFood)}"); stringBuilder.AppendLine($"Weapon Count: {items.Count((ItemRow i) => i.IsWeapon)}"); stringBuilder.AppendLine($"Armor Count: {items.Count((ItemRow i) => i.IsArmor)}"); stringBuilder.AppendLine($"Shield Count: {items.Count((ItemRow i) => i.IsShield)}"); stringBuilder.AppendLine($"Tool Count: {items.Count((ItemRow i) => i.IsTool)}"); stringBuilder.AppendLine($"Material Count: {items.Count((ItemRow i) => i.IsMaterial)}"); stringBuilder.AppendLine($"Placeable Count: {items.Count((ItemRow i) => i.IsPlaceable)}"); } if (kind == ExportKind.All || kind == ExportKind.Recipes) { stringBuilder.AppendLine($"Total Recipes: {recipes.Count}"); } log.LogInfo((object)stringBuilder.ToString().TrimEnd(Array.Empty<char>())); } private static string Localize(string? token) { if (string.IsNullOrEmpty(token)) { return ""; } try { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { Type type = assemblies[i].GetType("Localization"); if (type == null) { continue; } object obj = type.GetProperty("instance", BindingFlags.Static | BindingFlags.Public)?.GetValue(null); if (obj == null) { continue; } MethodInfo method = type.GetMethod("Localize", new Type[1] { typeof(string) }); if (!(method == null)) { string text = method.Invoke(obj, new object[1] { token }) as string; if (!string.IsNullOrEmpty(text)) { return text; } } } } catch { } return token ?? ""; } private static string PrefabName(GameObject go) { string name = ((Object)go).name; if (name.EndsWith("(Clone)", StringComparison.Ordinal)) { return name.Substring(0, name.Length - "(Clone)".Length).TrimEnd(Array.Empty<char>()); } return name; } private static string F(float v) { return v.ToString("0.###", CultureInfo.InvariantCulture); } private static string Bool(bool v) { if (!v) { return "FALSE"; } return "TRUE"; } } internal static class GameAccess { public static List<Recipe> Recipes(ObjectDB odb) { return RequireField<List<Recipe>>(odb, "m_recipes"); } public static List<GameObject> Items(ObjectDB odb) { return RequireField<List<GameObject>>(odb, "m_items"); } public static IEnumerable<GameObject> BuildPieces(ObjectDB odb) { return EnumerateGameObjects(odb, "m_buildPieces"); } public static IEnumerable<GameObject> PieceTablePieces(PieceTable table) { return EnumerateGameObjects(table, "m_pieces"); } public static List<GameObject> Prefabs(ZNetScene scene) { List<GameObject> list = Field<List<GameObject>>(scene, "m_prefabs"); if (list != null) { return list; } Dictionary<int, GameObject> dictionary = Field<Dictionary<int, GameObject>>(scene, "m_namedPrefabs"); if (dictionary == null) { return new List<GameObject>(); } List<GameObject> list2 = new List<GameObject>(dictionary.Count); foreach (KeyValuePair<int, GameObject> item in dictionary) { if (Object.op_Implicit((Object)(object)item.Value)) { list2.Add(item.Value); } } return list2; } public static HashSet<string> KnownRecipes(Player player) { return RequireField<HashSet<string>>(player, "m_knownRecipes"); } public static HashSet<string> KnownMaterials(Player player) { return RequireField<HashSet<string>>(player, "m_knownMaterial"); } public static Dictionary<string, int> KnownStations(Player player) { return RequireField<Dictionary<string, int>>(player, "m_knownStations"); } public static void RefreshPlayerLists(Player player) { Call(player, "UpdateKnownRecipesList"); Call(player, "UpdateAvailablePiecesList"); } public static void SetSkillLevel(Skill skill, float level) { Traverse obj = Traverse.Create((object)skill); obj.Field("m_level").SetValue((object)level); obj.Field("m_accumulator").SetValue((object)0f); } public static IEnumerable<SkillType> AllSkillTypes() { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) SkillType[] value = Traverse.Create(typeof(Skills)).Field("s_allSkills").GetValue<SkillType[]>(); if (value != null && value.Length != 0) { return value; } List<SkillType> list = new List<SkillType>(); foreach (SkillType value2 in Enum.GetValues(typeof(SkillType))) { list.Add(value2); } return list; } public static bool IsSkillValid(SkillType type) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) try { return Skills.IsSkillValid(type); } catch (MethodAccessException) { return Traverse.Create(typeof(Skills)).Method("IsSkillValid", new object[1] { type }).GetValue<bool>(); } } public static Skill GetSkill(Skills skills, SkillType type) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) try { return skills.GetSkill(type); } catch (MethodAccessException) { return Traverse.Create((object)skills).Method("GetSkill", new object[1] { type }).GetValue<Skill>(); } } public static T? Field<T>(object obj, string name) where T : class { try { return Traverse.Create(obj).Field(name).GetValue<T>(); } catch (InvalidCastException) { return null; } } public static T RequireField<T>(object obj, string name) where T : class { return Field<T>(obj, name) ?? throw new InvalidOperationException(obj.GetType().Name + "." + name + " unavailable."); } public static void Call(object obj, string method, params object[] args) { ((args.Length == 0) ? Traverse.Create(obj).Method(method, Array.Empty<object>()) : Traverse.Create(obj).Method(method, args)).GetValue(); } private static IEnumerable<GameObject> EnumerateGameObjects(object obj, string fieldName) { object value; try { value = Traverse.Create(obj).Field(fieldName).GetValue(); } catch (Exception) { yield break; } if (value == null) { yield break; } if (value is IEnumerable enumerable) { foreach (object item in enumerable) { GameObject val = AsGameObject(item); if (Object.op_Implicit((Object)(object)val)) { yield return val; } } } else { GameObject val2 = AsGameObject(value); if (Object.op_Implicit((Object)(object)val2)) { yield return val2; } } } private static GameObject? AsGameObject(object? item) { GameObject val = (GameObject)((item is GameObject) ? item : null); if (val == null) { Component val2 = (Component)((item is Component) ? item : null); if (val2 != null) { if (!Object.op_Implicit((Object)(object)val2)) { return null; } return val2.gameObject; } return null; } if (!Object.op_Implicit((Object)(object)val)) { return null; } return val; } } [BepInPlugin("matthb1.sandboxtools", "SandboxTools", "1.0.0")] public class SandboxToolsPlugin : BaseUnityPlugin { public const string GUID = "matthb1.sandboxtools"; public const string NAME = "SandboxTools"; public const string VERSION = "1.0.0"; private bool _exportedThisSession; private void Awake() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) ((BaseUnityPlugin)this).Logger.LogInfo((object)"SandboxTools 1.0.0 loaded - type 'sandbox' for commands."); new Harmony("matthb1.sandboxtools").PatchAll(); SandboxCommand.Register(((BaseUnityPlugin)this).Logger); ((MonoBehaviour)this).StartCoroutine(WaitUntilReadyThenExport()); } private IEnumerator WaitUntilReadyThenExport() { while ((Object)(object)ObjectDB.instance == (Object)null || (Object)(object)ZNetScene.instance == (Object)null) { yield return (object)new WaitForSeconds(1f); } int lastRecipes = -1; int lastItems = -1; int stableTicks = 0; while (stableTicks < 4) { yield return (object)new WaitForSeconds(0.5f); ObjectDB instance = ObjectDB.instance; ZNetScene instance2 = ZNetScene.instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance2 == (Object)null) { stableTicks = 0; continue; } int count = GameAccess.Recipes(instance).Count; int count2 = GameAccess.Items(instance).Count; if (count > 0 && count2 > 0 && count == lastRecipes && count2 == lastItems) { stableTicks++; continue; } stableTicks = 0; lastRecipes = count; lastItems = count2; } if (_exportedThisSession) { yield break; } _exportedThisSession = true; try { ExportService.Run(((BaseUnityPlugin)this).Logger); } catch (Exception arg) { ((BaseUnityPlugin)this).Logger.LogError((object)$"Automatic export failed: {arg}"); _exportedThisSession = false; } } } }