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 CraftingLibrary v1.0.0
CraftingLibrary.dll
Decompiled 4 hours 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.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.TextCore; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("AmazingAssets.TerrainToMesh")] [assembly: IgnoresAccessChecksTo("Assembly-CSharp-firstpass")] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: IgnoresAccessChecksTo("ClientNetworkTransform")] [assembly: IgnoresAccessChecksTo("com.olegknyazev.softmask")] [assembly: IgnoresAccessChecksTo("DissonanceVoip")] [assembly: IgnoresAccessChecksTo("DunGen")] [assembly: IgnoresAccessChecksTo("DunGen.Integration.ASPP")] [assembly: IgnoresAccessChecksTo("DunGen.Integration.UnityNav")] [assembly: IgnoresAccessChecksTo("EasyTextEffects")] [assembly: IgnoresAccessChecksTo("Facepunch Transport for Netcode for GameObjects")] [assembly: IgnoresAccessChecksTo("Facepunch.Steamworks.Win64")] [assembly: IgnoresAccessChecksTo("Unity.AI.Navigation")] [assembly: IgnoresAccessChecksTo("Unity.Animation.Rigging")] [assembly: IgnoresAccessChecksTo("Unity.Animation.Rigging.DocCodeExamples")] [assembly: IgnoresAccessChecksTo("Unity.Burst")] [assembly: IgnoresAccessChecksTo("Unity.Burst.Unsafe")] [assembly: IgnoresAccessChecksTo("Unity.Collections")] [assembly: IgnoresAccessChecksTo("Unity.Collections.LowLevel.ILSupport")] [assembly: IgnoresAccessChecksTo("Unity.InputSystem")] [assembly: IgnoresAccessChecksTo("Unity.InputSystem.ForUI")] [assembly: IgnoresAccessChecksTo("Unity.Jobs")] [assembly: IgnoresAccessChecksTo("Unity.Mathematics")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.Common")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.MetricTypes")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStats")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Component")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Configuration")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Implementation")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsReporting")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetworkProfiler.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetworkSolutionInterface")] [assembly: IgnoresAccessChecksTo("Unity.Netcode.Components")] [assembly: IgnoresAccessChecksTo("Unity.Netcode.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.Networking.Transport")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Csg")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.KdTree")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Poly2Tri")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Stl")] [assembly: IgnoresAccessChecksTo("Unity.Profiling.Core")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.Core.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.Core.ShaderLibrary")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.HighDefinition.Config.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.HighDefinition.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary")] [assembly: IgnoresAccessChecksTo("Unity.Services.Authentication")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Analytics")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Components")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Configuration")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Device")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Environments")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Environments.Internal")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Internal")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Networking")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Registration")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Scheduler")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Telemetry")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Threading")] [assembly: IgnoresAccessChecksTo("Unity.Services.QoS")] [assembly: IgnoresAccessChecksTo("Unity.Services.Relay")] [assembly: IgnoresAccessChecksTo("Unity.TextMeshPro")] [assembly: IgnoresAccessChecksTo("Unity.Timeline")] [assembly: IgnoresAccessChecksTo("Unity.VisualEffectGraph.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.XR.CoreUtils")] [assembly: IgnoresAccessChecksTo("Unity.XR.Management")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.ConformanceAutomation")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.MetaQuestSupport")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.MockRuntime")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.OculusQuestSupport")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.RuntimeDebugger")] [assembly: IgnoresAccessChecksTo("UnityEngine.ARModule")] [assembly: IgnoresAccessChecksTo("UnityEngine.NVIDIAModule")] [assembly: IgnoresAccessChecksTo("UnityEngine.SpatialTracking")] [assembly: IgnoresAccessChecksTo("UnityEngine.UI")] [assembly: IgnoresAccessChecksTo("UnityEngine.XR.LegacyInputHelpers")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("CraftingLibrary")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("A crafting library for Lethal Company. Scrap lying in the ship becomes other items, from recipes written in plain text or registered by other mods. Ships no recipes of its own.")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("CraftingLibrary")] [assembly: AssemblyTitle("CraftingLibrary")] [assembly: AssemblyMetadata("AI_Assisted_Creation", "This assembly was fully created with the assistance of Generative AI: the code, its documentation and the icon-drawing script were written by Claude (Anthropic) through Claude Code, directed and play-tested by Maco.")] [assembly: AssemblyMetadata("AI_Model_Vendor", "Anthropic")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 CraftingLibrary { internal static class Cfg { public static ConfigEntry<string> Verb; public static ConfigEntry<bool> ShowInMenu; public static ConfigEntry<bool> Interactive; public static ConfigEntry<int> PageLines; public static ConfigEntry<int> PageWidth; public static ConfigEntry<bool> MarkParts; public static ConfigEntry<string> PartMark; public static ConfigEntry<bool> WarnAtDesk; public static ConfigEntry<bool> VerboseLogs; public static void Bind(ConfigFile c) { Verb = c.Bind<string>("Terminal", "Command", "craft", "The word typed at the ship terminal to open the crafting list.\nChange it if another mod already owns 'craft'. The terminal strips punctuation and lowercases everything before this is matched, so use a single plain word."); ShowInMenu = c.Bind<bool>("Terminal", "Show In Menu", true, "Add the command to the terminal's front page, under MOONS and STORE.\nThe page is found by looking for '>MOONS' in whatever node the terminal loads, rather than by index, so it works with terminal reskins like TerminalFormatter. Turn it off if a reskin does not like being written to."); Interactive = c.Bind<bool>("Terminal", "Cursor Controls", true, "Drive the craft list with W/S, A/D and Enter, the way Lategame Upgrades does.\nWhile it is open the terminal's input field is disabled so the keys are ours; Backspace hands it back. Turn this off to get a plain printed list instead, which is also the thing to try if some other mod fights over the terminal's keyboard."); PageLines = c.Bind<int>("Terminal", "Recipe Lines Per Page", 0, "How many lines of recipe one page of the craft list may use, header and footer excluded. 0 measures the screen and uses everything it will hold.\nThe list is paged rather than scrolled because the terminal snaps the scrollbar to the bottom on every keypress (Terminal.cs:174-183), which throws a taller page away the moment you start typing. Set a number here only if the measurement is wrong: raise it if the page leaves empty space at the bottom, lower it if the top of the frame is cut off. The measured figure is in the log."); PageWidth = c.Bind<int>("Terminal", "Page Width", 0, "How many characters wide to draw the box. 0 measures the screen.\nRaise it if the box does not reach the right-hand edge, lower it if lines wrap and the right border goes ragged - a wrapped line is the one thing that really breaks the drawing. Clamped to 44-64 either way. The terminal shrinks text that does not fit rather than clipping it, so this is measured at the largest size it will ever use and is deliberately an underestimate: the box may sit a character or two shy of the edge."); MarkParts = c.Bind<bool>("Warnings", "Mark Ingredients When Scanned", true, "Add a marker to the scan label of any item a recipe wants.\nThe scan is where you decide whether something is worth a slot, so it is the useful place to find out that a $12 bolt is two thirds of a railgun. Only items wanted by a recipe whose product this profile actually has installed are marked."); PartMark = c.Bind<string>("Warnings", "Scan Marker", " [INGREDIENT]", "What to hang off the scan label. The scan box is narrow and the line already reads 'Value: $123', so shorten this if it crowds the value."); WarnAtDesk = c.Bind<bool>("Warnings", "Warn At Company Desk", true, "Say what an item is for when it goes on the Company counter.\nIt never blocks the sale, and it never touches what the Company pays. Only the player who placed the item sees it, and each item warns at most once every ten seconds, so emptying a bag is one banner rather than six."); VerboseLogs = c.Bind<bool>("Logging", "Verbose", false, "Log every recipe as it loads and every part a craft consumes.\nWorth turning on once after writing new recipes: an item name that no installed mod registered is always reported, but this also shows the names that did match, which is usually how you spot that you meant a different item."); } } internal static class Crafting { internal enum Refusal { None, NoSuchRecipe, UnknownProduct, MissingParts, Failed } internal sealed class Outcome { public int Collected; public int Total; public readonly List<ulong> Ids = new List<ulong>(); public readonly List<int> Values = new List<int>(); } internal const int NoValue = -1; internal static Refusal Execute(Recipe recipe, out Outcome outcome) { //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Unknown result type (might be due to invalid IL or missing references) outcome = new Outcome(); if (recipe == null) { return Refusal.NoSuchRecipe; } Item val = ShipParts.Resolve(recipe.Product); if ((Object)(object)val == (Object)null || (Object)(object)val.spawnPrefab == (Object)null) { Plugin.Log.LogWarning((object)("'" + recipe.Product + "' (from " + recipe.Source + ") is not an item any installed mod registered, so it cannot be crafted.")); return Refusal.UnknownProduct; } if ((Object)(object)val.spawnPrefab.GetComponentInChildren<GrabbableObject>() == (Object)null) { Plugin.Log.LogError((object)("'" + recipe.Product + "' (from " + recipe.Source + ") has no GrabbableObject on its prefab, so it cannot be crafted.")); return Refusal.UnknownProduct; } List<GrabbableObject> list = ShipParts.Gather(recipe); if (list == null) { return Refusal.MissingParts; } Vector3 origin = ShipParts.Middle(list); List<GrabbableObject> list2 = new List<GrabbableObject>(recipe.Yield); try { for (int i = 0; i < recipe.Yield; i++) { list2.Add(Spawn(val, Scatter(origin, i), recipe.Value)); } } catch (Exception ex) { Plugin.Log.LogError((object)("'" + recipe.Product + "' could not be spawned, so nothing was consumed: " + ex)); foreach (GrabbableObject item in list2) { Remove(item); } return Refusal.Failed; } foreach (GrabbableObject item2 in list) { outcome.Collected -= ShipParts.QuotaValueOf(item2); outcome.Total -= ShipParts.LevelValueOf(item2); try { Remove(item2); } catch (Exception ex2) { Plugin.Log.LogError((object)("A " + item2.itemProperties.itemName + " could not be removed after crafting " + recipe.Product + "; it is still in the ship: " + ex2)); } } foreach (GrabbableObject item3 in list2) { outcome.Collected += ShipParts.QuotaValueOf(item3); outcome.Total += ShipParts.LevelValueOf(item3); outcome.Ids.Add(((NetworkBehaviour)item3).NetworkObject.NetworkObjectId); outcome.Values.Add(val.isScrap ? item3.scrapValue : (-1)); } ShipParts.SyncQuota(outcome.Collected, outcome.Total); Plugin.Log.LogInfo((object)$"Crafted {recipe.Yield}x {recipe.Product} from {list.Count} part(s)."); return Refusal.None; } private static GrabbableObject Spawn(Item product, Vector3 where, int? value) { //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) GrabbableObject componentInChildren = Object.Instantiate<GameObject>(product.spawnPrefab, where, Quaternion.identity, StartOfRound.Instance.elevatorTransform).GetComponentInChildren<GrabbableObject>(); if ((Object)(object)componentInChildren == (Object)null) { throw new InvalidOperationException(product.itemName + " lost its GrabbableObject on instantiation."); } componentInChildren.isInElevator = true; componentInChildren.isInShipRoom = true; if (!product.canBeGrabbedBeforeGameStart) { product.canBeGrabbedBeforeGameStart = true; Plugin.Log.LogDebug((object)(product.itemName + " can now be picked up before the lever, because it can be crafted in orbit.")); } if (product.isScrap) { componentInChildren.SetScrapValue(value ?? Random.Range(product.minValue, product.maxValue + 1)); } ((NetworkBehaviour)componentInChildren).NetworkObject.Spawn(false); return componentInChildren; } private static void Remove(GrabbableObject item) { if (!((Object)(object)item == (Object)null)) { NetworkObject networkObject = ((NetworkBehaviour)item).NetworkObject; if ((Object)(object)networkObject != (Object)null && networkObject.IsSpawned) { networkObject.Despawn(true); } else { Object.Destroy((Object)(object)((Component)item).gameObject); } } } private static Vector3 Scatter(Vector3 origin, int index) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0020: 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_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) if (index == 0) { return origin; } float num = (float)index * 2.39996f; return origin + new Vector3(Mathf.Cos(num), 0f, Mathf.Sin(num)) * (0.25f * (float)index); } internal static Dictionary<string, int> Held(Recipe recipe) { Dictionary<string, int> dictionary = ShipParts.Count(); Dictionary<string, int> dictionary2 = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); foreach (Part part in recipe.Parts) { dictionary.TryGetValue(part.Name, out var value); dictionary2[part.Name] = value; } return dictionary2; } } internal static class CraftList { internal sealed class Entry { public Recipe Recipe; public string Head = string.Empty; public string Summary = string.Empty; public readonly List<string> Note = new List<string>(); public readonly List<string> Parts = new List<string>(); public int Rank; public int Short; public int Detail => Note.Count + Parts.Count; public bool Buildable => Rank == 0; public int Height => 1 + Detail; } private const int SummaryColumn = 24; internal const string Title = "CRAFTING"; internal static List<Entry> Entries(IDictionary<string, int> counts) { return (from recipe in RecipeBook.All select Render(recipe, counts) into e orderby e.Rank, e.Short select e).ThenBy<Entry, string>((Entry e) => e.Recipe.Product, StringComparer.OrdinalIgnoreCase).ToList(); } private static Entry Render(Recipe recipe, IDictionary<string, int> counts) { Entry entry = new Entry { Recipe = recipe, Head = ((recipe.Yield == 1) ? recipe.Product.ToUpperInvariant() : (recipe.Product.ToUpperInvariant() + " x" + recipe.Yield)) }; if ((Object)(object)ShipParts.Resolve(recipe.Product) == (Object)null) { entry.Rank = 2; entry.Parts.Add(" not installed in this profile"); return entry; } List<string> list = new List<string>(recipe.Parts.Count); foreach (Part part in recipe.Parts) { counts.TryGetValue(part.Name, out var value); if (value < part.Count) { entry.Short++; } list.Add(Dots(value, part.Count) + " " + part.Name); } entry.Summary = Dots(recipe.Parts.Count - entry.Short, recipe.Parts.Count); entry.Rank = ((entry.Short != 0) ? 1 : 0); entry.Parts.AddRange(Wrap(list)); entry.Note.AddRange(Sentence(recipe.Info)); return entry; } internal static List<string> Sentence(string text, string indent = " ") { List<string> list = new List<string>(); if (string.IsNullOrWhiteSpace(text)) { return list; } StringBuilder stringBuilder = new StringBuilder(indent); bool flag = true; string[] array = text.Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text2 in array) { if (!flag && stringBuilder.Length + 1 + text2.Length > Frame.Width) { list.Add(stringBuilder.ToString()); stringBuilder = new StringBuilder(indent).Append(text2); continue; } if (!flag) { stringBuilder.Append(' '); } stringBuilder.Append(text2); flag = false; } if (!flag) { list.Add(stringBuilder.ToString()); } return list; } internal static string Dots(int have, int need) { return new string('●', Math.Min(Math.Max(have, 0), need)) + new string('○', Math.Max(0, need - have)); } private static List<string> Wrap(IEnumerable<string> tokens) { List<string> list = new List<string>(); StringBuilder stringBuilder = new StringBuilder(" "); bool flag = true; foreach (string token in tokens) { if (!flag && stringBuilder.Length + " | ".Length + token.Length > Frame.Width) { list.Add(stringBuilder.ToString()); stringBuilder = new StringBuilder(" ").Append(token); continue; } if (!flag) { stringBuilder.Append(" | "); } stringBuilder.Append(token); flag = false; } if (stringBuilder.Length > " ".Length) { list.Add(stringBuilder.ToString()); } return list; } internal static List<List<Entry>> Paginate(IReadOnlyList<Entry> entries, int budget) { budget = Math.Max(4, budget); List<List<Entry>> list = new List<List<Entry>>(); List<Entry> list2 = new List<Entry>(); int num = 0; foreach (Entry entry in entries) { if (list2.Count > 0 && num + entry.Height > budget) { list.Add(list2); list2 = new List<Entry>(); num = 0; } list2.Add(entry); num += entry.Height; } if (list2.Count > 0) { list.Add(list2); } return list; } internal static string NoRecipes(string foot) { return Frame.Draw("CRAFTING", string.Empty, new List<string> { "No recipes are installed.", string.Empty, "Write your own in", "BepInEx/config/CraftingLibrary/recipes.txt", "or install a mod that uses this library." }, foot); } internal static List<string> Rows(IReadOnlyList<Entry> page, int selected, int detailLines) { List<string> list = new List<string>(); int num = 0; for (int i = 0; i < page.Count; i++) { Entry entry = page[i]; string text = Pad(entry.Head) + entry.Summary; list.Add((i == selected) ? ("> " + (entry.Buildable ? "<mark=#00FF0066><color=#FFFFFFFF>" : "<mark=#66666633><color=#FFFFFFFF>") + text + "</color></mark>") : (" " + text)); if (selected < 0) { list.AddRange(entry.Note); list.AddRange(entry.Parts); } else if (i == selected) { num = Math.Min(entry.Parts.Count, detailLines); for (int j = 0; j < num; j++) { list.Add(entry.Parts[j]); } } } if (selected >= 0) { for (int k = num; k < detailLines; k++) { list.Add(string.Empty); } } return list; } internal static int DetailLines(IReadOnlyList<Entry> entries) { int val = 1; foreach (Entry entry in entries) { val = Math.Max(val, entry.Parts.Count); } return Math.Min(val, 5); } internal static string Facts(Item item, Recipe recipe) { if ((Object)(object)item == (Object)null) { return string.Empty; } List<string> list = new List<string>(); list.Add(Mathf.RoundToInt(Mathf.Clamp(item.weight - 1f, 0f, 100f) * 105f) + " lb"); if (item.twoHanded) { list.Add("two-handed"); } if (item.isConductiveMetal) { list.Add("conductive"); } if (item.requiresBattery) { list.Add("battery"); } if (recipe.Value == 0) { list.Add("worth nothing"); } else if (recipe.Value.HasValue) { list.Add("worth $" + recipe.Value.Value); } else if (item.isScrap) { list.Add("worth $" + item.minValue + "-" + item.maxValue); } return string.Join(", ", list); } private static string Pad(string head) { if (head.Length < 24) { return head.PadRight(24); } return head + " "; } } [HarmonyPatch] internal static class CraftNet { [CompilerGenerated] private static class <>O { public static HandleNamedMessageDelegate <0>__OnCraftServer; public static HandleNamedMessageDelegate <1>__OnRefusedClient; public static HandleNamedMessageDelegate <2>__OnBuiltClient; } private const string MsgCraft = "Maco.CraftingLibrary.Craft"; private const string MsgRefused = "Maco.CraftingLibrary.Refused"; private const string MsgBuilt = "Maco.CraftingLibrary.Built"; private const float ArrivalTimeout = 15f; internal static bool IsHost { get { if (!((Object)(object)NetworkManager.Singleton == (Object)null)) { return NetworkManager.Singleton.IsServer; } return true; } } [HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")] [HarmonyPostfix] private static void Init() { //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Expected O, but got Unknown //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Expected O, but got Unknown try { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || singleton.CustomMessagingManager == null) { return; } CustomMessagingManager customMessagingManager = singleton.CustomMessagingManager; if (singleton.IsServer) { object obj = <>O.<0>__OnCraftServer; if (obj == null) { HandleNamedMessageDelegate val = OnCraftServer; <>O.<0>__OnCraftServer = val; obj = (object)val; } customMessagingManager.RegisterNamedMessageHandler("Maco.CraftingLibrary.Craft", (HandleNamedMessageDelegate)obj); return; } object obj2 = <>O.<1>__OnRefusedClient; if (obj2 == null) { HandleNamedMessageDelegate val2 = OnRefusedClient; <>O.<1>__OnRefusedClient = val2; obj2 = (object)val2; } customMessagingManager.RegisterNamedMessageHandler("Maco.CraftingLibrary.Refused", (HandleNamedMessageDelegate)obj2); object obj3 = <>O.<2>__OnBuiltClient; if (obj3 == null) { HandleNamedMessageDelegate val3 = OnBuiltClient; <>O.<2>__OnBuiltClient = val3; obj3 = (object)val3; } customMessagingManager.RegisterNamedMessageHandler("Maco.CraftingLibrary.Built", (HandleNamedMessageDelegate)obj3); } catch (Exception ex) { Plugin.Log.LogError((object)("Could not set up crafting networking; crafting will be host-only this session: " + ex)); } } internal unsafe static Crafting.Refusal Request(Recipe recipe) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) if (recipe == null) { return Crafting.Refusal.NoSuchRecipe; } try { if (IsHost) { Crafting.Refusal refusal = Perform(recipe, 0uL); if (refusal != Crafting.Refusal.None) { Announce(refusal, recipe.Product); } return refusal; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(FastBufferWriter.GetWriteSize(recipe.Product, false), (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe(recipe.Product, false); NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage("Maco.CraftingLibrary.Craft", 0uL, val, (NetworkDelivery)3); return Crafting.Refusal.None; } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { Plugin.Log.LogError((object)("Could not send the craft request: " + ex)); Announce(Crafting.Refusal.Failed, recipe.Product); return Crafting.Refusal.Failed; } } private static Crafting.Refusal Perform(Recipe recipe, ulong requester) { Crafting.Outcome outcome; Crafting.Refusal num = Crafting.Execute(recipe, out outcome); if (num == Crafting.Refusal.None) { Broadcast(requester, recipe.Product, outcome); } return num; } private static void OnCraftServer(ulong senderClientId, FastBufferReader reader) { try { string text = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false); Recipe recipe = RecipeBook.Find(text); Crafting.Refusal refusal = ((recipe == null) ? Crafting.Refusal.NoSuchRecipe : Perform(recipe, senderClientId)); if (refusal != Crafting.Refusal.None) { Refuse(senderClientId, refusal, text); } } catch (Exception ex) { Plugin.Log.LogError((object)("A craft request could not be read: " + ex)); } } private unsafe static void Broadcast(ulong requester, string product, Crafting.Outcome outcome) { //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: 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_010f: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) try { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || singleton.CustomMessagingManager == null) { return; } List<ulong> list = new List<ulong>(); foreach (ulong connectedClientsId in singleton.ConnectedClientsIds) { if (connectedClientsId != 0L) { list.Add(connectedClientsId); } } if (list.Count == 0) { return; } int count = outcome.Ids.Count; int num = 8 + FastBufferWriter.GetWriteSize(product, false) + 12 + count * 12; FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(num, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref requester, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(product, false); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref outcome.Collected, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref outcome.Total, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref count, default(ForPrimitives)); for (int i = 0; i < count; i++) { ulong num2 = outcome.Ids[i]; ((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref num2, default(ForPrimitives)); int num3 = outcome.Values[i]; ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num3, default(ForPrimitives)); } singleton.CustomMessagingManager.SendNamedMessage("Maco.CraftingLibrary.Built", (IReadOnlyList<ulong>)list, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not tell the clients about a craft: " + ex)); } } private static void OnBuiltClient(ulong _, FastBufferReader reader) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003c: 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_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) try { ulong num = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref num, default(ForPrimitives)); string text = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false); int collected = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref collected, default(ForPrimitives)); int total = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref total, default(ForPrimitives)); int num2 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num2, default(ForPrimitives)); ShipParts.SyncQuota(collected, total); ulong id = default(ulong); int value = default(int); for (int i = 0; i < num2; i++) { ((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref id, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref value, default(ForPrimitives)); if ((Object)(object)Plugin.Instance != (Object)null) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(Adopt(id, value)); } } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton != (Object)null && num == singleton.LocalClientId) { CraftScreen.Notify("BUILT " + text.ToUpperInvariant()); } } catch (Exception ex) { Plugin.Log.LogError((object)("Could not read what the host built: " + ex)); } } private static IEnumerator Adopt(ulong id, int value) { float deadline = Time.realtimeSinceStartup + 15f; while (Time.realtimeSinceStartup < deadline) { if (TryAdopt(id, value)) { yield break; } yield return null; } Plugin.Log.LogWarning((object)($"A crafted item (network id {id}) never arrived here, so its " + "value and ship flags stay as the prefab had them.")); } private static bool TryAdopt(ulong id, int value) { try { NetworkManager singleton = NetworkManager.Singleton; Dictionary<ulong, NetworkObject> dictionary = ((singleton == null) ? null : singleton.SpawnManager?.SpawnedObjects); if (dictionary == null || !dictionary.TryGetValue(id, out var value2) || (Object)(object)value2 == (Object)null) { return false; } GrabbableObject componentInChildren = ((Component)value2).GetComponentInChildren<GrabbableObject>(); if ((Object)(object)componentInChildren == (Object)null) { return false; } componentInChildren.isInElevator = true; componentInChildren.isInShipRoom = true; if (value != -1) { componentInChildren.SetScrapValue(value); } return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not adopt a crafted item: " + ex.Message)); return true; } } private unsafe static void Refuse(ulong clientId, Crafting.Refusal refusal, string product) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) try { FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(1 + FastBufferWriter.GetWriteSize(product, false), (Allocator)2, -1); try { byte b = (byte)refusal; ((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(product, false); NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage("Maco.CraftingLibrary.Refused", clientId, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { Plugin.Log.LogError((object)("Could not tell the client its craft was refused: " + ex)); } } private static void OnRefusedClient(ulong _, FastBufferReader reader) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) try { byte refusal = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref refusal, default(ForPrimitives)); string product = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref product, false); Announce((Crafting.Refusal)refusal, product); CraftScreen.Notify("NOTHING BUILT"); } catch (Exception ex) { Plugin.Log.LogError((object)("Could not read a craft refusal: " + ex)); } } private static void Announce(Crafting.Refusal refusal, string product) { string text = Reason(refusal, product); try { HUDManager instance = HUDManager.Instance; if (instance != null) { instance.DisplayTip("Nothing was built", text, true, false, "LC_Tip1"); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Craft refused (" + text + ") but the tip could not be shown: " + ex.Message)); } } internal static string Reason(Crafting.Refusal refusal, string product) { return refusal switch { Crafting.Refusal.MissingParts => "Somebody got to the parts first. Nothing was consumed.", Crafting.Refusal.UnknownProduct => product + " is not an item the host has installed.", Crafting.Refusal.NoSuchRecipe => "The host has no recipe for " + product + ".", _ => "The assembly failed. Check the host's log.", }; } } [HarmonyPatch] internal static class CraftScreen { private enum Mode { List, Ask } internal const string NodeName = "CraftingLibraryScreen"; private const float FirstRepeat = 0.4f; private const float NextRepeat = 0.09f; private const float RefreshEvery = 1f; private const float StatusFor = 4f; private const int Label = 9; private static readonly string Indent = new string(' ', 10); private static Terminal _terminal; private static TerminalNode _node; private static bool _open; private static bool _tookOver; private static bool _armed; private static Mode _mode; private static int _cursor; private static int _choice; private static Color _caret; private static bool _caretSaved; private static string _status; private static float _statusUntil; private static int _rows; private static readonly List<int> _pageStarts = new List<int>(); private static CraftList.Entry _selected; private static float _up; private static float _down; private static float _left; private static float _right; private static float _refreshAt; internal static TerminalNode Open(Terminal terminal, int page) { _terminal = terminal; Frame.Remember(terminal); _open = true; _tookOver = false; _armed = false; _status = null; _cursor = 0; _mode = Mode.List; _choice = 0; _up = (_down = (_left = (_right = 0f))); string text = Render(); if (page > 1 && _pageStarts.Count > 1) { _cursor = _pageStarts[Mathf.Clamp(page - 1, 0, _pageStarts.Count - 1)]; text = Render(); } return Node(text); } internal static void Close() { //IL_0055: Unknown result type (might be due to invalid IL or missing references) Terminal terminal = _terminal; _open = false; _tookOver = false; _armed = false; _status = null; _selected = null; _mode = Mode.List; _choice = 0; TMP_InputField val = terminal?.screenText; if ((Object)(object)val != (Object)null) { ((Selectable)val).interactable = true; if (_caretSaved) { val.caretColor = _caret; } if (terminal.terminalInUse) { val.ActivateInputField(); ((Selectable)val).Select(); } } _caretSaved = false; _terminal = null; } private static void Hold() { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) TMP_InputField val = _terminal?.screenText; if (!((Object)(object)val == (Object)null)) { if (!_caretSaved) { _caret = val.caretColor; _caretSaved = true; } val.caretColor = new Color(0f, 0f, 0f, 0f); if (val.isFocused) { val.DeactivateInputField(false); } ((Selectable)val).interactable = false; _tookOver = true; } } [HarmonyPatch(typeof(Terminal), "Update")] [HarmonyPostfix] private static void Tick(Terminal __instance) { if (!_open) { return; } try { if ((Object)(object)_terminal == (Object)null) { _terminal = __instance; } if (!__instance.terminalInUse) { Close(); return; } Hold(); Keyboard current = Keyboard.current; if (current == null) { return; } if (!_armed) { if (!((ButtonControl)current.enterKey).isPressed && !((ButtonControl)current.numpadEnterKey).isPressed) { _armed = true; } return; } float unscaledDeltaTime = Time.unscaledDeltaTime; bool flag = Step(((ButtonControl)current.wKey).isPressed || ((ButtonControl)current.upArrowKey).isPressed, ref _up, unscaledDeltaTime); bool flag2 = Step(((ButtonControl)current.sKey).isPressed || ((ButtonControl)current.downArrowKey).isPressed, ref _down, unscaledDeltaTime); bool flag3 = Step(((ButtonControl)current.aKey).isPressed || ((ButtonControl)current.leftArrowKey).isPressed, ref _left, unscaledDeltaTime); bool flag4 = Step(((ButtonControl)current.dKey).isPressed || ((ButtonControl)current.rightArrowKey).isPressed, ref _right, unscaledDeltaTime); if (((ButtonControl)current.backspaceKey).wasPressedThisFrame) { Back(); return; } bool flag5 = ((ButtonControl)current.enterKey).wasPressedThisFrame || ((ButtonControl)current.numpadEnterKey).wasPressedThisFrame || (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame); if (_mode == Mode.Ask) { if (flag) { Pick(-1); } if (flag2) { Pick(1); } if (flag5) { Answer(); return; } } else { if (flag) { Move(-1); } if (flag2) { Move(1); } if (flag3) { Turn(-1); } if (flag4) { Turn(1); } if (flag5) { Chose(); return; } } if (Time.realtimeSinceStartup >= _refreshAt) { Draw(); } } catch (Exception ex) { Plugin.Log.LogError((object)("The craft screen failed and has been closed: " + ex)); try { Close(); } catch { } } } private static bool Step(bool down, ref float timer, float dt) { if (!down) { timer = 0f; return false; } if (timer == 0f) { timer = 0.4f; return true; } timer -= dt; if (timer > 0f) { return false; } timer = 0.09f; return true; } private static void Move(int by) { if (_rows > 0) { _cursor = ((_cursor + by) % _rows + _rows) % _rows; Draw(); } } private static void Pick(int by) { int num = ((_selected == null || !_selected.Buildable) ? 1 : 2); _choice = ((_choice + by) % num + num) % num; Draw(); } private static void Turn(int by) { if (_pageStarts.Count > 1) { int num = PageOf(_cursor); num = ((num + by) % _pageStarts.Count + _pageStarts.Count) % _pageStarts.Count; _cursor = _pageStarts[num]; Draw(); } } private static void Back() { if (_mode != Mode.Ask) { Close(); return; } _mode = Mode.List; _choice = 0; Draw(); } private static void Chose() { if (_selected != null) { _mode = Mode.Ask; _choice = 0; Draw(); } } private static void Answer() { if (_selected != null && _selected.Buildable && _choice == 0) { Say((CraftNet.Request(_selected.Recipe) != Crafting.Refusal.None) ? "NOTHING BUILT" : (CraftNet.IsHost ? ("BUILT " + _selected.Head) : ("BUILDING " + _selected.Head))); } _mode = Mode.List; _choice = 0; Draw(); } private static void Say(string status) { _status = status; _statusUntil = Time.realtimeSinceStartup + 4f; } internal static void Notify(string status) { if (_open && !((Object)(object)_terminal == (Object)null)) { Say(status); Draw(); } } private static void Draw() { if (!((Object)(object)_terminal == (Object)null)) { _terminal.LoadNewNode(Node(Render())); if (_tookOver) { Hold(); } } } private static string Render() { _refreshAt = Time.realtimeSinceStartup + 1f; Dictionary<string, int> counts = ShipParts.Count(); string result = RenderList(counts); if (_mode == Mode.Ask && _selected != null) { return RenderAsk(_selected, counts); } _mode = Mode.List; return result; } private static string RenderList(IDictionary<string, int> counts) { List<CraftList.Entry> list = CraftList.Entries(counts); _rows = list.Count; _pageStarts.Clear(); _selected = null; if (list.Count == 0) { _pageStarts.Add(0); return CraftList.NoRecipes("BKSP exit"); } _cursor = Mathf.Clamp(_cursor, 0, list.Count - 1); int num = CraftList.DetailLines(list); int num2 = Math.Max(3, Frame.Budget(_terminal, 5) - num); int num3 = (list.Count + num2 - 1) / num2; for (int i = 0; i < num3; i++) { _pageStarts.Add(i * num2); } int num4 = _cursor / num2; int num5 = _pageStarts[num4]; int num6 = _cursor - num5; List<CraftList.Entry> range = list.GetRange(num5, Math.Min(num2, list.Count - num5)); _selected = range[num6]; string corner = ((num3 > 1) ? (num4 + 1 + "/" + num3) : string.Empty); string foot = ((num3 > 1) ? "W/S move ENTER build A/D page BKSP exit" : "W/S move ENTER build BKSP exit"); return Frame.Draw(Title(), corner, CraftList.Rows(range, num6, num), foot); } private static string RenderAsk(CraftList.Entry entry, IDictionary<string, int> counts) { Recipe recipe = entry.Recipe; Item val = ShipParts.Resolve(recipe.Product); List<string> list = new List<string>(); list.AddRange(CraftList.Sentence(recipe.Info, " ")); if (recipe.Info.Length > 0) { list.Add(string.Empty); } string label = ((recipe.Yield == 1) ? "NEEDS" : ("NEEDS x" + recipe.Yield)); foreach (Part part in recipe.Parts) { counts.TryGetValue(part.Name, out var value); Put(list, label, CraftList.Dots(value, part.Count) + " " + part.Name); label = null; } list.Add(string.Empty); Put(list, "ITEM", CraftList.Facts(val, recipe)); if (val?.toolTips != null) { label = "USE"; foreach (string item in val.toolTips.Where((string t) => !string.IsNullOrWhiteSpace(t)).Take(2)) { Put(list, label, item.Trim()); label = null; } } list.Add(string.Empty); if (entry.Rank == 2) { list.Add(" No installed mod registered this item."); list.Add(Choice("Back", 0)); } else if (!entry.Buildable) { list.Add(" Something on that list is not in the ship."); list.Add(Choice("Back", 0)); } else { list.Add(Choice("Build", 0)); list.Add(Choice("Abort", 1)); } return Frame.Draw(entry.Head, string.Empty, list, "W/S move ENTER choose BKSP back"); } private static void Put(List<string> body, string label, string text) { List<string> list = CraftList.Sentence(text, Indent); if (list.Count != 0) { list[0] = " " + (label ?? string.Empty).PadRight(9) + list[0].Substring(Indent.Length); body.AddRange(list); } } private static string Choice(string label, int index) { if (_choice != index) { return " " + label; } return "> <mark=#00FF0066><color=#FFFFFFFF>" + label + "</color></mark>"; } private static int PageOf(int row) { for (int num = _pageStarts.Count - 1; num >= 0; num--) { if (row >= _pageStarts[num]) { return num; } } return 0; } private static string Title() { if (_status == null || !(Time.realtimeSinceStartup < _statusUntil)) { return "CRAFTING"; } return _status; } private static TerminalNode Node(string text) { if ((Object)(object)_node == (Object)null) { _node = ScriptableObject.CreateInstance<TerminalNode>(); ((Object)_node).name = "CraftingLibraryScreen"; _node.clearPreviousText = true; _node.maxCharactersToType = 40; } _node.displayText = text; return _node; } [HarmonyPatch(typeof(Terminal), "QuitTerminal")] [HarmonyPostfix] private static void OnQuit(Terminal __instance) { if (_open) { _terminal = _terminal ?? __instance; Close(); } } [HarmonyPatch(typeof(Terminal), "BeginUsingTerminal")] [HarmonyPostfix] private static void OnBegin(Terminal __instance) { if (_open) { _terminal = __instance; Close(); } } } [HarmonyPatch] internal static class CraftTerminal { private const int NameColumn = 22; private const int TypedChrome = 9; internal const string NodeName = "CraftingLibraryNode"; private static TerminalNode _node; [HarmonyPatch(typeof(Terminal), "ParsePlayerSentence")] [HarmonyPrefix] private static bool Intercept(Terminal __instance, ref TerminalNode __result) { try { string text = Typed(__instance); if (text == null) { return true; } Frame.Remember(__instance); string text2 = __instance.RemovePunctuation(Cfg.Verb.Value).Trim(); if (text2.Length == 0) { text2 = "craft"; } if (!text.StartsWith(text2, StringComparison.Ordinal)) { return true; } string text3 = text.Substring(text2.Length); if (text3.Length > 0 && text3[0] != ' ') { return true; } __instance.broadcastedCodeThisFrame = false; __result = Answer(__instance, text3.Trim()); return false; } catch (Exception ex) { Plugin.Log.LogError((object)("The craft command failed; falling back to the normal parser: " + ex)); return true; } } private static string Typed(Terminal terminal) { TMP_InputField val = terminal?.screenText; if ((Object)(object)val == (Object)null || val.text == null) { return null; } int textAdded = terminal.textAdded; if (textAdded <= 0 || textAdded > val.text.Length) { return null; } return terminal.RemovePunctuation(val.text.Substring(val.text.Length - textAdded)).Trim(); } private static TerminalNode Answer(Terminal terminal, string argument) { int result = 1; if (argument.Length > 0 && !int.TryParse(argument, out result)) { return Build(argument); } if (!Cfg.Interactive.Value) { return TypedList(terminal, result); } return CraftScreen.Open(terminal, result); } private static TerminalNode TypedList(Terminal terminal, int wanted) { List<CraftList.Entry> list = CraftList.Entries(ShipParts.Count()); if (list.Count == 0) { return Node(CraftList.NoRecipes(string.Empty)); } List<List<CraftList.Entry>> list2 = CraftList.Paginate(list, Frame.Budget(terminal, 9)); int num = Mathf.Clamp(wanted, 1, list2.Count); int num2 = list.Count((CraftList.Entry e) => e.Rank == 0); int num3 = list.Count((CraftList.Entry e) => e.Rank == 1); List<string> list3 = new List<string> { num2 + " ready, " + num3 + " short", string.Empty }; list3.AddRange(CraftList.Rows(list2[num - 1], -1, 0)); list3.Add(string.Empty); string text = Cfg.Verb.Value.ToUpperInvariant(); int num4 = ((num >= list2.Count) ? 1 : (num + 1)); list3.Add((list2.Count > 1) ? (text + " " + num4 + " for more - " + text + " [name] to build") : ("Type " + text + " [name] to build one.")); string corner = ((list2.Count > 1) ? (num + "/" + list2.Count) : string.Empty); return Node(Frame.Draw("CRAFTING", corner, list3, string.Empty)); } private static TerminalNode Build(string argument) { Recipe recipe = RecipeBook.Find(argument); if (recipe == null) { return NoSuchRecipe(argument); } string text = recipe.Product.ToUpperInvariant(); List<string> list = new List<string>(); if ((Object)(object)ShipParts.Resolve(recipe.Product) == (Object)null) { list.Add(text + " is not an item this"); list.Add("profile has installed, so it cannot be built."); list.Add(string.Empty); list.Add("The recipe came from " + recipe.Source + "."); return Node(Frame.Draw("CRAFTING", string.Empty, list, string.Empty)); } Dictionary<string, int> held = Crafting.Held(recipe); int value2; List<Part> list2 = recipe.Parts.Where((Part p) => !held.TryGetValue(p.Name, out value2) || value2 < p.Count).ToList(); if (list2.Count > 0) { list.Add("Still needed:"); foreach (Part item in list2) { held.TryGetValue(item.Name, out var value); list.Add(" " + Pad(item.Name) + (item.Count - value) + " more"); } list.Add(string.Empty); list.Add("Ingredients must be loose in the ship:"); list.Add("not held, not in a bag."); return Node(Frame.Draw("CANNOT BUILD " + text, string.Empty, list, string.Empty)); } Crafting.Refusal refusal = CraftNet.Request(recipe); if (refusal != Crafting.Refusal.None) { list.AddRange(CraftList.Sentence(CraftNet.Reason(refusal, recipe.Product), string.Empty)); return Node(Frame.Draw("NOTHING BUILT", string.Empty, list, string.Empty)); } foreach (Part part in recipe.Parts) { list.Add(" consumed " + part); } list.Add(string.Empty); list.AddRange(CraftList.Sentence(CraftNet.IsHost ? "Assembled where the ingredients were lying." : "The host is assembling it where the ingredients lie. If somebody grabbed one first, a message will say so and nothing is consumed.", string.Empty)); return Node(Frame.Draw((CraftNet.IsHost ? "BUILT " : "BUILDING ") + text, string.Empty, list, string.Empty)); } private static TerminalNode NoSuchRecipe(string argument) { List<string> list = new List<string> { "No recipe for '" + argument + "'." }; IReadOnlyList<Recipe> readOnlyList = RecipeBook.Matching(argument); if (readOnlyList.Count > 0) { list.Add(string.Empty); list.Add("Did you mean:"); foreach (Recipe item in readOnlyList.Take(6)) { list.Add(" " + item.Product.ToUpperInvariant()); } } list.Add(string.Empty); list.Add("Type " + Cfg.Verb.Value.ToUpperInvariant() + " on its own for the list."); return Node(Frame.Draw("CRAFTING", string.Empty, list, string.Empty)); } private static string Pad(string name) { if (name.Length < 22) { return name.PadRight(22); } return name + " "; } private static TerminalNode Node(string text) { if ((Object)(object)_node == (Object)null) { _node = ScriptableObject.CreateInstance<TerminalNode>(); ((Object)_node).name = "CraftingLibraryNode"; _node.clearPreviousText = true; _node.maxCharactersToType = 40; } _node.displayText = text; return _node; } [HarmonyPatch(typeof(Terminal), "LoadNewNode")] [HarmonyPostfix] private static void Reclaim(Terminal __instance, TerminalNode node) { try { if ((Object)(object)node == (Object)null || __instance == null) { return; } TMP_InputField screenText = __instance.screenText; if (((screenText != null) ? screenText.text : null) != null && (!(((Object)node).name != "CraftingLibraryNode") || !(((Object)node).name != "CraftingLibraryScreen"))) { string text = __instance.screenText.text.TrimStart('\n', '\r'); if (text.Length != __instance.screenText.text.Length) { __instance.modifyingText = true; __instance.screenText.text = text; __instance.currentText = text; __instance.textAdded = 0; } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not trim the page header: " + ex.Message)); } } } internal static class Frame { private const int FallbackWidth = 51; private const int FallbackHeight = 16; private const int MinWidth = 44; private const int MaxWidth = 64; internal const int Chrome = 5; internal const string Ready = "<mark=#00FF0066><color=#FFFFFFFF>"; internal const string NotReady = "<mark=#66666633><color=#FFFFFFFF>"; internal const string End = "</color></mark>"; private static Terminal _terminal; private static int _width = 51; private static int _height = 16; private static bool _measured; private static bool _gaveUp; internal static int BoxWidth { get { Measure(); return _width; } } internal static int Width => BoxWidth - 4; internal static string Draw(string title, string corner, IReadOnlyList<string> body, string foot) { int boxWidth = BoxWidth; title = title ?? string.Empty; if (title.Length > boxWidth - 6) { title = title.Substring(0, boxWidth - 6); } int num = boxWidth - 6 - title.Length; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(' ').Append('╔').Append('═', title.Length + 2) .Append('╗') .Append('\n'); stringBuilder.Append('╭').Append('╢').Append(' ') .Append(title) .Append(' ') .Append('╟') .Append(Rule(num, corner, '─')) .Append('╮') .Append('\n'); stringBuilder.Append('│').Append('╚').Append('═', title.Length + 2) .Append('╝') .Append(' ', num) .Append('│') .Append('\n'); if (body != null) { foreach (string item in body) { stringBuilder.Append('│').Append(' ').Append(Fit(item ?? string.Empty)) .Append(' ') .Append('│') .Append('\n'); } } stringBuilder.Append('╰').Append(Rule(boxWidth - 2, foot, '─', left: true)).Append('╯') .Append('\n'); return stringBuilder.ToString(); } private static string Rule(int length, string note, char dash, bool left = false) { if (length <= 0) { return string.Empty; } if (string.IsNullOrEmpty(note) || note.Length + 4 > length) { return new string(dash, length); } int count = length - note.Length - 3; if (!left) { return new string(dash, count) + " " + note + " " + dash; } return dash + " " + note + " " + new string(dash, count); } internal static string Fit(string line) { int width = Width; int num = Visible(line); if (num < width) { return line + new string(' ', width - num); } if (num != width) { return Clip(line, width); } return line; } private static string Clip(string line, int width) { StringBuilder stringBuilder = new StringBuilder(line.Length); int num = 0; bool flag = false; foreach (char c in line) { if (!flag && c == '<') { flag = true; stringBuilder.Append(c); } else if (flag) { if (c == '>') { flag = false; } stringBuilder.Append(c); } else if (num < width) { stringBuilder.Append(c); num++; } } return stringBuilder.ToString(); } internal static int Visible(string text) { int num = 0; bool flag = false; foreach (char c in text) { if (!flag && c == '<') { flag = true; } else if (flag) { if (c == '>') { flag = false; } } else { num++; } } return num; } internal static int Budget(Terminal terminal, int chrome) { Remember(terminal); if (Cfg.PageLines.Value > 0) { return Cfg.PageLines.Value; } Measure(); return Mathf.Clamp(_height - chrome, 4, 60); } internal static void Remember(Terminal terminal) { if ((Object)(object)terminal != (Object)null) { _terminal = terminal; } } internal static void Forget() { _measured = false; _gaveUp = false; _width = 51; _height = 16; _terminal = null; } private static void Measure() { //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) if (_measured || _gaveUp) { return; } _width = ((Cfg.PageWidth.Value > 0) ? Mathf.Clamp(Cfg.PageWidth.Value, 44, 64) : 51); _height = 16; try { TMP_InputField obj = _terminal?.screenText; TMP_Text val = ((obj != null) ? obj.textComponent : null); RectTransform val2 = ((obj != null) ? obj.textViewport : null); TMP_FontAsset val3 = ((val != null) ? val.font : null); if ((Object)(object)val == (Object)null || (Object)(object)val2 == (Object)null || (Object)(object)val3 == (Object)null) { return; } FaceInfo faceInfo = val3.faceInfo; float num = ((FaceInfo)(ref faceInfo)).pointSize; float num2 = (val.enableAutoSizing ? val.fontSizeMax : val.fontSize); if (!(num <= 0.01f) && !(num2 <= 0.01f)) { float num3 = num2 / num * ((((FaceInfo)(ref faceInfo)).scale <= 0.01f) ? 1f : ((FaceInfo)(ref faceInfo)).scale); float num4 = ((FaceInfo)(ref faceInfo)).lineHeight * num3 + val.lineSpacing; float num5 = Advance(val3) * num3 + val.characterSpacing; Vector4 margin = val.margin; Rect rect = val2.rect; float num6 = ((Rect)(ref rect)).height - margin.y - margin.w; rect = val2.rect; float num7 = ((Rect)(ref rect)).width - margin.x - margin.z; if (num4 > 0.01f && num6 > 1f) { _height = Mathf.Clamp(Mathf.FloorToInt(num6 / num4), 8, 80); } if (Cfg.PageWidth.Value <= 0 && num5 > 0.01f && num7 > 1f) { _width = Mathf.Clamp(Mathf.FloorToInt(num7 / num5) - 1, 44, 64); } _measured = true; Plugin.Log.LogInfo((object)($"Terminal measured at {_width} columns x {_height} lines " + $"({num7:0.#} x {num6:0.#}, {num5:0.##} per column, " + $"{num4:0.##} per line). Terminal / Page Width and Recipe Lines " + "Per Page override these if the box does not fit.")); } } catch (Exception ex) { _gaveUp = true; Plugin.Log.LogWarning((object)($"Could not measure the terminal; assuming {_width} columns x " + _height + " lines. Reported once per lobby: " + ex.Message)); } } private static float Advance(TMP_FontAsset font) { //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) Dictionary<uint, TMP_Character> characterLookupTable = font.characterLookupTable; if (characterLookupTable != null) { uint[] array = new uint[5] { 77u, 109u, 48u, 65u, 32u }; foreach (uint key in array) { if (!characterLookupTable.TryGetValue(key, out var value)) { continue; } float? obj; if (value == null) { obj = null; } else { Glyph glyph = ((TMP_TextElement)value).glyph; if (glyph == null) { obj = null; } else { GlyphMetrics metrics = glyph.metrics; obj = ((GlyphMetrics)(ref metrics)).horizontalAdvance; } } float? num = obj; float valueOrDefault = num.GetValueOrDefault(); if (valueOrDefault > 0.01f) { return valueOrDefault; } } } FaceInfo faceInfo = font.faceInfo; return (float)((FaceInfo)(ref faceInfo)).pointSize * 0.6f; } } [HarmonyPatch] internal static class MenuEntry { private static string Entry => ">" + Cfg.Verb.Value.Trim().ToUpperInvariant(); [HarmonyPatch(typeof(Terminal), "LoadNewNode")] [HarmonyPrefix] private static void Annotate(TerminalNode node) { try { if (Cfg.ShowInMenu.Value) { string text = node?.displayText; if (!string.IsNullOrEmpty(text) && text.IndexOf(Entry, StringComparison.OrdinalIgnoreCase) < 0 && text.IndexOf(">MOONS", StringComparison.OrdinalIgnoreCase) >= 0) { node.displayText = text.TrimEnd('\n', '\r') + "\n\n" + Entry + "\nTo build equipment out of scrap lying in the ship.\n\n"; Plugin.Log.LogDebug((object)("Added " + Entry + " to the terminal's front page.")); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not add " + Entry + " to the terminal's front page. The command still works, it is just not advertised: " + ex.Message)); } } } [HarmonyPatch] internal static class PartMarks { private static readonly Dictionary<string, bool> _isPart = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase); private static int _revision = -1; private static readonly Dictionary<string, float> _warned = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase); private const float WarnCooldown = 10f; private static bool _complained; internal static void Forget() { _isPart.Clear(); _warned.Clear(); } [HarmonyPatch(typeof(HUDManager), "AttemptScanNode")] [HarmonyPrefix] private static void MarkScannedItem(ScanNodeProperties node) { try { if (!Cfg.MarkParts.Value || (Object)(object)node == (Object)null) { return; } string text = ((Component)node).GetComponentInParent<GrabbableObject>()?.itemProperties?.itemName; if (string.IsNullOrEmpty(text)) { return; } string text2 = MarkFor(text); if (text2 != null) { string text3 = node.subText ?? string.Empty; if (!text3.EndsWith(text2, StringComparison.Ordinal)) { node.subText = ((text3.Length == 0) ? text2.TrimStart() : (text3 + text2)); } } } catch (Exception e) { Complain("Could not mark a scanned item", e); } } [HarmonyPatch(typeof(DepositItemsDesk), "PlaceItemOnCounter")] [HarmonyPrefix] private static void WarnAtCounter(PlayerControllerB playerWhoTriggered) { try { if (!Cfg.WarnAtDesk.Value) { return; } PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController; if ((Object)(object)val == (Object)null || (Object)(object)playerWhoTriggered != (Object)(object)val) { return; } string text = playerWhoTriggered.currentlyHeldObjectServer?.itemProperties?.itemName; if (string.IsNullOrEmpty(text)) { return; } List<Recipe> list = Wanted(text); if (list.Count != 0 && (!_warned.TryGetValue(text, out var value) || !(Time.realtimeSinceStartup - value < 10f))) { _warned[text] = Time.realtimeSinceStartup; HUDManager instance = HUDManager.Instance; if (instance != null) { instance.DisplayTip("Crafting ingredient", Explain(text, list), true, false, "LC_Tip1"); } } } catch (Exception e) { Complain("Could not check an item placed on the counter", e); } } private static string Explain(string name, IReadOnlyList<Recipe> wanted) { string text = wanted[0].Product.ToUpperInvariant(); if (wanted.Count == 1) { return name + " is used by " + text + "."; } return name + " is used by " + text + " and " + (wanted.Count - 1) + ((wanted.Count == 2) ? " other recipe." : " other recipes."); } private static string MarkFor(string itemName) { string value = Cfg.PartMark.Value; if (string.IsNullOrEmpty(value)) { return null; } if (_revision != RecipeBook.Revision) { _isPart.Clear(); _revision = RecipeBook.Revision; } if (!_isPart.TryGetValue(itemName, out var value2)) { value2 = Wanted(itemName).Count > 0; _isPart[itemName] = value2; } if (!value2) { return null; } return value; } private static List<Recipe> Wanted(string itemName) { return (from recipe in RecipeBook.Consumers(itemName) where (Object)(object)ShipParts.Resolve(recipe.Product) != (Object)null select recipe).ToList(); } private static void Complain(string what, Exception e) { if (!_complained) { _complained = true; Plugin.Log.LogWarning((object)(what + ". Crafting still works; this is only the warning label. Reported once per session: " + e)); } } } [BepInPlugin("Maco.CraftingLibrary", "CraftingLibrary", "1.0.0")] public sealed class Plugin : BaseUnityPlugin { [HarmonyPatch] private static class Loader { [HarmonyPatch(typeof(StartOfRound), "Start")] [HarmonyPostfix] private static void Load() { try { CraftScreen.Close(); Frame.Forget(); ShipParts.ForgetItems(); PartMarks.Forget(); RecipeBook.ForgetFileRecipes(); RecipeFiles.LoadAll(); Verify(); } catch (Exception ex) { Log.LogError((object)("Recipes could not be loaded; nothing will be craftable: " + ex)); } } private static void Verify() { int num = 0; foreach (Recipe item in RecipeBook.All) { List<string> list = (from name in new string[1] { item.Product }.Concat(item.Parts.Select((Part p) => p.Name)) where (Object)(object)ShipParts.Resolve(name) == (Object)null select name).ToList(); if (list.Count == 0) { if (Cfg.VerboseLogs.Value) { Log.LogInfo((object)("OK: " + item)); } continue; } num++; Log.LogWarning((object)("'" + item.Product + "' (from " + item.Source + ") names " + $"{list.Count} item(s) that no installed mod registered: " + string.Join(", ", list.Select((string m) => "'" + m + "'")) + ". Check the spelling against the item's in-game name.")); } int num2 = RecipeBook.All.Count - num; Log.LogInfo((object)($"{num2} recipe(s) usable" + ((num > 0) ? $", {num} unusable (see above)." : "."))); } } public const string Guid = "Maco.CraftingLibrary"; public const string Name = "CraftingLibrary"; public const string Version = "1.0.0"; internal static ManualLogSource Log; internal static Plugin Instance; private void Awake() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected O, but got Unknown //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Expected O, but got Unknown //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Expected O, but got Unknown //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Expected O, but got Unknown //IL_00a8: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Cfg.Bind(((BaseUnityPlugin)this).Config); RecipeFiles.WriteStarterFile(); Harmony val = new Harmony("Maco.CraftingLibrary"); Patch(val, typeof(CraftTerminal), "the terminal command"); Patch(val, typeof(CraftScreen), "the craft screen"); Patch(val, typeof(MenuEntry), "the terminal menu entry"); Patch(val, typeof(PartMarks), "the crafting-part warnings"); Patch(val, typeof(CraftNet), "crafting networking"); Patch(val, typeof(Loader), "recipe loading"); Log.LogInfo((object)"CraftingLibrary 1.0.0 loaded."); } private static void Patch(Harmony harmony, Type type, string what) { try { harmony.PatchAll(type); } catch (Exception ex) { Log.LogError((object)("Could not set up " + what + ": " + ex)); } } } public sealed class Part { public string Name { get; } public int Count { get; } public Part(string name, int count) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("A part needs an item name.", "name"); } Name = name.Trim(); Count = Math.Max(1, count); } public override string ToString() { if (Count != 1) { return Name + " x" + Count; } return Name; } } public sealed class Recipe { public string Product { get; } public IReadOnlyList<Part> Parts { get; } public int Yield { get; } public int? Value { get; } public string Source { get; } public string Info { get; } internal bool FromFile { get; set; } public Recipe(string product, IEnumerable<Part> parts, int yield = 1, int? value = null, string source = "code", string info = "") { if (string.IsNullOrWhiteSpace(product)) { throw new ArgumentException("A recipe needs a product.", "product"); } List<Part> list = (parts ?? throw new ArgumentNullException("parts")).ToList(); if (list.Count == 0) { throw new ArgumentException("A recipe needs at least one part.", "parts"); } Product = product.Trim(); Parts = list; Yield = Math.Max(1, yield); Value = value; Source = (string.IsNullOrWhiteSpace(source) ? "code" : source.Trim()); Info = (string.IsNullOrWhiteSpace(info) ? string.Empty : info.Trim()); } public string PartsLine() { return string.Join(", ", Parts.Select((Part p) => p.ToString())); } public override string ToString() { return ((Yield == 1) ? Product : (Product + " x" + Yield)) + " = " + PartsLine(); } } public static class RecipeBook { private static readonly List<Recipe> _recipes = new List<Recipe>(); private static readonly IReadOnlyList<Recipe> _readOnly = _recipes.AsReadOnly(); private static Dictionary<string, List<Recipe>> _byPart; public static IReadOnlyList<Recipe> All => _readOnly; public static int Revision { get; private set; } public static IReadOnlyList<Recipe> Consumers(string itemName) { if (string.IsNullOrWhiteSpace(itemName)) { return Array.Empty<Recipe>(); } if (!(_byPart ?? (_byPart = BuildPartIndex())).TryGetValue(Normalize(itemName), out var value)) { return Array.Empty<Recipe>(); } return value; } private static Dictionary<string, List<Recipe>> BuildPartIndex() { Dictionary<string, List<Recipe>> dictionary = new Dictionary<string, List<Recipe>>(StringComparer.Ordinal); foreach (Recipe recipe in _recipes) { foreach (Part part in recipe.Parts) { string text = Normalize(part.Name); if (text.Length != 0) { if (!dictionary.TryGetValue(text, out var value)) { value = (dictionary[text] = new List<Recipe>()); } if (!value.Contains(recipe)) { value.Add(recipe); } } } } return dictionary; } public static void Add(Recipe recipe) { if (recipe == null) { throw new ArgumentNullException("recipe"); } Changed(); int num = _recipes.FindIndex((Recipe r) => Same(r.Product, recipe.Product)); if (num >= 0) { Plugin.Log.LogInfo((object)("'" + recipe.Product + "' from " + recipe.Source + " replaces the recipe from " + _recipes[num].Source + ".")); _recipes[num] = recipe; } else { _recipes.Add(recipe); } } public static void Add(string product, params string[] parts) { if (parts == null || parts.Length == 0) { throw new ArgumentException("A recipe needs at least one part.", "parts"); } Add(new Recipe(product, parts.Select(ParsePart), 1, null, CallerName())); } public static bool Remove(string product) { int num = _recipes.FindIndex((Recipe r) => Same(r.Product, product)); if (num < 0) { return false; } _recipes.RemoveAt(num); Changed(); return true; } internal static void ForgetFileRecipes() { _recipes.RemoveAll((Recipe r) => r.FromFile); Changed(); } private static void Changed() { _byPart = null; Revision++; } public static Recipe Find(string typed) { if (string.IsNullOrWhiteSpace(typed)) { return null; } string wanted = Normalize(typed); if (wanted.Length == 0) { return null; } Recipe recipe = _recipes.FirstOrDefault((Recipe r) => Normalize(r.Product) == wanted); if (recipe != null) { return recipe; } List<Recipe> list = _recipes.Where((Recipe r) => Normalize(r.Product).StartsWith(wanted, StringComparison.Ordinal)).ToList(); if (list.Count != 1) { return null; } return list[0]; } public static IReadOnlyList<Recipe> Matching(string typed) { if (string.IsNullOrWhiteSpace(typed)) { return Array.Empty<Recipe>(); } string wanted = Normalize(typed); if (wanted.Length == 0) { return Array.Empty<Recipe>(); } return _recipes.Where((Recipe r) => Normalize(r.Product).IndexOf(wanted, StringComparison.Ordinal) >= 0).ToList(); } private static string Normalize(string name) { if (string.IsNullOrEmpty(name)) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(name.Length); bool flag = true; foreach (char c in name) { if (char.IsPunctuation(c)) { continue; } if (char.IsWhiteSpace(c)) { if (!flag) { stringBuilder.Append(' '); } flag = true; } else { stringBuilder.Append(char.ToLowerInvariant(c)); flag = false; } } return stringBuilder.ToString().TrimEnd(); } internal static Part ParsePart(string text) { if (string.IsNullOrWhiteSpace(text)) { throw new ArgumentException("A part needs an item name.", "text"); } string text2 = text.Trim(); int num = text2.LastIndexOf(' '); if (num > 0) { string text3 = text2.Substring(num + 1); if (text3.Length > 1 && (text3[0] == 'x' || text3[0] == 'X') && int.TryParse(text3.Substring(1), out var result)) { return new Part(text2.Substring(0, num), result); } } return new Part(text2, 1); } private static bool Same(string a, string b) { return string.Equals(a, b, StringComparison.OrdinalIgnoreCase); } private static string CallerName() { try { StackFrame[] array = new StackTrace(2, fNeedFileInfo: false).GetFrames(); Assembly assembly = typeof(RecipeBook).Assembly; if (array == null) { array = Array.Empty<StackFrame>(); } StackFrame[] array2 = array; for (int i = 0; i < array2.Length; i++) { Assembly assembly2 = array2[i].GetMethod()?.DeclaringType?.Assembly; if (assembly2 != null && assembly2 != assembly) { return assembly2.GetName().Name; } } } catch { } return "code"; } } internal static class RecipeFiles { private static readonly string[] Extensions = new string[2] { ".txt", ".recipes" }; private const string StarterFile = "recipes.txt"; private const string LegacyReadme = "HOW-TO-WRITE-RECIPES.txt"; private static bool _checkedStarter; private static readonly string[] Starter = new string[8] { "# Recipes for Maco's Crafting Library. Type CRAFT at the ship terminal.", "#", "# Product = Part, Part x2 | value=0 | info=What it does.", "#", "# Item names as the game spells them. value=0 makes it worth nothing (equipment),", "# leave it out for normal scrap value. info= is optional. 'Product x2 =' builds", "# two. Re-read every lobby. Last recipe naming a product wins. # disables a line.", "" }; internal static string Folder => Path.Combine(Paths.ConfigPath, "CraftingLibrary"); internal static void LoadAll() { string folder = Folder; try { Directory.CreateDirectory(folder); } catch (Exception ex) { Plugin.Log.LogError((object)("Could not open the recipe folder at " + folder + ": " + ex)); return; } WriteStarterFile(); string[] array = Directory.GetFiles(folder, "*", SearchOption.AllDirectories).Where(IsRecipeFile).OrderBy<string, string>((string f) => f, StringComparer.OrdinalIgnoreCase) .ToArray(); if (array.Length == 0) { Plugin.Log.LogInfo((object)("No recipe files in " + folder + "; nothing to craft yet.")); return; } int num = 0; string[] array2 = array; foreach (string path in array2) { num += Load(path); } Plugin.Log.LogInfo((object)$"Loaded {num} recipe(s) from {array.Length} file(s)."); } private static bool IsRecipeFile(string path) { if (Path.GetFileName(path).Equals("HOW-TO-WRITE-RECIPES.txt", StringComparison.OrdinalIgnoreCase)) { return false; } string[] extensions = Extensions; foreach (string value in extensions) { if (path.EndsWith(value, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static int Load(string path) { string fileName = Path.GetFileName(path); string[] array; try { array = File.ReadAllLines(path); } catch (Exception ex) { Plugin.Log.LogError((object)("Could not read " + fileName + ": " + ex)); return 0; } int num = 0; for (int i = 0; i < array.Length; i++) { string text = Strip(array[i]); if (text.Length == 0) { continue; } try { Recipe recipe = Parse(text, fileName); recipe.FromFile = true; RecipeBook.Add(recipe); num++; if (Cfg.VerboseLogs.Value) { Plugin.Log.LogInfo((object)(" " + fileName + ": " + recipe)); } } catch (Exception ex2) { Plugin.Log.LogError((object)($"{fileName} line {i + 1} is not a recipe and was skipped: " + ex2.Message + "\n " + text)); } } return num; } private static string Strip(string line) { if (line == null) { return string.Empty; } int num = line.IndexOf('#'); if (num >= 0) { line = line.Substring(0, num); } return line.Trim(); } private static Recipe Parse(string line, string source) { string options = string.Empty; int num = line.IndexOf('|'); if (num >= 0) { options = line.Substring(num + 1).Trim(); line = line.Substring(0, num).Trim(); } int num2 = line.IndexOf('='); if (num2 <= 0) { throw new FormatException("expected 'Product = Part, Part'."); } Part part = RecipeBook.ParsePart(line.Substring(0, num2)); List<Part> list = (from p in line.Substring(num2 + 1).Split(',') select p.Trim() into p where p.Length > 0 select p).Select(RecipeBook.ParsePart).ToList(); if (list.Count == 0) { throw new FormatException("no parts listed after the '='."); } ReadOptions(options, out var value, out var info); return new Recipe(part.Name, list, part.Count, value, source, info); } private static void ReadOptions(string options, out int? value, out string info) { value = null; info = string.Empty; if (options.Length == 0) { return; } string[] array = options.Split('|'); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0) { continue; } int num = text.IndexOf('='); if (num <= 0) { throw new FormatException("option '" + text + "' has no '='."); } string text2 = text.Substring(0, num).Trim(); string text3 = text.Substring(num + 1).Trim(); if (text2.Equals("info", StringComparison.OrdinalIgnoreCase)) { info = text3; continue; } if (!text2.Equals("value", StringComparison.OrdinalIgnoreCase)) { throw new FormatException("unknown option '" + text2 + "'; there is 'value' and 'info'."); } if (!int.TryParse(text3, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || result < 0) { throw new FormatException("value must be a whole number of credits, not '" + text3 + "'."); } value = result; } } internal static void WriteStarterFile() { if (_checkedStarter) { return; } _checkedStarter = true; try { string folder = Folder; Directory.CreateDirectory(folder); string path = Path.Combine(folder, "recipes.txt"); if (!File.Exists(path)) { File.WriteAllText(path, string.Join("\n", Starter) + "\n"); Plugin.Log.LogInfo((object)"Wrote recipes.txt - add your own recipes there."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not write recipes.txt: " + ex.Message)); } } } internal static class ShipParts { private static Dictionary<string, Item> _byName; internal static void ForgetItems() { _byName = null; } internal static Item Resolve(string name) { if (string.IsNullOrWhiteSpace(name)) { return null; } if (_byName == null) { List<Item> list = StartOfRound.Instance?.allItemsList?.itemsList; if (list == null) { return null; } _byName = new Dictionary<string, Item>(StringComparer.OrdinalIgnoreCase); foreach (Item item in list) { if (!((Object)(object)item == (Object)null) && !string.IsNullOrEmpty(item.itemName) && !_byName.ContainsKey(item.itemName)) { _byName[item.itemName] = item; } } } if (!_byName.TryGetValue(name.Trim(), out var value)) { return null; } return value; } internal static List<GrabbableObject> Available() { List<GrabbableObject> list = new List<GrabbableObject>(); Collider val = StartOfRound.Instance?.shipBounds; if ((Object)(object)val == (Object)null) { return list; } GrabbableObject[] array = Object.FindObjectsByType<GrabbableObject>((FindObjectsSortMode)0); if (array == null) { return list; } HashSet<GrabbableObject> hashSet = Bagged(array); GrabbableObject[] array2 = array; foreach (GrabbableObject val2 in array2) { if (!((Object)(object)val2 == (Object)null) && !val2.deactivated && !val2.isHeld && !val2.isPocketed && !val2.isHeldByEnemy && !val2.heldByPlayerOnServer && !((Object)(object)val2.parentObject != (Object)null) && (hashSet == null || !hashSet.Contains(val2)) && !((Object)(object)val2.itemProperties == (Object)null) && !string.IsNullOrEmpty(val2.itemProperties.itemName) && InShip(val, val2)) { NetworkObject networkObject = ((NetworkBehaviour)val2).NetworkObject; if (!((Object)(object)networkObject == (Object)null) && networkObject.IsSpawned) { list.Add(val2); } } } return list; } private static HashSet<GrabbableObject> Bagged(GrabbableObject[] objects) { HashSet<GrabbableObject> hashSet = null; foreach (GrabbableObject obj in objects) { BeltBagItem val = (BeltBagItem)(object)((obj is BeltBagItem) ? obj : null); if (val == null || val.objectsInBag == null) { continue; } foreach (GrabbableObject item in val.objectsInBag) { if (!((Object)(object)item == (Object)null)) { if (hashSet == null) { hashSet = new HashSet<GrabbableObject>(); } hashSet.Add(item); } } } return hashSet; } private static bool InShip(Collider ship, GrabbableObject item) { //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_000f: Unknown result type (might be due to invalid IL or missing references) Bounds bounds = ship.bounds; return ((Bounds)(ref bounds)).Contains(((Component)item).transform.position); } internal static Dictionary<string, int> Count() { Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); foreach (GrabbableObject item in Available()) { string itemName = item.itemProperties.itemName; dictionary.TryGetValue(itemName, out var value); dictionary[itemName] = value + 1; } return dictionary; } internal static List<GrabbableObject> Gather(Recipe recipe) { if (recipe == null) { return null; } Dictionary<string, Queue<GrabbableObject>> dictionary = new Dictionary<string, Queue<GrabbableObject>>(StringComparer.OrdinalIgnoreCase); foreach (GrabbableObject item in from o in Available() orderby o.scrapValue select o) { string itemName = item.itemProperties.itemName; if (!dictionary.TryGetValue(itemName, out var value)) { value = (dictionary[itemName] = new Queue<GrabbableObject>()); } value.Enqueue(item); } List<GrabbableObject> list = new List<GrabbableObject>(); foreach (Part part in recipe.Parts) { if (!dictionary.TryGetValue(part.Name, out var value2) || value2.Count < part.Count) { return null; } for (int num = 0; num < part.Count; num++) { list.Add(value2.Dequeue()); } } return list; } internal static int QuotaValueOf(GrabbableObject item) { if ((Object)(object)item == (Object)null || item.scrapPersistedThroughRounds || !item.isInShipRoom) { return 0; } return item.scrapValue; } internal static int LevelValueOf(GrabbableObject item) { if ((Object)(object)item == (Object)null || item.scrapPersistedThroughRounds) { return 0; } if ((Object)(object)item.itemProperties == (Object)null || !item.itemProperties.isScrap) { return 0; } return item.scrapValue; } internal static void SyncQuota(int collected, int total) { try { if ((collected != 0 || total != 0) && !((Object)(object)RoundManager.Instance == (Object)null)) { RoundManager instance = RoundManager.Instance; instance.scrapCollectedInLevel += collected; RoundManager instance2 = RoundManager.Instance; instance2.totalScrapValueInLevel += (float)total; HUDManager instance3 = HUDManager.Instance; if (instance3 != null) { instance3.SetQuota(RoundManager.Instance.scrapCollectedInLevel, -1); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not update the collected-scrap total: " + ex.Message)); } } internal static Vector3 Middle(IReadOnlyList<GrabbableObject> objects) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: 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_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) if (objects == null || objects.Count == 0) { return StartOfRound.Instance.elevatorTransform.position; } Vector3 val = Vector3.zero; foreach (GrabbableObject @object in objects) { val += ((Component)@object).transform.position; } return val / (float)objects.Count; } } public static class MyPluginInfo { public const string PLUGIN_GUID = "CraftingLibrary"; public const string PLUGIN_NAME = "CraftingLibrary"; public const string PLUGIN_VERSION = "1.0.0"; } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }