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 StonePortalControl v1.0.4
dannanboi_StonePortalControl.dll
Decompiled 16 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using TMPro; using UnityEngine; using UnityEngine.SceneManagement; [assembly: AssemblyTitle("Stone Portal Control")] [assembly: AssemblyDescription("Server-controlled stone portal restrictions and scale")] [assembly: AssemblyCompany("Dannan")] [assembly: AssemblyFileVersion("1.0.4.0")] [assembly: AssemblyInformationalVersion("1.0.4")] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyVersion("1.0.4.0")] namespace StonePortalControl; internal static class ConfigStorage { internal const string LegacyGuid = "se.dannan.stoneportalcontrol"; internal const string LegacyFolder = "StonePortalControl"; internal static string GetPath(string root, string guid) { return Path.Combine(root, guid + ".cfg"); } internal static Settings Initialize(string root, string guid, string name, string version) { //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Expected O, but got Unknown //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Expected O, but got Unknown //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Expected O, but got Unknown //IL_01c7: Unknown result type (might be due to invalid IL or missing references) //IL_01d1: Expected O, but got Unknown //IL_0201: Unknown result type (might be due to invalid IL or missing references) //IL_020b: Expected O, but got Unknown //IL_029d: Unknown result type (might be due to invalid IL or missing references) //IL_02a7: Expected O, but got Unknown //IL_02cc: Unknown result type (might be due to invalid IL or missing references) //IL_02d6: Expected O, but got Unknown //IL_02fb: Unknown result type (might be due to invalid IL or missing references) //IL_0305: Expected O, but got Unknown //IL_032a: Unknown result type (might be due to invalid IL or missing references) //IL_0334: Expected O, but got Unknown string path = GetPath(root, guid); Directory.CreateDirectory(Path.GetDirectoryName(path)); if (!File.Exists(path)) { string[] array = new string[3] { Path.Combine(root, "StonePortalControl", guid + ".cfg"), Path.Combine(root, "StonePortalControl", "se.dannan.stoneportalcontrol.cfg"), GetPath(root, "se.dannan.stoneportalcontrol") }; string[] array2 = array; foreach (string text in array2) { if (text != path && File.Exists(text)) { File.Move(text, path); break; } } } string text2 = ((!File.Exists(path)) ? null : ReadText(path)); if (text2 != null && !Settings.ReadStartupEnabled(text2)) { return new Settings(restrictions: true, 1f, showEnabledText: false, showScaleText: false, disableStonePortalRecipe: false, modEnabled: false); } Settings result = ((text2 == null) ? new Settings(restrictions: true, 1f) : Settings.Parse(text2, upgradeLegacyScale: true)); ConfigFile val = new ConfigFile(path, false, new BepInPlugin(guid, name, version)); val.SaveOnConfigSet = false; val.Bind<bool>("General", "Enabled", true, "Enable Stone Portal Control on this machine. Restart required. False skips patches, networking and runtime updates. A server requiring the mod will reject a disabled client.").Value = result.ModEnabled; val.Bind<bool>("Settings", "Enabled", true, "Obsolete; renamed to Item restrictions."); val.Remove(new ConfigDefinition("Settings", "Enabled")); val.Bind<bool>("Settings", "Item restrictions", true, "Block non-teleportable items through stone portals. Server-controlled in multiplayer; local settings apply offline.").Value = result.ItemRestrictions; val.Bind<float>("Settings", "Portal scale", 1f, new ConfigDescription("Stone portal size: 0.5 = 50%, 1 = 100%. Independent of Item restrictions. Server-controlled in multiplayer.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 1f), Array.Empty<object>())).Value = result.Scale; val.Bind<bool>("Settings", "Hide hover text", false, "Obsolete; migrated to the two show options."); val.Remove(new ConfigDefinition("Settings", "Hide hover text")); val.Bind<bool>("Settings", "Disable Stone Portal Recipe", false, "Hide the stone portal building recipe and prevent discovery/building. Remove it from loaded characters; other characters are updated on load. Server-controlled in multiplayer. Existing portals remain.").Value = result.DisableStonePortalRecipe; val.Bind<bool>("Settings", "Use custom recipe", false, "Use Custom recipe for stone portal construction. False ignores the recipe string and restores the original requirements. Disable Stone Portal Recipe takes precedence. Server-controlled; updates live.").Value = result.UseCustomRecipe; val.Bind<string>("Settings", "Custom recipe", "GreydwarfEye[10],Grausten[30],MoltenCore[2]", "Stone portal materials: Item1[amount],Item2[amount]. Use exact item prefab names and positive whole-number amounts. Default is the vanilla recipe. Ignored unless Use custom recipe is true. Server-controlled in multiplayer.").Value = result.CustomRecipe; val.Bind<bool>("Settings", "Show enabled text", true, "Obsolete; moved to UI."); val.Remove(new ConfigDefinition("Settings", "Show enabled text")); val.Bind<bool>("Settings", "Show scale text", true, "Obsolete; moved to UI."); val.Remove(new ConfigDefinition("Settings", "Show scale text")); val.Bind<bool>("UI", "Show enabled text", true, "Obsolete; renamed to Show item restrictions text."); val.Remove(new ConfigDefinition("UI", "Show enabled text")); val.Bind<bool>("UI", "Show scale text", true, "Obsolete; renamed to Show portal scale text."); val.Remove(new ConfigDefinition("UI", "Show scale text")); val.Bind<bool>("UI", "Show item restrictions text", false, "Show Item restrictions: Enabled or Item restrictions: Disabled below the vanilla portal text. Local only; never synchronized.").Value = result.ShowEnabledText; val.Bind<bool>("UI", "Show portal scale text", false, "Show Scale: xx% below the vanilla portal text. Local only; never synchronized.").Value = result.ShowScaleText; val.Save(); return result; } internal static Settings Read(string path, bool upgradeLegacyScale = false) { return Settings.Parse(ReadText(path), upgradeLegacyScale); } private static string ReadText(string path) { if (new FileInfo(path).Length > 65536) { throw new FormatException("Config exceeds 64 KiB."); } return File.ReadAllText(path); } } [BepInPlugin("dannanboi.stoneportalcontrol", "Stone Portal Control", "1.0.4")] public sealed class Plugin : BaseUnityPlugin { [HarmonyPatch(typeof(ObjectDB), "Awake")] private static class RecipeDatabaseAwake { private static void Postfix() { Instance.RecipeAssetsReady(); } } [HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")] private static class RecipeDatabaseCopy { private static void Postfix() { Instance.RecipeAssetsReady(); } } [HarmonyPatch(typeof(ZNetScene), "Awake")] private static class RecipeSceneAwake { private static void Postfix() { Instance.RefreshRecipeRules(); } } private sealed class Connection { internal ZNetPeer Peer; internal string Version; internal float StartedAt; internal bool Rejected; internal float CloseAt; } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class NewConnection { [HarmonyPriority(800)] private static void Prefix(ZNet __instance, ZNetPeer peer) { Instance.RegisterConnection(__instance, peer); } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private static class PeerInfo { [HarmonyPriority(800)] private static bool Prefix(ZRpc rpc) { return Instance.GatePeerInfo(rpc); } } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError")] private static class ShowError { private static void Postfix(FejdStartup __instance) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 if (connectionError != null && (int)ZNet.GetConnectionStatus() == 3) { object? value = ErrorText.GetValue(__instance); TMP_Text val = (TMP_Text)((value is TMP_Text) ? value : null); if ((Object)(object)val != (Object)null) { val.text = connectionError; } } } } [HarmonyPatch(typeof(ZNet), "Awake")] private static class NetworkAwake { private static void Postfix(ZNet __instance) { Instance.BeginNetwork(__instance); } } private sealed class PortalState { internal TeleportWorld Portal; internal Vector3 OriginalScale; internal bool OriginalAllowAll; internal string HoverVanilla; internal string HoverStatus; internal string HoverCombined; } [HarmonyPatch(typeof(TeleportWorld), "Awake")] private static class PortalAwake { private static void Postfix(TeleportWorld __instance) { Instance.TrackPortal(__instance); } } [HarmonyPatch(typeof(ZNetView), "SetLocalScale")] private static class NetworkScaleChanged { private static void Postfix(ZNetView __instance) { TeleportWorld component = ((Component)__instance).GetComponent<TeleportWorld>(); if (Object.op_Implicit((Object)(object)component)) { Instance.PortalStarted(component); } } } [HarmonyPatch(typeof(TeleportWorld), "Teleport")] private static class PortalTeleport { [HarmonyPriority(800)] private static bool Prefix(TeleportWorld __instance, Player player) { Plugin instance = Instance; if (!IsStone(__instance)) { return true; } instance.TrackPortal(__instance); if (Object.op_Implicit((Object)(object)instance.network) && !instance.network.IsServer() && !instance.settingsReady) { return false; } if (!instance.settingsReady || !instance.current.ItemRestrictions || !Object.op_Implicit((Object)(object)player)) { return true; } AllowAll.SetValue(__instance, false); foreach (ItemData allItem in ((Humanoid)player).GetInventory().GetAllItems()) { if (allItem.m_shared == null || allItem.m_shared.m_teleportable) { continue; } ((Character)player).Message((MessageType)2, "$msg_noteleport", 0, (Sprite)null, false); return false; } return true; } } [HarmonyPatch(typeof(TeleportWorld), "GetHoverText")] private static class PortalHover { private static void Postfix(TeleportWorld __instance, ref string __result) { Plugin instance = Instance; if ((Object)(object)instance == (Object)null || !Object.op_Implicit((Object)(object)__instance)) { return; } string text = instance.SettingsStatus(); if (string.IsNullOrEmpty(text)) { return; } if (!instance.portals.TryGetValue(__instance, out var value)) { instance.TrackPortal(__instance); if (!instance.portals.TryGetValue(__instance, out value)) { return; } } if (value.HoverCombined == null || value.HoverVanilla != __result || value.HoverStatus != text) { value.HoverVanilla = __result; value.HoverStatus = text; value.HoverCombined = __result + "\n" + text; } __result = value.HoverCombined; } } [HarmonyPatch(typeof(Player), "AddKnownPiece")] private static class RecipeDiscovery { private static bool Prefix(Player __instance, Piece __0) { if (!IsStonePiece(__0)) { return true; } Instance.RememberStoneRecipe(__0); if (!Instance.BlockStoneRecipe) { return true; } Instance.PurgeKnownStoneRecipe(__instance); return false; } } [HarmonyPatch(typeof(Player), "Load")] private static class RecipeOnLoad { private static void Postfix(Player __instance) { Instance.PurgeKnownStoneRecipe(__instance); } } [HarmonyPatch(typeof(Player), "SetLocalPlayer")] private static class RecipeOnLocalPlayer { private static void Postfix() { Instance.RefreshRecipeRules(); } } [HarmonyPatch(typeof(Player), "UpdateKnownRecipesList")] private static class RecipeKnownRefresh { private static void Prefix() { Instance.ApplyRecipeSettings(); } private static void Postfix(Player __instance) { Instance.PurgeKnownStoneRecipe(__instance); } } [HarmonyPatch(typeof(PieceTable), "UpdateAvailable")] private static class RecipeBuildMenu { private static void Postfix(PieceTable __instance) { Instance.FilterStoneRecipe(__instance); } } [HarmonyPatch(typeof(Player), "TryPlacePiece")] private static class RecipePlacement { [HarmonyPriority(800)] private static bool Prefix(Piece __0, ref bool __result) { if (!IsStonePiece(__0)) { return true; } if (!Instance.BlockStoneRecipe) { if (Instance.current.UseCustomRecipe && Instance.installedRecipe != null) { __0.m_resources = Instance.installedRecipe; } return true; } __result = false; return false; } } private static readonly FieldInfo PlacementGhost = AccessTools.Field(typeof(Player), "m_placementGhost"); private Piece recipePrefab; private Requirement[] originalRecipe; private Requirement[] installedRecipe; private Requirement[] resolvedRecipe; private ObjectDB recipeDatabase; private string resolvedRecipeText; private string lastRecipeError; private ObjectDB failedRecipeDatabase; private int failedRecipeItemCount; private string failedRecipeText; private FormatException failedRecipeError; private bool customRecipeReady; private const string HelloRpc = "dannanboi.stoneportalcontrol.Hello"; private const string SettingsRpc = "dannanboi.stoneportalcontrol.Settings"; private const string RejectedRpc = "dannanboi.stoneportalcontrol.Rejected"; private const int Protocol = 5; private static string connectionError; private static readonly FieldInfo ConnectionStatus = AccessTools.Field(typeof(ZNet), "m_connectionStatus"); private static readonly FieldInfo ErrorText = AccessTools.Field(typeof(FejdStartup), "m_connectionFailedError"); private readonly Dictionary<ZRpc, Connection> connections = new Dictionary<ZRpc, Connection>(); private readonly List<ZRpc> removedConnections = new List<ZRpc>(); public const string Guid = "dannanboi.stoneportalcontrol"; public const string ModName = "Stone Portal Control"; public const string PluginVersion = "1.0.4"; public const string ModVersion = "1.0.4"; internal static Plugin Instance; private Harmony harmony; private ZNet network; private bool authority; private bool settingsReady; private Settings current = new Settings(restrictions: false, 1f); private Settings localSettings = new Settings(restrictions: true, 1f); private string configPath; private FileInfo configInfo; private DateTime lastConfigWrite; private long lastConfigLength = -1L; private string lastConfigError; private string statusText; private float nextPoll; private float nextPeerCheck; private bool restartNotice; private static readonly FieldInfo AllowAll = AccessTools.Field(typeof(TeleportWorld), "m_allowAllItems"); private readonly Dictionary<TeleportWorld, PortalState> portals = new Dictionary<TeleportWorld, PortalState>(); private static readonly FieldInfo KnownRecipes = AccessTools.Field(typeof(Player), "m_knownRecipes"); private static readonly FieldInfo AvailablePieces = AccessTools.Field(typeof(PieceTable), "m_availablePieces"); private static readonly FieldInfo EnabledPieces = AccessTools.Field(typeof(PieceTable), "m_enabledPieces"); private static readonly FieldInfo PiecesByCategory = AccessTools.Field(typeof(PieceTable), "m_availablePiecesByCategory"); private static readonly MethodInfo UpdateKnown = AccessTools.Method(typeof(Player), "UpdateKnownRecipesList", Type.EmptyTypes, (Type[])null); private static readonly MethodInfo UpdateAvailable = AccessTools.Method(typeof(Player), "UpdateAvailablePiecesList", Type.EmptyTypes, (Type[])null); private static readonly MethodInfo SetupGhost = AccessTools.Method(typeof(Player), "SetupPlacementGhost", Type.EmptyTypes, (Type[])null); private static readonly Predicate<Piece> StonePiecePredicate = IsStonePiece; private string stoneRecipeName; private bool BlockStoneRecipe => !settingsReady || current.DisableStonePortalRecipe || (current.UseCustomRecipe && !customRecipeReady); private Requirement[] ResolveCustomRecipe(Settings value) { //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Expected O, but got Unknown if (!value.UseCustomRecipe) { return null; } ObjectDB instance = ObjectDB.instance; if (!Object.op_Implicit((Object)(object)instance) || instance.m_items == null || instance.m_items.Count == 0) { return null; } if (HasFailedRecipe(value, instance)) { throw failedRecipeError; } if (object.ReferenceEquals(recipeDatabase, instance) && resolvedRecipe != null && string.Equals(resolvedRecipeText, value.CustomRecipe, StringComparison.Ordinal)) { return resolvedRecipe; } RecipeDefinition.Ingredient[] array = RecipeDefinition.Parse(value.CustomRecipe); Requirement[] array2 = (Requirement[])(object)new Requirement[array.Length]; for (int i = 0; i < array.Length; i++) { GameObject itemPrefab = instance.GetItemPrefab(array[i].Prefab); ItemDrop val = ((!Object.op_Implicit((Object)(object)itemPrefab)) ? null : itemPrefab.GetComponent<ItemDrop>()); if (!Object.op_Implicit((Object)(object)val)) { failedRecipeDatabase = instance; failedRecipeItemCount = instance.m_items.Count; failedRecipeText = value.CustomRecipe; failedRecipeError = new FormatException("Custom recipe item is unavailable: " + array[i].Prefab + ". Use an exact item prefab name installed on the server and all clients."); throw failedRecipeError; } int num = i; Requirement val2 = new Requirement(); val2.m_resItem = val; val2.m_amount = array[i].Amount; val2.m_amountPerLevel = 0; val2.m_recover = true; array2[num] = val2; } recipeDatabase = instance; resolvedRecipeText = value.CustomRecipe; resolvedRecipe = array2; failedRecipeDatabase = null; failedRecipeError = null; failedRecipeText = null; return array2; } private bool HasFailedRecipe(Settings value, ObjectDB db) { return Object.op_Implicit((Object)(object)db) && failedRecipeError != null && object.ReferenceEquals(failedRecipeDatabase, db) && db.m_items != null && db.m_items.Count == failedRecipeItemCount && string.Equals(failedRecipeText, value.CustomRecipe, StringComparison.Ordinal); } private void ReportRecipeFailure(string message) { customRecipeReady = false; if (!(lastRecipeError == message)) { lastRecipeError = message; ((BaseUnityPlugin)this).Logger.LogWarning((object)("Stone portal construction is blocked until the custom recipe can be applied. " + message)); } } private void RestoreCustomRecipe() { if (Object.op_Implicit((Object)(object)recipePrefab) && object.ReferenceEquals(recipePrefab.m_resources, installedRecipe)) { recipePrefab.m_resources = originalRecipe; } recipePrefab = null; originalRecipe = null; installedRecipe = null; customRecipeReady = false; } private void ApplyRecipeSettings() { if (!settingsReady || !current.UseCustomRecipe) { RestoreCustomRecipe(); lastRecipeError = null; return; } try { if (HasFailedRecipe(current, ObjectDB.instance)) { ReportRecipeFailure(failedRecipeError.Message); return; } Requirement[] array = ResolveCustomRecipe(current); GameObject val = ((!Object.op_Implicit((Object)(object)ZNetScene.instance)) ? null : ZNetScene.instance.GetPrefab("portal_stone")); Piece val2 = ((!Object.op_Implicit((Object)(object)val)) ? null : val.GetComponent<Piece>()); if (array == null || !Object.op_Implicit((Object)(object)val2)) { customRecipeReady = false; return; } if (!object.ReferenceEquals(recipePrefab, val2)) { RestoreCustomRecipe(); recipePrefab = val2; originalRecipe = val2.m_resources; } if (!object.ReferenceEquals(val2.m_resources, array)) { val2.m_resources = array; } installedRecipe = array; customRecipeReady = true; lastRecipeError = null; } catch (Exception ex) { ReportRecipeFailure(ex.Message); } } private void UpdatePlacementRecipe(Player player) { object? value = PlacementGhost.GetValue(player); GameObject val = (GameObject)((value is GameObject) ? value : null); Piece val2 = ((!Object.op_Implicit((Object)(object)val)) ? null : val.GetComponent<Piece>()); if (IsStonePiece(val2)) { GameObject val3 = ((!Object.op_Implicit((Object)(object)ZNetScene.instance)) ? null : ZNetScene.instance.GetPrefab("portal_stone")); Piece val4 = ((!Object.op_Implicit((Object)(object)val3)) ? null : val3.GetComponent<Piece>()); if (Object.op_Implicit((Object)(object)val4)) { val2.m_resources = val4.m_resources; } } } private void RecipeAssetsReady() { recipeDatabase = null; resolvedRecipe = null; resolvedRecipeText = null; failedRecipeDatabase = null; failedRecipeText = null; failedRecipeError = null; if (authority) { lastConfigLength = -1L; } RefreshRecipeRules(); } internal static bool VersionMatches(string received) { return string.Equals(received, "1.0.4", StringComparison.Ordinal); } private void RegisterConnection(ZNet net, ZNetPeer peer) { BeginNetwork(net); if (!net.IsServer() && peer.m_server) { connectionError = null; settingsReady = false; current = new Settings(restrictions: false, 1f); RefreshStatus(); } Connection connection = new Connection(); connection.Peer = peer; connection.StartedAt = Time.realtimeSinceStartup; Connection value = connection; connections[peer.m_rpc] = value; peer.m_rpc.Register<string>("dannanboi.stoneportalcontrol.Hello", (Action<ZRpc, string>)ReceiveHello); peer.m_rpc.Register<ZPackage>("dannanboi.stoneportalcontrol.Settings", (Action<ZRpc, ZPackage>)ReceiveSettings); peer.m_rpc.Register<string>("dannanboi.stoneportalcontrol.Rejected", (Action<ZRpc, string>)ReceiveRejection); peer.m_rpc.Invoke("dannanboi.stoneportalcontrol.Hello", new object[1] { "1.0.4" }); } private bool IsServerConnection(ZRpc rpc) { Connection value; return Object.op_Implicit((Object)(object)network) && !network.IsServer() && connections.TryGetValue(rpc, out value) && value.Peer.m_server; } private void ReceiveHello(ZRpc rpc, string version) { if (!Object.op_Implicit((Object)(object)network) || !connections.TryGetValue(rpc, out var value) || value.Rejected || value.Version != null) { return; } value.Version = version ?? string.Empty; if (!VersionMatches(version)) { if (network.IsServer()) { Reject(rpc, value, Requirement(version)); } else if (IsServerConnection(rpc)) { FailClient("This server requires Stone Portal Control " + SafeVersion(version) + ". You have 1.0.4. Install the matching version to join."); } } else if (authority) { SendSettings(rpc); } } private static string SafeVersion(string value) { if (string.IsNullOrEmpty(value) || value.Length > 32) { return "(unknown version)"; } foreach (char c in value) { if (!char.IsLetterOrDigit(c) && c != '.' && c != '-' && c != '+' && c != '_') { return "(unknown version)"; } } return value; } private static string Requirement(string found) { return "This server requires Stone Portal Control 1.0.4. " + ((!string.IsNullOrEmpty(found)) ? ("Your version is " + SafeVersion(found) + ". ") : "The mod is missing. ") + "Install the matching version to join."; } private void Reject(ZRpc rpc, Connection connection, string message) { if (!connection.Rejected) { connection.Rejected = true; connection.CloseAt = Time.realtimeSinceStartup + 2f; ((BaseUnityPlugin)this).Logger.LogWarning((object)message); bool flag = connection.Version != null; if (flag) { rpc.Invoke("dannanboi.stoneportalcontrol.Rejected", new object[1] { message }); } rpc.Invoke("Error", new object[1] { (!flag) ? 5 : 3 }); } } private void ReceiveRejection(ZRpc rpc, string message) { if (IsServerConnection(rpc) && !connections[rpc].Rejected) { if (string.IsNullOrEmpty(message) || message.Length > 512) { message = Requirement(null); } FailClient(message.Replace("<", string.Empty).Replace(">", string.Empty)); } } private void FailClient(string message) { connectionError = message; settingsReady = false; RefreshStatus(); foreach (Connection value in connections.Values) { value.Rejected = true; value.CloseAt = Time.realtimeSinceStartup; } ConnectionStatus.SetValue(null, (object)(ConnectionStatus)3); ((BaseUnityPlugin)this).Logger.LogError((object)message); } private bool GatePeerInfo(ZRpc rpc) { if (!connections.TryGetValue(rpc, out var value)) { if (Object.op_Implicit((Object)(object)network) && network.IsServer()) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(Requirement(null) + " Connection registration was not verified.")); rpc.Invoke("Error", new object[1] { 5 }); } else { FailClient("The connection could not verify Stone Portal Control 1.0.4."); } return false; } if (value.Rejected) { return false; } if (VersionMatches(value.Version) && (network.IsServer() || settingsReady)) { return true; } if (network.IsServer()) { Reject(rpc, value, Requirement(value.Version)); } else if (connectionError == null) { FailClient("The server must also run Stone Portal Control 1.0.4 and send its settings before you can join."); } return false; } private void SendSettings(ZRpc rpc) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(5); val.Write(current.ItemRestrictions); val.Write(current.Scale); val.Write(current.DisableStonePortalRecipe); val.Write(current.UseCustomRecipe); val.Write(current.CustomRecipe); rpc.Invoke("dannanboi.stoneportalcontrol.Settings", new object[1] { val }); } private void BroadcastSettings() { if (!Object.op_Implicit((Object)(object)network) || !network.IsServer() || connections.Count == 0) { return; } foreach (KeyValuePair<ZRpc, Connection> connection in connections) { if (VersionMatches(connection.Value.Version) && !connection.Value.Rejected && connection.Key.IsConnected()) { SendSettings(connection.Key); } } } private void ReceiveSettings(ZRpc rpc, ZPackage data) { if (!IsServerConnection(rpc)) { return; } Connection connection = connections[rpc]; if (!VersionMatches(connection.Version) || connection.Rejected) { return; } try { if (data.ReadInt() != 5) { throw new FormatException("Settings protocol mismatch."); } bool flag = data.ReadBool(); float num = data.ReadSingle(); bool flag2 = data.ReadBool(); bool flag3 = data.ReadBool(); string text = data.ReadString(); if (!Settings.ValidScale(num)) { throw new FormatException("Invalid portal scale from server."); } RecipeDefinition.Validate(text, flag3); ref Settings reference = ref localSettings; bool restrictions = flag; float scale = num; bool disableStonePortalRecipe = flag2; bool useCustomRecipe = flag3; Settings settings = reference.WithGameplayFrom(new Settings(restrictions, scale, showEnabledText: false, showScaleText: false, disableStonePortalRecipe, modEnabled: true, useCustomRecipe, text)); if (settingsReady && current.SameGameplayAs(settings)) { return; } ResolveCustomRecipe(settings); bool flag4 = !settingsReady || current.ItemRestrictions != flag || current.Scale != num; bool flag5 = !settingsReady || !current.SameActiveRecipeAs(settings); current = settings; settingsReady = true; if (flag4) { foreach (PortalState value in portals.Values) { ApplyPortal(value); } } if (flag5) { RefreshRecipeRules(); } LogSettings(); } catch (Exception ex) { FailClient("Stone Portal Control 1.0.4: " + ex.Message); } } private void PumpConnections(float now) { if (connections.Count == 0) { return; } removedConnections.Clear(); bool flag = network.IsServer(); foreach (KeyValuePair<ZRpc, Connection> connection in connections) { Connection value = connection.Value; if (!connection.Key.IsConnected()) { removedConnections.Add(connection.Key); continue; } if (flag && !value.Rejected && value.Version == null && now - value.StartedAt > 30f) { Reject(connection.Key, value, Requirement(null)); } else if (!flag && value.Peer.m_server && !settingsReady && connectionError == null && now - value.StartedAt > 30f) { FailClient("The server did not complete the Stone Portal Control 1.0.4 handshake."); } if (value.Rejected && now >= value.CloseAt) { network.Disconnect(value.Peer); removedConnections.Add(connection.Key); } } foreach (ZRpc removedConnection in removedConnections) { connections.Remove(removedConnection); } removedConnections.Clear(); } private static void ValidateApi() { ValidateRecipeApi(); if (AllowAll == null || AllowAll.FieldType != typeof(bool) || ConnectionStatus == null || ErrorText == null) { throw new MissingMemberException("This Valheim build is incompatible with Stone Portal Control."); } if (AccessTools.Method(typeof(ZNet), "OnNewConnection", new Type[1] { typeof(ZNetPeer) }, (Type[])null) == null || AccessTools.Method(typeof(ZNet), "RPC_PeerInfo", new Type[2] { typeof(ZRpc), typeof(ZPackage) }, (Type[])null) == null || AccessTools.Method(typeof(TeleportWorld), "Teleport", new Type[1] { typeof(Player) }, (Type[])null) == null) { throw new MissingMethodException("This Valheim build is incompatible with Stone Portal Control."); } } private void Awake() { //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Expected O, but got Unknown ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; configPath = ConfigStorage.GetPath(Paths.ConfigPath, "dannanboi.stoneportalcontrol"); try { localSettings = ConfigStorage.Initialize(Paths.ConfigPath, "dannanboi.stoneportalcontrol", "Stone Portal Control", "1.0.4"); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Cannot initialize config; plugin disabled until the file is corrected and the game/server restarted: " + ex.Message)); ((Behaviour)this).enabled = false; return; } if (!localSettings.ModEnabled) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Stone Portal Control is disabled. Restart after enabling it in the config."); ((Behaviour)this).enabled = false; return; } Instance = this; configInfo = new FileInfo(configPath); authority = true; current = localSettings; settingsReady = true; PollConfig(); RefreshStatus(); ValidateApi(); harmony = new Harmony("dannanboi.stoneportalcontrol"); harmony.PatchAll(typeof(Plugin).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Stone Portal Control 1.0.4 loaded. Local settings apply offline; multiplayer settings are locked to the server."); } private void BeginNetwork(ZNet value) { if (!object.ReferenceEquals(network, value)) { RestorePortals(); RestoreCustomRecipe(); connections.Clear(); network = value; authority = !Object.op_Implicit((Object)(object)value) || value.IsServer(); settingsReady = false; current = new Settings(restrictions: false, 1f); if (authority) { current = localSettings; settingsReady = true; lastConfigLength = -1L; PollConfig(); } RefreshRecipeRules(); LogSettings(); nextPoll = 0f; nextPeerCheck = 0f; } } private void Update() { if (!object.ReferenceEquals(network, ZNet.instance)) { BeginNetwork(ZNet.instance); } float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup >= nextPoll) { nextPoll = realtimeSinceStartup + 1f; PollConfig(); } if (Object.op_Implicit((Object)(object)network) && realtimeSinceStartup >= nextPeerCheck) { nextPeerCheck = realtimeSinceStartup + 1f; PumpConnections(realtimeSinceStartup); } } private void PollConfig() { if (configInfo == null) { return; } try { FileInfo fileInfo = configInfo; fileInfo.Refresh(); if (!fileInfo.Exists) { throw new FileNotFoundException("Config file is missing."); } if (fileInfo.LastWriteTimeUtc == lastConfigWrite && fileInfo.Length == lastConfigLength) { return; } Settings value; try { value = ConfigStorage.Read(configPath); if (authority) { ResolveCustomRecipe(value); } } catch (FormatException) { lastConfigWrite = fileInfo.LastWriteTimeUtc; lastConfigLength = fileInfo.Length; throw; } lastConfigWrite = fileInfo.LastWriteTimeUtc; lastConfigLength = fileInfo.Length; lastConfigError = null; Settings settings = current; bool flag = localSettings.ShowEnabledText != value.ShowEnabledText || localSettings.ShowScaleText != value.ShowScaleText; localSettings = value; if (!value.ModEnabled && !restartNotice) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Enabled changed to false. Restart the game/server to disable the plugin completely."); } restartNotice = !value.ModEnabled; if (authority) { current = value; } else { current = value.WithGameplayFrom(current); } bool flag2 = authority && !settings.SameGameplayAs(current); if (flag2) { if (settings.ItemRestrictions != current.ItemRestrictions || settings.Scale != current.Scale) { foreach (PortalState value2 in portals.Values) { ApplyPortal(value2); } } if (!settings.SameActiveRecipeAs(current)) { RefreshRecipeRules(); } BroadcastSettings(); } if (flag2 || flag) { LogSettings(); } } catch (Exception ex2) { if (ex2 is IOException) { lastConfigLength = -1L; } if (!(lastConfigError == ex2.Message)) { lastConfigError = ex2.Message; ((BaseUnityPlugin)this).Logger.LogWarning((object)("Config not applied; retaining previous values. " + ex2.Message)); } } } private string SettingsStatus() { return statusText; } private void RefreshStatus() { statusText = string.Empty; if (settingsReady) { if (localSettings.ShowEnabledText) { statusText = "Item restrictions: " + ((!current.ItemRestrictions) ? "Disabled" : "Enabled"); } if (localSettings.ShowScaleText) { string text = statusText; statusText = text + ((statusText.Length != 0) ? "\n" : string.Empty) + "Scale: " + (current.Scale * 100f).ToString("0.#", CultureInfo.InvariantCulture) + "%"; } } } private void LogSettings() { RefreshStatus(); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Item restrictions: " + ((!current.ItemRestrictions) ? "Disabled" : "Enabled") + "; Scale: " + (current.Scale * 100f).ToString("0.#", CultureInfo.InvariantCulture) + "%; Show item restrictions text=" + localSettings.ShowEnabledText + "; Show portal scale text=" + localSettings.ShowScaleText + "; Disable Stone Portal Recipe=" + current.DisableStonePortalRecipe + "; Use custom recipe=" + current.UseCustomRecipe + " (gameplay: " + ((!authority) ? "server locked" : "local settings") + "; UI: local)")); } private void OnDestroy() { if (harmony != null) { RestorePortals(); RestoreCustomRecipe(); harmony.UnpatchSelf(); settingsReady = true; current = new Settings(restrictions: false, 1f); RefreshRecipeRules(); if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } } private static bool IsStone(TeleportWorld portal) { if (!Object.op_Implicit((Object)(object)portal)) { return false; } string name = ((Object)((Component)portal).gameObject).name; return name == "portal_stone" || name.StartsWith("portal_stone(", StringComparison.Ordinal); } private void TrackPortal(TeleportWorld portal) { //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_0054: 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) if (!Object.op_Implicit((Object)(object)portal) || portals.ContainsKey(portal) || !IsStone(portal)) { return; } Scene scene = ((Component)portal).gameObject.scene; if (((Scene)(ref scene)).IsValid()) { PortalState portalState = new PortalState(); portalState.Portal = portal; portalState.OriginalScale = ((Component)portal).transform.localScale; portalState.OriginalAllowAll = (bool)AllowAll.GetValue(portal); PortalState portalState2 = portalState; portals.Add(portal, portalState2); PortalLifetime portalLifetime = ((Component)portal).GetComponent<PortalLifetime>(); if (!Object.op_Implicit((Object)(object)portalLifetime)) { portalLifetime = ((Component)portal).gameObject.AddComponent<PortalLifetime>(); } portalLifetime.Initialize(portal); ApplyPortal(portalState2); } } private void ApplyPortal(PortalState state) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0037: 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_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)state.Portal)) { Vector3 val = state.OriginalScale * ((!settingsReady) ? 1f : current.Scale); if (((Component)state.Portal).transform.localScale != val) { ((Component)state.Portal).transform.localScale = val; } bool flag = (!settingsReady || !current.ItemRestrictions) && state.OriginalAllowAll; if ((bool)AllowAll.GetValue(state.Portal) != flag) { AllowAll.SetValue(state.Portal, flag); } } } internal void PortalStarted(TeleportWorld portal) { if (Object.op_Implicit((Object)(object)portal)) { if (portals.TryGetValue(portal, out var value)) { ApplyPortal(value); } else { TrackPortal(portal); } } } internal void UntrackPortal(TeleportWorld portal) { if (!object.ReferenceEquals(portal, null)) { portals.Remove(portal); } } private void RestorePortals() { //IL_003f: Unknown result type (might be due to invalid IL or missing references) foreach (PortalState value in portals.Values) { if (Object.op_Implicit((Object)(object)value.Portal)) { ((Component)value.Portal).transform.localScale = value.OriginalScale; AllowAll.SetValue(value.Portal, value.OriginalAllowAll); } } portals.Clear(); } private static bool IsStonePiece(Piece piece) { if (!Object.op_Implicit((Object)(object)piece)) { return false; } string name = ((Object)((Component)piece).gameObject).name; return name == "portal_stone" || name.StartsWith("portal_stone(", StringComparison.Ordinal); } private void RememberStoneRecipe(Piece piece) { if (Object.op_Implicit((Object)(object)piece) && !string.IsNullOrEmpty(piece.m_name)) { stoneRecipeName = piece.m_name; } } private void PurgeKnownStoneRecipe(Player player) { if (!Object.op_Implicit((Object)(object)player) || !BlockStoneRecipe) { return; } if (string.IsNullOrEmpty(stoneRecipeName) && Object.op_Implicit((Object)(object)ZNetScene.instance)) { GameObject prefab = ZNetScene.instance.GetPrefab("portal_stone"); if (Object.op_Implicit((Object)(object)prefab)) { RememberStoneRecipe(prefab.GetComponent<Piece>()); } } if (!string.IsNullOrEmpty(stoneRecipeName)) { ((HashSet<string>)KnownRecipes.GetValue(player)).Remove(stoneRecipeName); } } private void RefreshRecipeRules() { ApplyRecipeSettings(); Player localPlayer = Player.m_localPlayer; if (Object.op_Implicit((Object)(object)localPlayer)) { if (BlockStoneRecipe) { PurgeKnownStoneRecipe(localPlayer); } else { UpdateKnown.Invoke(localPlayer, null); } UpdateAvailable.Invoke(localPlayer, null); SetupGhost.Invoke(localPlayer, null); UpdatePlacementRecipe(localPlayer); } } private void FilterStoneRecipe(PieceTable table) { if (!BlockStoneRecipe) { return; } HashSet<Piece> hashSet = (HashSet<Piece>)AvailablePieces.GetValue(table); HashSet<Piece> hashSet2 = (HashSet<Piece>)EnabledPieces.GetValue(table); hashSet?.RemoveWhere(StonePiecePredicate); hashSet2?.RemoveWhere(StonePiecePredicate); List<List<Piece>> list = (List<List<Piece>>)PiecesByCategory.GetValue(table); if (list == null) { return; } foreach (List<Piece> item in list) { item?.RemoveAll(StonePiecePredicate); } } private static void ValidateRecipeApi() { if (KnownRecipes == null || KnownRecipes.FieldType != typeof(HashSet<string>) || AvailablePieces == null || AvailablePieces.FieldType != typeof(HashSet<Piece>) || EnabledPieces == null || EnabledPieces.FieldType != typeof(HashSet<Piece>) || PiecesByCategory == null || PiecesByCategory.FieldType != typeof(List<List<Piece>>) || UpdateKnown == null || UpdateAvailable == null || SetupGhost == null || PlacementGhost == null || PlacementGhost.FieldType != typeof(GameObject) || AccessTools.Method(typeof(ObjectDB), "Awake", Type.EmptyTypes, (Type[])null) == null || AccessTools.Method(typeof(ObjectDB), "CopyOtherDB", new Type[1] { typeof(ObjectDB) }, (Type[])null) == null || AccessTools.Method(typeof(ZNetScene), "Awake", Type.EmptyTypes, (Type[])null) == null) { throw new MissingMemberException("This Valheim build's recipe API is incompatible with Stone Portal Control."); } } } internal sealed class PortalLifetime : MonoBehaviour { private TeleportWorld portal; internal void Initialize(TeleportWorld value) { portal = value; } private IEnumerator Start() { yield return null; if ((Object)(object)Plugin.Instance != (Object)null && Object.op_Implicit((Object)(object)portal)) { Plugin.Instance.PortalStarted(portal); } } private void OnDestroy() { if ((Object)(object)Plugin.Instance != (Object)null) { Plugin.Instance.UntrackPortal(portal); } } } internal static class RecipeDefinition { internal struct Ingredient { internal readonly string Prefab; internal readonly int Amount; internal Ingredient(string prefab, int amount) { Prefab = prefab; Amount = amount; } } internal const string Default = "GreydwarfEye[10],Grausten[30],MoltenCore[2]"; internal const int MaxLength = 2048; internal const int MaxIngredients = 32; private static readonly char[] InvalidNameCharacters = new char[3] { '\r', '\n', '=' }; internal static void Validate(string text, bool enabled) { if (text == null || text.Length > 2048) { throw new FormatException("Custom recipe must be at most " + 2048 + " characters."); } if (enabled) { Parse(text); } } internal static Ingredient[] Parse(string text) { if (string.IsNullOrWhiteSpace(text) || text.Length > 2048) { throw new FormatException("Custom recipe requires Item1[amount],Item2[amount] entries."); } string[] array = text.Split(','); if (array.Length > 32) { throw new FormatException("Custom recipe supports at most " + 32 + " ingredients."); } Ingredient[] array2 = new Ingredient[array.Length]; HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); int num = text2.IndexOf('['); if (num <= 0 || !text2.EndsWith("]", StringComparison.Ordinal) || text2.IndexOf('[', num + 1) >= 0 || text2.IndexOf(']') != text2.Length - 1) { throw new FormatException("Invalid custom recipe entry: " + text2 + ". Use Item[amount]."); } string text3 = text2.Substring(0, num).Trim(); if (text3.Length == 0 || text3.Length > 128 || text3.IndexOfAny(InvalidNameCharacters) >= 0 || !int.TryParse(text2.Substring(num + 1, text2.Length - num - 2).Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < 1) { throw new FormatException("Custom recipe amounts must be positive whole numbers; use exact item prefab names."); } if (!hashSet.Add(text3)) { throw new FormatException("Duplicate custom recipe item: " + text3); } ref Ingredient reference = ref array2[i]; reference = new Ingredient(text3, result); } return array2; } } internal struct Settings { internal readonly bool ItemRestrictions; internal readonly bool DisableStonePortalRecipe; internal readonly bool UseCustomRecipe; internal readonly string CustomRecipe; internal readonly float Scale; internal readonly bool ShowEnabledText; internal readonly bool ShowScaleText; internal readonly bool ModEnabled; internal const string DefaultFile = "[General]\nEnabled = true\n\n[Settings]\nItem restrictions = true\nPortal scale = 1\nDisable Stone Portal Recipe = false\nUse custom recipe = false\nCustom recipe = GreydwarfEye[10],Grausten[30],MoltenCore[2]\n\n[UI]\nShow item restrictions text = false\nShow portal scale text = false\n"; internal Settings(bool restrictions, float scale, bool showEnabledText = false, bool showScaleText = false, bool disableStonePortalRecipe = false, bool modEnabled = true, bool useCustomRecipe = false, string customRecipe = "GreydwarfEye[10],Grausten[30],MoltenCore[2]") { ItemRestrictions = restrictions; Scale = scale; ShowEnabledText = showEnabledText; ShowScaleText = showScaleText; DisableStonePortalRecipe = disableStonePortalRecipe; ModEnabled = modEnabled; UseCustomRecipe = useCustomRecipe; CustomRecipe = customRecipe; } internal bool SameGameplayAs(Settings other) { return ItemRestrictions == other.ItemRestrictions && Scale == other.Scale && DisableStonePortalRecipe == other.DisableStonePortalRecipe && UseCustomRecipe == other.UseCustomRecipe && string.Equals(CustomRecipe, other.CustomRecipe, StringComparison.Ordinal); } internal bool SameActiveRecipeAs(Settings other) { return DisableStonePortalRecipe == other.DisableStonePortalRecipe && UseCustomRecipe == other.UseCustomRecipe && (!UseCustomRecipe || string.Equals(CustomRecipe, other.CustomRecipe, StringComparison.Ordinal)); } internal bool SameAs(Settings other) { return SameGameplayAs(other) && ShowEnabledText == other.ShowEnabledText && ShowScaleText == other.ShowScaleText && ModEnabled == other.ModEnabled; } internal Settings WithGameplayFrom(Settings server) { return new Settings(server.ItemRestrictions, server.Scale, ShowEnabledText, ShowScaleText, server.DisableStonePortalRecipe, ModEnabled, server.UseCustomRecipe, server.CustomRecipe); } internal static bool ValidScale(float scale) { return !float.IsNaN(scale) && !float.IsInfinity(scale) && scale >= 0.5f && scale <= 1f; } internal static bool ReadStartupEnabled(string text) { bool result = true; bool flag = false; string text2 = string.Empty; using StringReader stringReader = new StringReader(text); string text3; while ((text3 = stringReader.ReadLine()) != null) { text3 = text3.Trim(); if (text3.StartsWith("[") && text3.EndsWith("]")) { text2 = text3.Substring(1, text3.Length - 2).Trim(); continue; } int num = text3.IndexOf('='); if (!(text2 != "General") && num >= 0 && !(text3.Substring(0, num).Trim() != "Enabled")) { if (flag || !bool.TryParse(text3.Substring(num + 1).Trim(), out result)) { throw new FormatException("[General] Enabled must be one true/false value."); } flag = true; } } return result; } internal static Settings Parse(string text, bool upgradeLegacyScale = false) { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal); string text2 = string.Empty; using (StringReader stringReader = new StringReader(text)) { string text3; while ((text3 = stringReader.ReadLine()) != null) { text3 = text3.Trim(); if (text3.Length == 0 || text3.StartsWith("#") || text3.StartsWith(";")) { continue; } if (text3.StartsWith("[") && text3.EndsWith("]")) { text2 = text3.Substring(1, text3.Length - 2).Trim(); continue; } int num = text3.IndexOf('='); if (num < 0) { throw new FormatException("Expected 'name = value'."); } string text4 = text3.Substring(0, num).Trim(); string value = text3.Substring(num + 1).Trim(); if (text2 == "General" && text4 == "Enabled") { goto IL_01ed; } if (text2 == "UI") { switch (text4) { case "Show item restrictions text": case "Show portal scale text": case "Show enabled text": case "Show scale text": goto IL_01ed; } } int num2; if (text2 == "Settings") { switch (text4) { default: num2 = ((text4 == "Hide hover text") ? 1 : 0); break; case "Item restrictions": case "Portal scale": case "Disable Stone Portal Recipe": case "Use custom recipe": case "Custom recipe": case "Enabled": case "Show enabled text": case "Show scale text": num2 = 1; break; } } else { num2 = 0; } goto IL_01ee; IL_01ed: num2 = 1; goto IL_01ee; IL_01ee: bool flag = (byte)num2 != 0; string text5 = text2 + "/" + text4; if (!flag || dictionary.ContainsKey(text5)) { throw new FormatException("Unknown or duplicate setting: " + text5); } dictionary.Add(text5, value); if (text4 != "Portal scale" && text4 != "Custom recipe") { Boolean(value, text5); } } } if (!dictionary.TryGetValue("Settings/Item restrictions", out var value2) && !dictionary.TryGetValue("Settings/Enabled", out value2)) { throw new FormatException("Item restrictions is required in [Settings]."); } if (!dictionary.TryGetValue("Settings/Portal scale", out var value3)) { throw new FormatException("Portal scale is required in [Settings]."); } if (!float.TryParse(value3, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { throw new FormatException("Portal scale must be from 0.5 to 1, using a decimal point."); } if (upgradeLegacyScale && result >= 0.1f && result < 0.5f) { result = 0.5f; } if (!ValidScale(result)) { throw new FormatException("Portal scale must be from 0.5 to 1, using a decimal point."); } bool fallback = dictionary.ContainsKey("Settings/Hide hover text") && !GetBoolean(dictionary, "Settings/Hide hover text", fallback: false); bool boolean = GetBoolean(dictionary, "Settings/Use custom recipe", fallback: false); if (!dictionary.TryGetValue("Settings/Custom recipe", out var value4)) { value4 = "GreydwarfEye[10],Grausten[30],MoltenCore[2]"; } RecipeDefinition.Validate(value4, boolean); return new Settings(Boolean(value2, "Item restrictions"), result, GetBoolean(dictionary, "UI/Show item restrictions text", GetBoolean(dictionary, "UI/Show enabled text", GetBoolean(dictionary, "Settings/Show enabled text", fallback))), GetBoolean(dictionary, "UI/Show portal scale text", GetBoolean(dictionary, "UI/Show scale text", GetBoolean(dictionary, "Settings/Show scale text", fallback))), GetBoolean(dictionary, "Settings/Disable Stone Portal Recipe", fallback: false), GetBoolean(dictionary, "General/Enabled", fallback: true), boolean, value4); } private static bool Boolean(string value, string name) { if (!bool.TryParse(value, out var result)) { throw new FormatException(name + " must be true or false."); } return result; } private static bool GetBoolean(Dictionary<string, string> values, string key, bool fallback) { string value; return (!values.TryGetValue(key, out value)) ? fallback : Boolean(value, key); } }