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 FishermansFixes v1.0.1
BepInEx/plugins/dannanboi_FishermansFixes.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("FishermansFixes")] [assembly: AssemblyDescription("Configurable fishing bait, progression, Fishing skill requirements, rod recipes and treasure removal.")] [assembly: AssemblyCompany("dannanboi")] [assembly: AssemblyProduct("FishermansFixes")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace FishermansFixes { [BepInPlugin("dannanboi.fishermansfixes", "FishermansFixes", "1.0.0")] public sealed class Plugin : BaseUnityPlugin { [HarmonyPatch(typeof(ZNet), "Awake")] private static class NetworkAwake { private static void Postfix(ZNet __instance) { Instance?.BeginNetwork(__instance); } } [HarmonyPatch(typeof(ZNet), "Shutdown")] private static class NetworkShutdown { private static void Postfix(ZNet __instance) { if ((Object)(object)Instance != (Object)null && Instance.network == __instance) { Instance.BeginNetwork(null); } } } 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) { if (!((Object)(object)Instance == (Object)null)) { return Instance.GatePeerInfo(rpc); } return true; } } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError")] private static class ShowError { private static void Postfix(FejdStartup __instance) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: 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; } } } } public const string Guid = "dannanboi.fishermansfixes"; public const string ModName = "FishermansFixes"; public const string PluginVersion = "1.0.0"; public const string ModVersion = "1.0.0"; internal static Plugin Instance; private LiveConfig settings; private Harmony core; private SectionPatches sections; private RecipeRegistry recipes; private ZNet network; private bool dirty; private int refresh; private float nextPublish; private float nextPeerCheck; private float nextYamlPoll; private FileInfo yamlInfo; private DateTime yamlWrite; private long yamlLength; private bool yamlObserved; private bool yamlPending; private float yamlReloadAt; private string yamlError; private byte[] settingsPacket; private Settings encodedSettings; private const string HelloRpc = "dannanboi.fishermansfixes.Hello"; private const string SettingsRpc = "dannanboi.fishermansfixes.Settings"; private const string RejectedRpc = "dannanboi.fishermansfixes.Rejected"; private const int Protocol = 1; 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>(); private bool authority => !settings.Client; private bool settingsReady => settings.Ready; private void Awake() { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Expected O, but got Unknown Instance = this; settings = new LiveConfig(((BaseUnityPlugin)this).Config); recipes = new RecipeRegistry(); settings.Refresh += QueueRefresh; settings.AuthorityChanged += Changed; core = new Harmony("dannanboi.fishermansfixes"); Type[] array = new Type[2] { typeof(SaveLocalPreferences), typeof(ReloadLocalPreferences) }; foreach (Type type in array) { core.CreateClassProcessor(type).Patch(); } array = typeof(Plugin).GetNestedTypes(BindingFlags.NonPublic); foreach (Type type2 in array) { if (Attribute.IsDefined(type2, typeof(HarmonyPatch))) { core.CreateClassProcessor(type2).Patch(); } } sections = new SectionPatches(); BeginNetwork(ZNet.instance); QueueRefresh(); Refresh(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"FishermansFixes 1.0.0 loaded."); } internal static void Warn(string message) { Plugin instance = Instance; if (instance != null) { ((BaseUnityPlugin)instance).Logger.LogWarning((object)message); } } private void Changed() { dirty = true; QueueRefresh(); } private void QueueRefresh() { Interlocked.Exchange(ref refresh, 1); } internal void Database(ObjectDB db) { recipes.Register(db); recipes.Apply(settings.Current, settingsReady); } private static void Unregister(ZRpc rpc) { rpc.Unregister("dannanboi.fishermansfixes.Hello"); rpc.Unregister("dannanboi.fishermansfixes.Settings"); rpc.Unregister("dannanboi.fishermansfixes.Rejected"); } private void BeginNetwork(ZNet value) { if (network == value) { return; } foreach (ZRpc key in connections.Keys) { Unregister(key); } connections.Clear(); network = value; connectionError = null; dirty = false; settingsPacket = null; encodedSettings = null; nextPublish = (nextPeerCheck = 0f); settings.SetClient(client: false); settings.SetClient((Object)(object)value != (Object)null && !value.IsServer()); yamlInfo = null; yamlObserved = false; QueueRefresh(); BaitRules.Configure(settings.Current, ready: false); recipes.Apply(settings.Current, ready: false); } private void Refresh() { if (authority && settings.Current.Bait) { if (yamlInfo == null) { string text = Path.Combine(Paths.ConfigPath, "FishermansFixes"); Directory.CreateDirectory(text); string text2 = Path.Combine(text, "baits.yaml"); if (!File.Exists(text2)) { File.WriteAllText(text2, "# FishermansFixes\n# Server/host controls this file in multiplayer; reloads automatically while Bait is enabled.\n# Top-level keys and fish lists use case-sensitive console prefab names.\n# Section headings: 2 spaces. Dashed rows: 4 spaces, matching Food Knowledge style.\n# levelUnlock: Fishing skill 0-100; 0 is unrestricted. Temporary bonuses count.\n# recipe: \"\" keeps native recipes. Otherwise use Item1[amount],Item2[amount].\n# Existing recipes retain their station, station level and output amount.\n# A bait without a native recipe gets 20 bait per craft at a level 1 Workbench.\n# Lower Tier Progression order: Meadows, BlackForest, Swamp, Ocean, Mountain,\n# Plains, Mistlands, Ashlands, DeepNorth. Lists are inherited from lower biomes.\n# Custom bait must already exist in the game; specify biome to include it in progression.\n# An omitted bait or omitted fish section uses native fish rules. fish: [] disallows direct catches (lower tiers still apply).\n\nFishingBait:\n basics:\n - biome: Meadows\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish1\n - Fish2\n\nFishingBaitForest:\n basics:\n - biome: BlackForest\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish5\n\nFishingBaitSwamp:\n basics:\n - biome: Swamp\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish6\n\nFishingBaitOcean:\n basics:\n - biome: Ocean\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish3\n - Fish8\n\nFishingBaitCave:\n basics:\n - biome: Mountain\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish4\n - Fish4_cave\n\nFishingBaitPlains:\n basics:\n - biome: Plains\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish7\n\nFishingBaitMistlands:\n basics:\n - biome: Mistlands\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish9\n - Fish12\n\nFishingBaitAshlands:\n basics:\n - biome: Ashlands\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish11\n\nFishingBaitDeepNorth:\n basics:\n - biome: DeepNorth\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish10\n\n"); } yamlInfo = new FileInfo(text2); LoadYaml(); } } else { yamlInfo = null; yamlObserved = (yamlPending = false); } Settings current = settings.Current; BaitRules.Configure(current, settingsReady); sections.Refresh(current, settingsReady); recipes.Register(ObjectDB.instance); recipes.Apply(current, settingsReady); Interlocked.Exchange(ref refresh, 0); } private void LoadYaml() { try { yamlInfo.Refresh(); if (yamlInfo.Exists) { if (yamlInfo.Length > 131072) { throw new FormatException("baits.yaml exceeds 128 KiB."); } string yaml = File.ReadAllText(yamlInfo.FullName); settings.SetYaml(yaml); yamlWrite = yamlInfo.LastWriteTimeUtc; yamlLength = yamlInfo.Length; yamlObserved = true; yamlPending = false; yamlError = null; } } catch (Exception ex) { if (yamlError != ex.Message) { Warn("baits.yaml was not applied; keeping the last valid definitions: " + ex.Message); } yamlError = ex.Message; yamlObserved = true; yamlPending = false; yamlInfo.Refresh(); yamlWrite = yamlInfo.LastWriteTimeUtc; yamlLength = (yamlInfo.Exists ? yamlInfo.Length : (-1)); } } private void PollYaml(float now) { if (yamlInfo == null || now < nextYamlPoll) { return; } nextYamlPoll = now + 1f; try { yamlInfo.Refresh(); if (yamlInfo.Exists) { if (!yamlObserved || yamlWrite != yamlInfo.LastWriteTimeUtc || yamlLength != yamlInfo.Length) { yamlWrite = yamlInfo.LastWriteTimeUtc; yamlLength = yamlInfo.Length; yamlObserved = true; yamlPending = true; yamlReloadAt = now + 0.25f; } else if (yamlPending && now >= yamlReloadAt) { LoadYaml(); } } } catch (Exception ex) { if (yamlError != ex.Message) { Warn("Could not reload baits.yaml: " + ex.Message); } yamlError = ex.Message; } } private void Update() { float realtimeSinceStartup = Time.realtimeSinceStartup; BeginNetwork(ZNet.instance); settings.PollFile(realtimeSinceStartup); PollYaml(realtimeSinceStartup); if (Interlocked.Exchange(ref refresh, 0) != 0) { Refresh(); } if (Object.op_Implicit((Object)(object)network)) { if (dirty && authority && realtimeSinceStartup >= nextPublish) { dirty = false; nextPublish = realtimeSinceStartup + 0.25f; BroadcastSettings(); } if (realtimeSinceStartup >= nextPeerCheck) { nextPeerCheck = realtimeSinceStartup + 1f; PumpConnections(realtimeSinceStartup); } } } private void OnDestroy() { if (settings != null) { settings.Refresh -= QueueRefresh; settings.AuthorityChanged -= Changed; foreach (ZRpc key in connections.Keys) { Unregister(key); } connections.Clear(); settings.Dispose(); } sections?.Dispose(); recipes?.Dispose(); Harmony obj = core; if (obj != null) { obj.UnpatchSelf(); } if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } internal static bool VersionMatches(string received) { return string.Equals(received, "1.0.0", StringComparison.Ordinal); } private void RegisterConnection(ZNet net, ZNetPeer peer) { if ((Object)(object)net == (Object)null || peer == null || peer.m_rpc == null) { return; } BeginNetwork(net); if (!connections.ContainsKey(peer.m_rpc)) { if (!net.IsServer() && peer.m_server) { connectionError = null; settings.WaitForServer(); } Connection value = new Connection { Peer = peer, StartedAt = Time.realtimeSinceStartup }; connections[peer.m_rpc] = value; peer.m_rpc.Register<string>("dannanboi.fishermansfixes.Hello", (Action<ZRpc, string>)ReceiveHello); peer.m_rpc.Register<ZPackage>("dannanboi.fishermansfixes.Settings", (Action<ZRpc, ZPackage>)ReceiveSettings); peer.m_rpc.Register<string>("dannanboi.fishermansfixes.Rejected", (Action<ZRpc, string>)ReceiveRejection); peer.m_rpc.Invoke("dannanboi.fishermansfixes.Hello", new object[1] { "1.0.0" }); } } private bool IsServerConnection(ZRpc rpc) { if (Object.op_Implicit((Object)(object)network) && !network.IsServer() && connections.TryGetValue(rpc, out var value)) { return value.Peer.m_server; } return false; } 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 != null && version.Length <= 32) ? version : ""); if (!VersionMatches(value.Version)) { if (network.IsServer()) { Reject(rpc, value, Requirement(version)); } else if (IsServerConnection(rpc)) { FailClient("This server requires FishermansFixes " + SafeVersion(version) + ". You have 1.0.0. 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 FishermansFixes 1.0.0. " + (string.IsNullOrEmpty(found) ? "The mod is missing. " : ("Your version is " + SafeVersion(found) + ". ")) + "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.fishermansfixes.Rejected", new object[1] { message }); } rpc.Invoke("Error", new object[1] { flag ? 3 : 5 }); } } 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("<", "").Replace(">", "")); } } private void FailClient(string message) { connectionError = message; settings.WaitForServer(); 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 FishermansFixes 1.0.0."); } 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 FishermansFixes 1.0.0 and send its settings before you can join."); } return false; } private void SendSettings(ZRpc rpc) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Expected O, but got Unknown //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Expected O, but got Unknown if (authority && settings.Current.Bait && yamlInfo == null) { Refresh(); } Settings current = settings.Current; if (settingsPacket == null || !current.SamePacket(encodedSettings)) { ZPackage val = new ZPackage(); val.Write(1); current.Write(val); settingsPacket = val.GetArray(); encodedSettings = current; if (settingsPacket.Length > 262144) { throw new FormatException("Settings packet is too large."); } } rpc.Invoke("dannanboi.fishermansfixes.Settings", new object[1] { (object)new ZPackage(settingsPacket) }); } private void BroadcastSettings() { if (!Object.op_Implicit((Object)(object)network) || !network.IsServer() || connections.Count == 0 || settings.Current.SamePacket(encodedSettings)) { 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 == null || data.Size() < 7 || data.Size() > 262144 || data.ReadInt() != 1) { throw new FormatException("Invalid settings packet or protocol."); } Settings values = Settings.Read(data); if (data.GetPos() != data.Size()) { throw new FormatException("Unexpected data in settings packet."); } settings.Receive(values); } catch (Exception ex) { FailClient("FishermansFixes 1.0.0: " + 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 FishermansFixes 1.0.0 handshake."); } if (value.Rejected && now >= value.CloseAt) { network.Disconnect(value.Peer); removedConnections.Add(connection.Key); } } foreach (ZRpc removedConnection in removedConnections) { Unregister(removedConnection); connections.Remove(removedConnection); } removedConnections.Clear(); } } internal sealed class RecipeRegistry : IDisposable { private sealed class Plan { internal ObjectDB DB; internal Recipe Source; internal ItemDrop Item; internal Requirement[] Costs; internal CraftingStation Station; internal bool Rod; } private readonly List<ObjectDB> databases = new List<ObjectDB>(); private readonly List<Recipe> created = new List<Recipe>(); private readonly Dictionary<Recipe, bool> previous = new Dictionary<Recipe, bool>(); private string lastError; internal void Register(ObjectDB db) { if (Object.op_Implicit((Object)(object)db) && !databases.Contains(db)) { databases.Add(db); } } private static ItemDrop Item(ObjectDB db, string name) { GameObject val = db.m_items.Find((GameObject p) => Object.op_Implicit((Object)(object)p) && ((Object)p).name == name); if (!Object.op_Implicit((Object)(object)val)) { return null; } return val.GetComponent<ItemDrop>(); } private static CraftingStation Workbench(ObjectDB db) { foreach (Recipe recipe in db.m_recipes) { if (Object.op_Implicit((Object)(object)recipe) && Object.op_Implicit((Object)(object)recipe.m_craftingStation) && ((Object)((Component)recipe.m_craftingStation).gameObject).name == "piece_workbench") { return recipe.m_craftingStation; } } ZNetScene instance = ZNetScene.instance; GameObject val = (Object.op_Implicit((Object)(object)instance) ? instance.GetPrefab("piece_workbench") : null); if (!Object.op_Implicit((Object)(object)val)) { return null; } return val.GetComponent<CraftingStation>(); } private static Requirement[] Costs(ObjectDB db, string recipe) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Expected O, but got Unknown List<MaterialCost> list = Materials.Parse(recipe); Requirement[] array = (Requirement[])(object)new Requirement[list.Count]; for (int i = 0; i < array.Length; i++) { ItemDrop val = Item(db, list[i].Item); if (!Object.op_Implicit((Object)(object)val)) { throw new FormatException("Unknown material prefab '" + list[i].Item + "'."); } array[i] = new Requirement { m_resItem = val, m_amount = list[i].Amount, m_amountPerLevel = 0, m_recover = true }; } return array; } private void AddPlans(List<Plan> plans, ObjectDB db, string itemName, string text, bool rod) { ItemDrop val = Item(db, itemName); if (!Object.op_Implicit((Object)(object)val)) { throw new FormatException("Unknown item prefab '" + itemName + "'."); } Requirement[] costs = Costs(db, text); bool flag = false; foreach (Recipe recipe in db.m_recipes) { if (Object.op_Implicit((Object)(object)recipe) && !created.Contains(recipe) && Object.op_Implicit((Object)(object)recipe.m_item) && !(((Object)recipe.m_item).name != itemName)) { plans.Add(new Plan { DB = db, Source = recipe, Item = val, Costs = costs, Rod = rod, Station = (rod ? Workbench(db) : recipe.m_craftingStation) }); flag = true; } } if (!flag) { plans.Add(new Plan { DB = db, Item = val, Costs = costs, Rod = rod, Station = Workbench(db) }); } if ((rod || !flag) && !Object.op_Implicit((Object)(object)Workbench(db))) { throw new FormatException("Workbench prefab is not ready."); } } internal void Apply(Settings s, bool ready) { bool flag = ready && s.Bait; bool flag2 = ready && s.Rods && s.Craft; if (!flag && !flag2) { Restore(); return; } Restore(); try { List<Plan> list = new List<Plan>(); databases.RemoveAll((ObjectDB db) => !Object.op_Implicit((Object)(object)db)); HashSet<List<Recipe>> hashSet = new HashSet<List<Recipe>>(); foreach (ObjectDB database in databases) { if (database.m_items == null || database.m_recipes == null || !Object.op_Implicit((Object)(object)Item(database, "FishingRod")) || !hashSet.Add(database.m_recipes)) { continue; } if (flag) { foreach (BaitDefinition value2 in s.Catalog.Baits.Values) { if (!string.IsNullOrWhiteSpace(value2.Recipe)) { AddPlans(list, database, value2.Name, value2.Recipe, rod: false); } } } if (flag2) { AddPlans(list, database, "FishingRod", s.RodRecipe, rod: true); } } foreach (Plan item in list) { Recipe val = (Object.op_Implicit((Object)(object)item.Source) ? Object.Instantiate<Recipe>(item.Source) : ScriptableObject.CreateInstance<Recipe>()); ((Object)val).name = "dbff_Recipe_" + ((Object)item.Item).name + "_" + created.Count; val.m_item = item.Item; val.m_resources = item.Costs; val.m_requireOnlyOneIngredient = false; bool flag3 = !Object.op_Implicit((Object)(object)item.Source) || item.Source.m_enabled; if (Object.op_Implicit((Object)(object)item.Source)) { if (previous.TryGetValue(item.Source, out var value)) { flag3 = value; } else { previous.Add(item.Source, flag3); } item.Source.m_enabled = false; } if (item.Rod) { val.m_amount = 1; val.m_minStationLevel = 2; val.m_craftingStation = (val.m_repairStation = item.Station); val.m_requireOnlyOneIngredient = false; } else if (!Object.op_Implicit((Object)(object)item.Source)) { val.m_amount = 20; val.m_minStationLevel = 1; val.m_craftingStation = (val.m_repairStation = item.Station); } val.m_enabled = item.Rod || flag3; created.Add(val); item.DB.m_recipes.Add(val); } lastError = null; } catch (Exception ex) { Restore(); string text = "Recipes were not updated; using original recipes until valid ingredients are available: " + ex.Message; if (text != lastError) { Plugin.Warn(text); } lastError = text; } } private void Restore() { foreach (ObjectDB database in databases) { if (Object.op_Implicit((Object)(object)database) && database.m_recipes != null) { database.m_recipes.RemoveAll((Recipe r) => created.Contains(r)); } } foreach (KeyValuePair<Recipe, bool> previou in previous) { if (Object.op_Implicit((Object)(object)previou.Key)) { previou.Key.m_enabled = previou.Value; } } foreach (Recipe item in created) { if (Object.op_Implicit((Object)(object)item)) { Object.Destroy((Object)(object)item); } } created.Clear(); previous.Clear(); } public void Dispose() { Restore(); databases.Clear(); } } internal sealed class MaterialCost { internal string Item; internal int Amount; } internal static class Materials { internal static List<MaterialCost> Parse(string text) { if (string.IsNullOrWhiteSpace(text) || text.Length > 16000) { throw new FormatException("Use Item1[amount],Item2[amount] with positive whole amounts."); } List<MaterialCost> list = new List<MaterialCost>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); string[] array = text.Split(','); foreach (string text2 in array) { string text3 = text2.Trim(); int num = text3.IndexOf('['); if (num <= 0 || !text3.EndsWith("]", StringComparison.Ordinal) || !int.TryParse(text3.Substring(num + 1, text3.Length - num - 2), NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < 1) { throw new FormatException("Invalid material '" + text2 + "'. Use Wood[20]."); } string text4 = text3.Substring(0, num).Trim(); BaitCatalog.ValidateName(text4); if (!hashSet.Add(text4)) { throw new FormatException("Duplicate material '" + text4 + "'."); } if (list.Count == 32) { throw new FormatException("At most 32 ingredients are allowed."); } list.Add(new MaterialCost { Item = text4, Amount = result }); } return list; } } internal static class BaitRules { private sealed class FishInfo { internal string Name; internal float Fallback = 1f; internal readonly Dictionary<string, float> Chances = new Dictionary<string, float>(StringComparer.Ordinal); } private static ConditionalWeakTable<Fish, FishInfo> cache = new ConditionalWeakTable<Fish, FishInfo>(); private static readonly FieldRef<Fish, List<BaitSetting>> nativeBaits = AccessTools.FieldRefAccess<Fish, List<BaitSetting>>("m_baits"); private static Dictionary<string, HashSet<string>> allowed = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal); private static BaitCatalog catalog = BaitCatalog.Empty; internal static void Configure(Settings s, bool ready) { catalog = ((ready && s.Bait) ? s.Catalog : BaitCatalog.Empty); allowed = catalog.BuildAllowed(s.Lower); cache = new ConditionalWeakTable<Fish, FishInfo>(); } internal static string PrefabName(GameObject obj) { if (!Object.op_Implicit((Object)(object)obj)) { return ""; } string name = ((Object)obj).name; int num = name.IndexOf("(Clone)", StringComparison.Ordinal); if (num >= 0) { return name.Substring(0, num); } return name; } private static FishInfo ReadFish(Fish fish) { FishInfo fishInfo = new FishInfo { Name = PrefabName(((Component)fish).gameObject) }; float num = 0f; List<BaitSetting> list = nativeBaits.Invoke(fish); if (list != null) { foreach (BaitSetting item in list) { if (item != null && Object.op_Implicit((Object)(object)item.m_bait)) { float num2 = Mathf.Clamp01(item.m_chance); string name = ((Object)item.m_bait).name; fishInfo.Chances.TryGetValue(name, out var value); fishInfo.Chances[name] = 1f - (1f - value) * (1f - num2); num = Mathf.Max(num, num2); } } } if (num > 0f) { fishInfo.Fallback = num; } return fishInfo; } internal static bool Test(Fish fish, FishingFloat ff, out bool result) { result = false; if (!Object.op_Implicit((Object)(object)fish) || !Object.op_Implicit((Object)(object)ff)) { return false; } string bait = ff.GetBait(); if (!allowed.TryGetValue(bait, out var value)) { return false; } FishInfo value2 = cache.GetValue(fish, ReadFish); if (!value.Contains(value2.Name)) { return true; } if (!value2.Chances.TryGetValue(bait, out var value3)) { value3 = value2.Fallback; } result = Random.value < value3; return true; } internal static int Required(ItemData item) { if (item == null || !Object.op_Implicit((Object)(object)item.m_dropPrefab)) { return 0; } if (!catalog.Baits.TryGetValue(((Object)item.m_dropPrefab).name, out var value)) { return 0; } return value.Level; } internal static bool CanUse(Humanoid player, ItemData bait, bool message) { if (!IsLocalPlayer(player)) { return true; } int num = Required(bait); if (num <= 0 || !Object.op_Implicit((Object)(object)player) || BaitCatalog.MeetsLevel(((Character)player).GetSkillLevel((SkillType)104), num)) { return true; } if (message) { ShowLockMessage(num); } return false; } internal static bool IsLocalPlayer(Humanoid player) { if (Object.op_Implicit((Object)(object)player)) { return (object)player == Player.m_localPlayer; } return false; } private static void ShowLockMessage(int level) { if (Object.op_Implicit((Object)(object)MessageHud.instance)) { MessageHud.instance.ShowMessage((MessageType)2, "Requires Fishing level " + level + " to use this bait.", 0, (Sprite)null, false, false); } } internal static bool IsFishingRod(ItemData item) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Invalid comparison between Unknown and I4 if (item?.m_shared != null) { if (!Object.op_Implicit((Object)(object)item.m_dropPrefab) || !(((Object)item.m_dropPrefab).name == "FishingRod")) { return (int)item.m_shared.m_skillType == 104; } return true; } return false; } internal static ItemData NativeAmmo(Humanoid player, ItemData weapon) { ItemData ammoItem = player.GetAmmoItem(); Inventory inventory = player.GetInventory(); string ammoType = weapon.m_shared.m_ammoType; if (ammoItem != null && inventory.ContainsItem(ammoItem) && ammoItem.m_shared.m_ammoType == ammoType) { return ammoItem; } return inventory.GetAmmoItem(ammoType, (string)null); } internal static ItemData UnlockedAmmo(Humanoid player, ItemData weapon) { ItemData val = NativeAmmo(player, weapon); if (!IsLocalPlayer(player) || val == null) { return val; } int num = Required(val); if (num <= 0) { return val; } float skillLevel = ((Character)player).GetSkillLevel((SkillType)104); if (BaitCatalog.MeetsLevel(skillLevel, num)) { return val; } foreach (ItemData allItem in player.GetInventory().GetAllItems()) { if (allItem.m_shared.m_ammoType == weapon.m_shared.m_ammoType && allItem.m_stack > 0 && BaitCatalog.MeetsLevel(skillLevel, Required(allItem))) { return allItem; } } return null; } internal static bool CanCast(Humanoid player, ItemData weapon) { if (!IsLocalPlayer(player) || !IsFishingRod(weapon)) { return true; } ItemData val = NativeAmmo(player, weapon); if (val == null || UnlockedAmmo(player, weapon) != null) { return true; } ShowLockMessage(Required(val)); return false; } } [HarmonyPatch(typeof(Fish), "TestBate")] internal static class BaitCatch { private static bool Prefix(Fish __instance, FishingFloat ff, ref bool __result) { if (!BaitRules.Test(__instance, ff, out var result)) { return true; } __result = result; return false; } } [HarmonyPatch(typeof(Humanoid), "EquipItem")] internal static class BaitEquip { private static bool Prefix(Humanoid __instance, ItemData item, ref bool __result) { if (BaitRules.CanUse(__instance, item, message: true)) { return true; } __result = false; return false; } } [HarmonyPatch(typeof(Attack), "FindAmmo")] internal static class BaitFindAmmo { private static void Postfix(Humanoid character, ItemData weapon, ref ItemData __result) { if (BaitRules.IsLocalPlayer(character) && BaitRules.IsFishingRod(weapon)) { __result = BaitRules.UnlockedAmmo(character, weapon); } } } [HarmonyPatch(typeof(Attack), "EquipAmmoItem")] internal static class BaitAutoEquip { private static bool Prefix(Humanoid character, ItemData weapon, ref bool __result) { if (!BaitRules.IsLocalPlayer(character) || !BaitRules.IsFishingRod(weapon)) { return true; } ItemData val = BaitRules.UnlockedAmmo(character, weapon); if (val == null) { ItemData val2 = BaitRules.NativeAmmo(character, weapon); if (val2 != null) { BaitRules.CanUse(character, val2, message: true); } __result = false; } else { __result = character.EquipItem(val, true); } return false; } } [HarmonyPatch(typeof(Attack), "StartDraw")] internal static class BaitDraw { private static bool Prefix(Humanoid character, ItemData weapon, ref bool __result) { if (BaitRules.CanCast(character, weapon)) { return true; } __result = false; return false; } } [HarmonyPatch(typeof(Humanoid), "StartAttack")] internal static class BaitCast { private static readonly FieldRef<Player, float> queuedAttack = AccessTools.FieldRefAccess<Player, float>("m_queuedAttackTimer"); private static bool Prefix(Humanoid __instance, bool secondaryAttack, ref bool __result) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown if (secondaryAttack || !BaitRules.IsLocalPlayer(__instance)) { return true; } if (BaitRules.CanCast(__instance, __instance.GetCurrentWeapon())) { return true; } queuedAttack.Invoke((Player)__instance) = 0f; __result = false; return false; } } internal static class TreasureRules { private static readonly FieldRef<Fish, DropTable> extra = AccessTools.FieldRefAccess<Fish, DropTable>("m_extraDrops"); private static readonly DropTable empty = new DropTable(); internal static bool IsVanillaFish(string name) { if (name == "Fish4_cave") { return true; } if (!name.StartsWith("Fish", StringComparison.Ordinal)) { return false; } if (int.TryParse(name.Substring(4), out var result) && result >= 1 && result <= 12) { return name == "Fish" + result; } return false; } internal static DropTable GetDrops(Fish fish) { if (!IsVanillaFish(BaitRules.PrefabName(((Component)fish).gameObject))) { return extra.Invoke(fish); } return empty; } } [HarmonyPatch(typeof(FishingFloat), "Catch")] internal static class RemoveTreasure { private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { FieldInfo field = AccessTools.Field(typeof(Fish), "m_extraDrops"); MethodInfo replace = AccessTools.Method(typeof(TreasureRules), "GetDrops", (Type[])null, (Type[])null); int changed = 0; foreach (CodeInstruction instruction in instructions) { if (CodeInstructionExtensions.LoadsField(instruction, field, false)) { instruction.opcode = OpCodes.Call; instruction.operand = replace; changed++; } yield return instruction; } if (changed == 0) { throw new MissingMethodException("Cannot locate the native fishing treasure table."); } } } internal static class DefaultBaits { internal const string Text = "# FishermansFixes\n# Server/host controls this file in multiplayer; reloads automatically while Bait is enabled.\n# Top-level keys and fish lists use case-sensitive console prefab names.\n# Section headings: 2 spaces. Dashed rows: 4 spaces, matching Food Knowledge style.\n# levelUnlock: Fishing skill 0-100; 0 is unrestricted. Temporary bonuses count.\n# recipe: \"\" keeps native recipes. Otherwise use Item1[amount],Item2[amount].\n# Existing recipes retain their station, station level and output amount.\n# A bait without a native recipe gets 20 bait per craft at a level 1 Workbench.\n# Lower Tier Progression order: Meadows, BlackForest, Swamp, Ocean, Mountain,\n# Plains, Mistlands, Ashlands, DeepNorth. Lists are inherited from lower biomes.\n# Custom bait must already exist in the game; specify biome to include it in progression.\n# An omitted bait or omitted fish section uses native fish rules. fish: [] disallows direct catches (lower tiers still apply).\n\nFishingBait:\n basics:\n - biome: Meadows\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish1\n - Fish2\n\nFishingBaitForest:\n basics:\n - biome: BlackForest\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish5\n\nFishingBaitSwamp:\n basics:\n - biome: Swamp\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish6\n\nFishingBaitOcean:\n basics:\n - biome: Ocean\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish3\n - Fish8\n\nFishingBaitCave:\n basics:\n - biome: Mountain\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish4\n - Fish4_cave\n\nFishingBaitPlains:\n basics:\n - biome: Plains\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish7\n\nFishingBaitMistlands:\n basics:\n - biome: Mistlands\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish9\n - Fish12\n\nFishingBaitAshlands:\n basics:\n - biome: Ashlands\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish11\n\nFishingBaitDeepNorth:\n basics:\n - biome: DeepNorth\n - levelUnlock: 0\n - recipe: \"\"\n fish:\n - Fish10\n\n"; } internal sealed class SectionPatches : IDisposable { private sealed class Group { private readonly Harmony harmony; private readonly Type[] types; private bool active; internal Group(string name, params Type[] patches) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown harmony = new Harmony("dannanboi.fishermansfixes." + name); types = patches; } internal void Set(bool enabled) { if (active == enabled) { return; } if (enabled) { try { Type[] array = types; foreach (Type type in array) { harmony.CreateClassProcessor(type).Patch(); } } catch { harmony.UnpatchSelf(); throw; } } else { harmony.UnpatchSelf(); } active = enabled; } } private readonly Group bait = new Group("Bait", typeof(BaitCatch)); private readonly Group locks = new Group("BaitLocks", typeof(BaitEquip), typeof(BaitFindAmmo), typeof(BaitAutoEquip), typeof(BaitDraw), typeof(BaitCast)); private readonly Group recipes = new Group("Recipes", typeof(DatabaseAwake), typeof(DatabaseCopy), typeof(SceneAwake)); private readonly Group treasure = new Group("Treasure", typeof(RemoveTreasure)); internal void Refresh(Settings s, bool ready) { bool flag = ready && s.Bait; bool flag2 = false; if (flag) { foreach (BaitDefinition value in s.Catalog.Baits.Values) { if (value.HasFish) { flag2 = true; break; } } } bait.Set(flag && flag2); bool flag3 = false; bool flag4 = false; if (flag) { foreach (BaitDefinition value2 in s.Catalog.Baits.Values) { if (value2.Level > 0) { flag3 = true; } if (value2.Recipe.Length != 0) { flag4 = true; } } } locks.Set((!Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsDedicated()) && flag && s.Lock && flag3); recipes.Set((flag && flag4) || (ready && s.Rods && s.Craft)); treasure.Set(ready && s.Treasure); } public void Dispose() { bait.Set(enabled: false); locks.Set(enabled: false); recipes.Set(enabled: false); treasure.Set(enabled: false); } } [HarmonyPatch(typeof(ObjectDB), "Awake")] internal static class DatabaseAwake { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(ObjectDB __instance) { Plugin.Instance?.Database(__instance); } } [HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")] internal static class DatabaseCopy { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(ObjectDB __instance) { Plugin.Instance?.Database(__instance); } } [HarmonyPatch(typeof(ZNetScene), "Awake")] internal static class SceneAwake { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix() { Plugin.Instance?.Database(ObjectDB.instance); } } internal sealed class BaitDefinition { internal string Name; internal string Recipe = ""; internal int Tier = -1; internal int Level; internal bool HasFish; internal readonly HashSet<string> Fish = new HashSet<string>(StringComparer.Ordinal); } internal sealed class BaitCatalog { internal const int MaxBytes = 131072; internal static readonly BaitCatalog Empty = new BaitCatalog(); internal readonly Dictionary<string, BaitDefinition> Baits = new Dictionary<string, BaitDefinition>(StringComparer.Ordinal); internal Dictionary<string, HashSet<string>> BuildAllowed(bool lower) { Dictionary<string, HashSet<string>> dictionary = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal); foreach (KeyValuePair<string, BaitDefinition> bait in Baits) { if (!bait.Value.HasFish) { continue; } HashSet<string> hashSet = new HashSet<string>(bait.Value.Fish, StringComparer.Ordinal); if (lower && bait.Value.Tier >= 0) { foreach (BaitDefinition value in Baits.Values) { if (value.Tier >= 0 && value.Tier < bait.Value.Tier) { hashSet.UnionWith(value.Fish); } } } dictionary.Add(bait.Key, hashSet); } return dictionary; } internal static bool MeetsLevel(float effective, int required) { if (required > 0) { if (!float.IsNaN(effective)) { return effective >= (float)required; } return false; } return true; } internal static int Tier(string biome) { switch ((biome ?? "").Replace(" ", "").ToLowerInvariant()) { case "meadows": return 0; case "blackforest": return 1; case "swamp": return 2; case "ocean": return 3; case "mountain": case "mountains": return 4; case "plains": return 5; case "mistlands": return 6; case "ashlands": return 7; case "deepnorth": return 8; default: throw new FormatException("Unknown biome '" + biome + "'."); } } internal static int NativeTier(string bait) { return bait switch { "FishingBait" => 0, "FishingBaitForest" => 1, "FishingBaitSwamp" => 2, "FishingBaitOcean" => 3, "FishingBaitCave" => 4, "FishingBaitPlains" => 5, "FishingBaitMistlands" => 6, "FishingBaitAshlands" => 7, "FishingBaitDeepNorth" => 8, _ => -1, }; } internal static void ValidateName(string s) { if (string.IsNullOrEmpty(s) || s.Length > 128) { throw new FormatException("Missing or oversized prefab name."); } foreach (char c in s) { if (!char.IsLetterOrDigit(c) && c != '_') { throw new FormatException("Invalid prefab name '" + s + "'."); } } } internal static BaitCatalog Parse(string text) { if (text == null || Encoding.UTF8.GetByteCount(text) > 131072) { throw new FormatException("baits.yaml exceeds 128 KiB."); } BaitCatalog baitCatalog = new BaitCatalog(); BaitDefinition baitDefinition = null; string text2 = null; HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); HashSet<string> hashSet2 = new HashSet<string>(StringComparer.Ordinal); string[] array = text.TrimStart('\ufeff').Replace("\r\n", "\n").Replace('\r', '\n') .Split('\n'); bool flag = false; bool flag2 = false; for (int i = 0; i < array.Length; i++) { try { string text3 = array[i]; if (text3.IndexOf('\t') >= 0) { throw new FormatException("Use spaces, not tabs."); } string text4 = Uncomment(text3).Trim(); if (text4.Length == 0) { continue; } int num = text3.Length - text3.TrimStart(' ').Length; if (text4 == "---" && num == 0 && !flag2) { flag2 = true; continue; } if ((text4 == "..." || text4 == "{}") && num == 0) { flag = true; continue; } if (flag) { throw new FormatException("Multiple documents or fields after document end are not supported."); } flag2 = true; if (num == 0) { int num2 = text4.IndexOf(':'); if (num2 < 1 || text4.Substring(num2 + 1).Trim().Length != 0) { throw new FormatException("Start each bait with PrefabName: followed by indented fields."); } string text5 = Scalar(text4.Substring(0, num2)); ValidateName(text5); if (baitCatalog.Baits.Count >= 256) { throw new FormatException("At most 256 baits are allowed."); } if (baitCatalog.Baits.ContainsKey(text5)) { throw new FormatException("Duplicate bait '" + text5 + "'."); } baitDefinition = new BaitDefinition { Name = text5, Tier = NativeTier(text5) }; baitCatalog.Baits.Add(text5, baitDefinition); hashSet.Clear(); hashSet2.Clear(); text2 = null; continue; } if (baitDefinition == null) { throw new FormatException("Start with a bait prefab name."); } switch (num) { case 2: { int num4 = text4.IndexOf(':'); if (num4 < 1) { throw new FormatException("Expected a field or section."); } string text8 = text4.Substring(0, num4).Trim(); string text9 = text4.Substring(num4 + 1).Trim(); if (text8 == "fish" || text8 == "basics") { if (!hashSet2.Add(text8)) { throw new FormatException("Duplicate section '" + text8 + "'."); } if (text9.Length != 0 && (!(text8 == "fish") || !(text9 == "[]"))) { throw new FormatException("Sections require indented rows; fish: [] is allowed."); } if (text8 == "fish") { baitDefinition.HasFish = true; } text2 = text8; } else { text2 = null; Field(baitDefinition, hashSet, text8, text9); } break; } case 4: if (text2 != null && text4.StartsWith("- ", StringComparison.Ordinal)) { string text6 = text4.Substring(2).Trim(); if (text2 == "fish") { string text7 = Scalar(text6); ValidateName(text7); if (baitDefinition.Fish.Count >= 512) { throw new FormatException("At most 512 fish per bait are allowed."); } if (!baitDefinition.Fish.Add(text7)) { throw new FormatException("Duplicate fish '" + text7 + "'."); } } else { int num3 = text6.IndexOf(':'); if (num3 <= 0) { throw new FormatException("Expected - field: value."); } Field(baitDefinition, hashSet, text6.Substring(0, num3).Trim(), text6.Substring(num3 + 1).Trim()); } break; } goto default; default: throw new FormatException("Use 2 spaces for sections and 4 spaces for dashed rows."); } } catch (FormatException ex) { throw new FormatException("baits.yaml:" + (i + 1) + ": " + ex.Message); } } return baitCatalog; } private static void Field(BaitDefinition bait, HashSet<string> seen, string key, string raw) { if (!seen.Add(key)) { throw new FormatException("Duplicate field '" + key + "'."); } string text = Scalar(raw); switch (key) { case "biome": bait.Tier = Tier(text); break; case "recipe": bait.Recipe = text; if (text.Length != 0) { Materials.Parse(text); } break; case "levelUnlock": if (!int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out bait.Level) || bait.Level < 0 || bait.Level > 100) { throw new FormatException("levelUnlock must be a whole number from 0 to 100."); } break; default: throw new FormatException("Unknown field '" + key + "'. Use biome, recipe, levelUnlock, or fish."); } } private static string Uncomment(string s) { char c = '\0'; for (int i = 0; i < s.Length; i++) { char c2 = s[i]; if (c != 0) { if (c2 == '\\' && c == '"') { i++; } else if (c2 == c) { if (c == '\'' && i + 1 < s.Length && s[i + 1] == '\'') { i++; } else { c = '\0'; } } continue; } switch (c2) { case '"': case '\'': c = c2; break; case '#': if (i == 0 || char.IsWhiteSpace(s[i - 1])) { return s.Substring(0, i); } break; } } return s; } private static string Scalar(string s) { s = s.Trim(); if (s == "null" || s == "~") { return ""; } if (s.Length == 0) { return s; } if (s[0] == '\'' || s[0] == '"') { char c = s[0]; if (s.Length < 2 || s[s.Length - 1] != c) { throw new FormatException("Unclosed quote."); } string text = s.Substring(1, s.Length - 2); if (c == '\'') { return text.Replace("''", "'"); } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < text.Length; i++) { char c2 = text[i]; switch (c2) { case '\\': if (++i == text.Length) { throw new FormatException("Incomplete escape."); } c2 = text[i]; if (c2 != '\\' && c2 != '"') { throw new FormatException("Only escaped quotes and backslashes are supported."); } break; case '"': throw new FormatException("Escape embedded quotes."); } stringBuilder.Append(c2); } return stringBuilder.ToString(); } if ("[{&*!%@`".IndexOf(s[0]) >= 0 || s.Contains(": ")) { throw new FormatException("Unsupported scalar; use plain or quoted values."); } return s; } } internal sealed class Settings { internal const string DefaultRodRecipe = "Wood[20],Resin[5]"; internal const int MaxPacket = 262144; internal readonly bool Bait; internal readonly bool Lower; internal readonly bool Lock; internal readonly bool Rods; internal readonly bool Craft; internal readonly bool Treasure; internal readonly string RodRecipe; internal readonly string Yaml; internal readonly BaitCatalog Catalog; internal Settings(bool bait, bool lower, bool levelLock, bool rods, bool craft, string rodRecipe, bool treasure, string yaml = "") { if (rodRecipe == null || rodRecipe.Length > 16000) { throw new FormatException("Fishingrod Recipe is too long or missing."); } if (rods && craft) { Materials.Parse(rodRecipe); } Bait = bait; Lower = lower; Lock = levelLock; Rods = rods; Craft = craft; RodRecipe = rodRecipe; Treasure = treasure; Yaml = (bait ? (yaml ?? "") : ""); Catalog = ((bait && Yaml.Length != 0) ? BaitCatalog.Parse(Yaml) : BaitCatalog.Empty); } internal Settings WithYaml(string yaml) { return new Settings(Bait, Lower, Lock, Rods, Craft, RodRecipe, Treasure, yaml); } internal bool Same(Settings s) { if (s != null && Bait == s.Bait && Lower == s.Lower && Lock == s.Lock && Rods == s.Rods && Craft == s.Craft && RodRecipe == s.RodRecipe && Treasure == s.Treasure) { return Yaml == s.Yaml; } return false; } internal bool SamePacket(Settings s) { if (s != null && Bait == s.Bait && (!Bait || (Lower == s.Lower && Lock == s.Lock && Yaml == s.Yaml)) && Rods == s.Rods && (!Rods || (Craft == s.Craft && (!Craft || RodRecipe == s.RodRecipe)))) { return Treasure == s.Treasure; } return false; } internal void Write(ZPackage p) { p.Write(Bait); if (Bait) { p.Write(Lower); p.Write(Lock); p.Write(Yaml); } p.Write(Rods); if (Rods) { p.Write(Craft); if (Craft) { p.Write(RodRecipe); } } p.Write(Treasure); } internal static Settings Read(ZPackage p) { bool num = p.ReadBool(); bool lower = true; bool levelLock = false; string yaml = ""; if (num) { lower = p.ReadBool(); levelLock = p.ReadBool(); yaml = p.ReadString(); } bool flag = p.ReadBool(); bool flag2 = true; string rodRecipe = "Wood[20],Resin[5]"; if (flag) { flag2 = p.ReadBool(); if (flag2) { rodRecipe = p.ReadString(); } } return new Settings(num, lower, levelLock, flag, flag2, rodRecipe, p.ReadBool(), yaml); } } internal sealed class ConfigurationManagerAttributes { public bool? ReadOnly; public int? Order; public bool? ShowRangeAsPercent; } internal sealed class LiveConfig : IDisposable { internal sealed class SaveScope { private LiveConfig owner; internal SaveScope(LiveConfig value) { owner = value; } internal void Finish() { if (owner == null) { return; } LiveConfig liveConfig = owner; owner = null; try { liveConfig.ApplyValues(liveConfig.Current); } finally { liveConfig.applying--; } } } internal sealed class ReloadScope { private LiveConfig owner; private readonly Settings before; internal ReloadScope(LiveConfig value, Settings previous) { owner = value; before = previous; } internal void Finish(Exception error) { if (owner == null) { return; } LiveConfig liveConfig = owner; owner = null; try { try { liveConfig.Local = ((error == null) ? liveConfig.Capture() : before); } catch (FormatException ex) { liveConfig.Local = before; Plugin.Warn("Config reload not applied: " + ex.Message); } liveConfig.ApplyValues(liveConfig.Current); } finally { liveConfig.applying--; } liveConfig.Refresh?.Invoke(); if (!liveConfig.Client && !liveConfig.Local.Same(before)) { liveConfig.AuthorityChanged?.Invoke(); } } } internal static LiveConfig Instance; internal readonly ConfigFile File; internal readonly ConfigEntry<bool> BaitSection; internal readonly ConfigEntry<bool> LowerTiers; internal readonly ConfigEntry<bool> LevelLock; internal readonly ConfigEntry<bool> RodSection; internal readonly ConfigEntry<bool> CraftRod; internal readonly ConfigEntry<bool> RemoveTreasure; internal readonly ConfigEntry<string> RodRecipe; internal Settings Local; internal Settings Remote; private int applying; private readonly bool autoSave; private bool disposed; private readonly FileInfo configInfo; private DateTime observedWrite; private long observedLength; private float nextPoll; private float reloadAt; private bool filePending; private string fileError; private readonly List<ConfigurationManagerAttributes> tags = new List<ConfigurationManagerAttributes>(); internal bool Client { get; private set; } internal bool Ready { get { if (Client) { return Remote != null; } return true; } } internal Settings Current { get { if (!Client || Remote == null) { return Local; } return Remote; } } internal event Action AuthorityChanged; internal event Action Refresh; private ConfigDescription Describe(string text, int order, AcceptableValueBase acceptable = null) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown ConfigurationManagerAttributes configurationManagerAttributes = new ConfigurationManagerAttributes { Order = order, ShowRangeAsPercent = false }; tags.Add(configurationManagerAttributes); return new ConfigDescription(text, acceptable, new object[1] { configurationManagerAttributes }); } internal LiveConfig(ConfigFile file) { File = file; configInfo = new FileInfo(file.ConfigFilePath); autoSave = file.SaveOnConfigSet; file.SaveOnConfigSet = false; BaitSection = file.Bind<bool>("Bait", "Enable Section", true, Describe("Enable bait fish lists, recipes, progression and skill locks. False removes all bait gameplay hooks and stops YAML loading/polling/sync. Config and multiplayer authority essentials remain. Server-synced and locked in multiplayer.", 100)); LowerTiers = file.Bind<bool>("Bait", "Lower Tier Progression", true, Describe("Higher tier bait can catch fish listed under every lower biome in baits.yaml. Order: Meadows, BlackForest, Swamp, Ocean, Mountain, Plains, Mistlands, Ashlands, DeepNorth. Server-synced and locked in multiplayer.", 90)); LevelLock = file.Bind<bool>("Bait", "Level lock bait", false, Describe("Require the bait's levelUnlock Fishing skill to equip, begin charging and cast. Checks and lock messages are entirely local to the controlling client, before draw/cast animations and bait consumption. Effective skill includes equipment, buffs and temporary bonuses; there are no added fishing RPCs or idle skill polling. 0 is unrestricted. Server-synced and locked in multiplayer.", 80)); RodSection = file.Bind<bool>("Fishing Rods", "Enable Section", true, Describe("Enable fishing rod recipes. False removes recipe registration hooks and restores previous recipes. Config and multiplayer authority essentials remain. Server-synced and locked in multiplayer.", 100)); CraftRod = file.Bind<bool>("Fishing Rods", "Craftable Fishingrod", true, Describe("Craft FishingRod at a level 2 Workbench. Existing rod recipes are replaced while enabled and restored when disabled. Server-synced and locked in multiplayer.", 90)); RodRecipe = file.Bind<string>("Fishing Rods", "Fishingrod Recipe", "Wood[20],Resin[5]", Describe("Materials using exact prefab names: Item1[amount],Item2[amount]. Positive whole amounts, at most 32 unique ingredients. Default: Wood[20],Resin[5]. Server-synced and locked in multiplayer.", 80)); RemoveTreasure = file.Bind<bool>("Misc", "Remove default treasure", false, Describe("Remove the extra-drop tables on vanilla Fish1–Fish12 and Fish4_cave when caught by fishing, including amber, stone and other biome loot. The fish itself still drops. Custom fish are unaffected. Server-synced and locked in multiplayer.", 100)); try { Local = Capture(); } catch (FormatException ex) { Plugin.Warn("Invalid settings; using safe recipe defaults: " + ex.Message); Local = new Settings(BaitSection.Value, LowerTiers.Value, LevelLock.Value, RodSection.Value, craft: false, "Wood[20],Resin[5]", RemoveTreasure.Value); ApplyValues(Local); } file.Save(); RememberFile(); file.SettingChanged += Changed; Instance = this; } internal void RememberFile() { configInfo.Refresh(); if (configInfo.Exists) { observedWrite = configInfo.LastWriteTimeUtc; observedLength = configInfo.Length; filePending = false; } } internal void PollFile(float now) { if (disposed || now < nextPoll) { return; } nextPoll = now + 1f; try { configInfo.Refresh(); if (!configInfo.Exists) { return; } if (configInfo.LastWriteTimeUtc != observedWrite || configInfo.Length != observedLength) { observedWrite = configInfo.LastWriteTimeUtc; observedLength = configInfo.Length; filePending = true; reloadAt = now + 0.25f; } else { if (!filePending || now < reloadAt) { return; } ReloadScope reloadScope = BeforeReload(); Exception error = null; try { File.Reload(); } catch (Exception ex) { error = ex; throw; } finally { reloadScope?.Finish(error); } RememberFile(); fileError = null; } } catch (Exception ex2) { if (fileError != ex2.Message) { Plugin.Warn("Config file reload not applied: " + ex2.Message); } fileError = ex2.Message; reloadAt = now + 1f; } } private Settings Capture() { return new Settings(BaitSection.Value, LowerTiers.Value, LevelLock.Value, RodSection.Value, CraftRod.Value, RodRecipe.Value, RemoveTreasure.Value, Local?.Yaml ?? ""); } private void ApplyValues(Settings values) { BaitSection.Value = values.Bait; LowerTiers.Value = values.Lower; LevelLock.Value = values.Lock; RodSection.Value = values.Rods; CraftRod.Value = values.Craft; RodRecipe.Value = values.RodRecipe; RemoveTreasure.Value = values.Treasure; } internal void SetYaml(string text) { if (!Client && !(text == Local.Yaml)) { Local = Local.WithYaml(text); this.Refresh?.Invoke(); this.AuthorityChanged?.Invoke(); } } private void Overlay() { applying++; try { ApplyValues(Current); } finally { applying--; } foreach (ConfigurationManagerAttributes tag in tags) { tag.ReadOnly = Client; } this.Refresh?.Invoke(); } internal void SetClient(bool client) { if (Client != client) { Client = client; Remote = null; Overlay(); } } internal void WaitForServer() { if (Client) { Remote = null; Overlay(); } } internal void Receive(Settings values) { if (Client && values != null) { Remote = values; Overlay(); } } private void Changed(object sender, SettingChangedEventArgs args) { if (applying != 0 || disposed) { return; } if (Client) { Overlay(); return; } Settings local = Local; Settings local2; try { local2 = Capture(); } catch (FormatException ex) { Plugin.Warn("Config edit not applied: " + ex.Message); Overlay(); return; } Local = local2; Overlay(); if (!Client && !Local.Same(local)) { this.AuthorityChanged?.Invoke(); } try { File.Save(); RememberFile(); } catch (Exception ex2) { Plugin.Warn("Could not save local config: " + ex2.Message); } } internal SaveScope BeforeSave() { if (applying != 0 || !Client || Remote == null) { return null; } applying++; try { ApplyValues(Local); return new SaveScope(this); } catch { applying--; throw; } } internal ReloadScope BeforeReload() { if (applying != 0) { return null; } applying++; try { ApplyValues(Local); return new ReloadScope(this, Local); } catch { applying--; throw; } } public void Dispose() { if (!disposed) { disposed = true; File.SettingChanged -= Changed; Client = false; Remote = null; Overlay(); File.SaveOnConfigSet = autoSave; if (Instance == this) { Instance = null; } } } } [HarmonyPatch(typeof(ConfigFile), "Save")] internal static class SaveLocalPreferences { private static void Prefix(ConfigFile __instance, out LiveConfig.SaveScope __state) { LiveConfig instance = LiveConfig.Instance; __state = ((instance != null && instance.File == __instance) ? instance.BeforeSave() : null); } private static void Postfix(ConfigFile __instance, LiveConfig.SaveScope __state) { __state?.Finish(); LiveConfig instance = LiveConfig.Instance; if (instance != null && instance.File == __instance) { instance.RememberFile(); } } private static Exception Finalizer(Exception __exception, LiveConfig.SaveScope __state) { __state?.Finish(); return __exception; } } [HarmonyPatch(typeof(ConfigFile), "Reload")] internal static class ReloadLocalPreferences { private static void Prefix(ConfigFile __instance, out LiveConfig.ReloadScope __state) { LiveConfig instance = LiveConfig.Instance; __state = ((instance != null && instance.File == __instance) ? instance.BeforeReload() : null); } private static void Postfix(LiveConfig.ReloadScope __state) { __state?.Finish(null); } private static Exception Finalizer(Exception __exception, LiveConfig.ReloadScope __state) { __state?.Finish(__exception); return __exception; } } }