Decompiled source of FlexibleTraders v1.0.5

dannanboi_FlexibleTraders.dll

Decompiled a week ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("FlexibleTraders")]
[assembly: AssemblyDescription("YAML trader stock, custom currencies, unit sales and price tooltips.")]
[assembly: AssemblyCompany("Dannan")]
[assembly: AssemblyFileVersion("1.0.5")]
[assembly: AssemblyInformationalVersion("1.0.5")]
[assembly: AssemblyVersion("1.0.5.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 FlexibleTraders
{
	internal sealed class Entry
	{
		public string Item;

		public string Currency;

		public string SellCurrency;

		public string Description;

		public int Amount;

		public int Price;

		public int UnitSellPrice;

		public bool CanSell;

		public string[] RequiredGlobalKeys = new string[0];

		public string[] ForbiddenGlobalKeys = new string[0];

		public KeyMatch RequiredGlobalKeysMode;

		public string PayoutCurrency
		{
			get
			{
				if (!string.IsNullOrEmpty(SellCurrency))
				{
					return SellCurrency;
				}
				return Currency;
			}
		}
	}
	internal sealed class Catalog
	{
		public const int MaxBytes = 262144;

		public const int MaxEntries = 1024;

		public static readonly string[] Traders = new string[3] { "Haldor", "Hildir", "BogWitch" };

		public readonly List<string> TraderOrder = new List<string>();

		public readonly Dictionary<string, List<Entry>> Stock = new Dictionary<string, List<Entry>>(StringComparer.OrdinalIgnoreCase);

		private readonly Dictionary<string, Dictionary<string, Entry>> index = new Dictionary<string, Dictionary<string, Entry>>(StringComparer.OrdinalIgnoreCase);

		public const string Example = "# FlexibleTraders inventory conditions (1.0.5)\n# Spawn prefab names are case-sensitive. Price buys Amount; UnitSellPrice pays for ONE item.\n# Empty, \"\", null, ~, or omitted fields use defaults. Empty Item entries are ignored.\n# Empty Description uses the original tooltip; empty Currency uses Coins.\n# Empty Amount, Price, UnitSellPrice use 0; empty CanSell uses false.\n# Selling requires CanSell: true and a positive UnitSellPrice.\n# Currency is the purchase currency. SellCurrency is the sale payout currency.\n# SellCurrency: empty, \"\", or omitted defaults to Currency. Save to reload automatically.\n# Amount: 0 makes an item sell-only: no purchase listing, including matching native stock.\n# Set CanSell: true and UnitSellPrice for sales; Price is ignored when Amount is 0.\n# Optional purchase conditions (selling still uses CanSell / UnitSellPrice):\n#   RequiredGlobalKeys: defeated_gdking,defeated_serpent\n#   RequiredGlobalKeysMode: All   # All or Any; default All\n#   ForbiddenGlobalKeys: defeated_bonemass\n# Empty keys impose no restriction, including RequiredGlobalKeysMode: Any.\n# Empty RequiredGlobalKeysMode uses All.\n# ANY forbidden key hides the item, even if all required conditions are met.\n# Keys use comma-separated names: key1,key2,key3 (spaces are optional).\n# Legacy [key1, key2] lists and quoted names remain accepted.\n# Keys are checked against the world's existing global keys; no personal progress is saved.\n# In multiplayer the server owns these rules and rechecks purchases. Client YAML is ignored.\n# Unknown but syntactically valid names are accepted (for custom keys); check spelling carefully.\n#\n# Vanilla keys used directly by trader inventories:\n# defeated_gdking          - The Elder defeated (Haldor / Bog Witch stock).\n# defeated_dragon          - Moder defeated (Haldor / Bog Witch stock).\n# defeated_goblinking      - Yagluth defeated (Haldor / Bog Witch stock).\n# defeated_queen           - The Queen defeated (Haldor / Bog Witch stock).\n# defeated_fader           - Fader defeated (Bog Witch stock).\n# defeated_serpent         - A Serpent killed (Bog Witch stock).\n# Hildir1                  - Hildir's Brass Chest returned.\n# Hildir2                  - Hildir's Silver Chest returned.\n# Hildir3                  - Hildir's Bronze Chest returned.\n#\n# Other built-in progression keys usable by the same checks (no extra tracking):\n# defeated_eikthyr         - Eikthyr defeated.\n# defeated_bonemass        - Bonemass defeated.\n# KilledTroll              - A Troll killed.\n# killed_surtling          - A Surtling killed.\n# KilledBat                - A Bat killed.\n# BossHildir1              - Brenna defeated; distinct from returning her chest.\n# BossHildir2              - Geirrhafa defeated; distinct from returning the chest.\n# BossHildir3              - Zil & Thungr encounter defeat; distinct from chest return.\n# Key names are normalized to lowercase; use these spellings in your configuration.\n# Deep North keys are intentionally omitted to avoid spoilers. Look them up online yourself if needed.\n# Version note: vendor mappings verified from Valheim 1.0.12 extracted data.\n# Older game builds may never set newer keys. Internal/legacy keys are not listed here.\n#\n# Examples:\n# Elder defeated, until Bonemass: RequiredGlobalKeys: defeated_gdking\n#                               ForbiddenGlobalKeys: defeated_bonemass\n# Any Hildir chest: RequiredGlobalKeys: Hildir1,Hildir2,Hildir3\n#                  RequiredGlobalKeysMode: Any\n# Leave both lists empty to preserve the original always-available custom stock.\n\nHaldor:\n  - Item: Wood\n    Description:\n    Amount: 20\n    Currency: Coins\n    Price: 20\n    CanSell: false\n    SellCurrency:\n    UnitSellPrice: 1\n    RequiredGlobalKeys:\n    RequiredGlobalKeysMode: All\n    ForbiddenGlobalKeys:\nHildir: []\nBogWitch: []\n";

		private static readonly Regex PrefabName = new Regex("^[A-Za-z0-9_\\-\\.]+$", RegexOptions.CultureInvariant);

		public Entry Find(string trader, string item)
		{
			if (trader == null || item == null || !index.TryGetValue(trader, out var value) || !value.TryGetValue(item, out var value2))
			{
				return null;
			}
			return value2;
		}

		public Catalog()
		{
			string[] traders = Traders;
			foreach (string key in traders)
			{
				Stock.Add(key, new List<Entry>());
			}
		}

		internal static string Compact(string yaml)
		{
			StringBuilder stringBuilder = new StringBuilder();
			string[] array = yaml.TrimStart('\ufeff').Replace("\r", "").Split('\n');
			for (int i = 0; i < array.Length; i++)
			{
				string value = YamlScalar.Uncomment(array[i]).TrimEnd();
				if (!string.IsNullOrWhiteSpace(value))
				{
					if (stringBuilder.Length > 0)
					{
						stringBuilder.Append('\n');
					}
					stringBuilder.Append(value);
				}
			}
			return stringBuilder.ToString();
		}

		private static int Number(Dictionary<string, string> fields, string key, int min, int max)
		{
			if (!fields.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
			{
				return 0;
			}
			if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < min || result > max)
			{
				throw new FormatException(key + " must be an integer from " + min + " to " + max + ".");
			}
			return result;
		}

		private static string Name(Dictionary<string, string> fields, string key, string fallback = null)
		{
			if (!fields.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
			{
				return fallback;
			}
			if (value.Length > 128 || !PrefabName.IsMatch(value))
			{
				throw new FormatException(key + " must be a spawn prefab name.");
			}
			return value;
		}

		public static Catalog Parse(string yaml)
		{
			if (yaml == null || Encoding.UTF8.GetByteCount(yaml) > 262144)
			{
				throw new FormatException("YAML exceeds 256 KiB.");
			}
			Catalog result = new Catalog();
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			Dictionary<string, string> row = null;
			string trader = null;
			int total = 0;
			int num = 0;
			Action action = delegate
			{
				if (row != null)
				{
					string item2 = Name(row, "Item");
					if (item2 == null)
					{
						row = null;
					}
					else
					{
						string text5 = Name(row, "Currency", "Coins");
						bool result2 = false;
						if (row.TryGetValue("CanSell", out var value) && !string.IsNullOrWhiteSpace(value))
						{
							if (string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase))
							{
								result2 = true;
							}
							else if (string.Equals(value, "no", StringComparison.OrdinalIgnoreCase))
							{
								result2 = false;
							}
							else if (!bool.TryParse(value, out result2))
							{
								throw new FormatException("CanSell must be yes/no or true/false.");
							}
						}
						Entry entry = new Entry
						{
							Item = item2,
							Currency = text5,
							Amount = Number(row, "Amount", 0, 10000),
							Price = Number(row, "Price", 0, 1000000),
							CanSell = result2,
							UnitSellPrice = Number(row, "UnitSellPrice", 0, 1000000),
							SellCurrency = Name(row, "SellCurrency")
						};
						if (row.TryGetValue("Description", out var value2))
						{
							if (value2.Length > 4096)
							{
								throw new FormatException("Description exceeds 4096 characters.");
							}
							entry.Description = (string.IsNullOrWhiteSpace(value2) ? null : value2);
						}
						if (row.TryGetValue("RequiredGlobalKeys", out var value3))
						{
							entry.RequiredGlobalKeys = PurchaseConditions.ParseKeys(value3, "RequiredGlobalKeys");
						}
						if (row.TryGetValue("ForbiddenGlobalKeys", out var value4))
						{
							entry.ForbiddenGlobalKeys = PurchaseConditions.ParseKeys(value4, "ForbiddenGlobalKeys");
						}
						if (row.TryGetValue("RequiredGlobalKeysMode", out var value5))
						{
							entry.RequiredGlobalKeysMode = PurchaseConditions.ParseMode(value5);
						}
						if (entry.Amount > 0 && item2 == text5)
						{
							throw new FormatException("Item and Currency must differ for purchases.");
						}
						if (entry.CanSell && entry.UnitSellPrice > 0 && item2 == entry.PayoutCurrency)
						{
							throw new FormatException("Item and SellCurrency must differ for sales.");
						}
						if (result.Stock[trader].Exists((Entry e) => e.Item == item2))
						{
							throw new FormatException("Duplicate item " + item2 + " for " + trader + ".");
						}
						if (++total > 1024)
						{
							throw new FormatException("At most 1024 entries are supported.");
						}
						result.Stock[trader].Add(entry);
						row = null;
					}
				}
			};
			try
			{
				string[] array = yaml.TrimStart('\ufeff').Replace("\r", "").Split('\n');
				foreach (string obj in array)
				{
					num++;
					if (obj.IndexOf('\t') >= 0)
					{
						throw new FormatException("Indent with spaces, not tabs.");
					}
					string text = YamlScalar.Uncomment(obj).TrimEnd();
					string text2 = text.TrimStart();
					if (text2.Length == 0)
					{
						continue;
					}
					int num3 = text.Length - text2.Length;
					if (text2 == "---" && hashSet.Count == 0)
					{
						continue;
					}
					if (num3 == 0)
					{
						action();
						int num4 = text2.IndexOf(':');
						if (num4 < 1)
						{
							throw new FormatException("Expected Haldor:, Hildir: or BogWitch:.");
						}
						trader = text2.Substring(0, num4);
						string text3 = text2.Substring(num4 + 1).Trim();
						if (!result.Stock.ContainsKey(trader) || !hashSet.Add(trader))
						{
							throw new FormatException("Unknown or duplicate trader: " + trader);
						}
						result.TraderOrder.Add(trader);
						if (text3 != "" && text3 != "[]")
						{
							if (!string.IsNullOrWhiteSpace(YamlScalar.Read(text3, allowEmpty: true)))
							{
								throw new FormatException("Trader section must contain an indented list or be empty.");
							}
							trader = null;
						}
						if (text3 == "[]")
						{
							trader = null;
						}
						continue;
					}
					if (trader == null)
					{
						throw new FormatException("Item appears outside a trader list.");
					}
					if (num3 == 2 && text2.StartsWith("- ", StringComparison.Ordinal))
					{
						action();
						row = new Dictionary<string, string>(StringComparer.Ordinal);
						text2 = text2.Substring(2).TrimStart();
					}
					else if (num3 != 4 || row == null)
					{
						throw new FormatException("Use two spaces before '- Item:' and four before its fields.");
					}
					int num5 = text2.IndexOf(':');
					if (num5 < 1)
					{
						throw new FormatException("Expected key: value.");
					}
					string text4 = text2.Substring(0, num5).Trim();
					if (text4 == "description")
					{
						text4 = "Description";
					}
					if (text4 == "SellPrice")
					{
						text4 = "UnitSellPrice";
					}
					bool flag = text4 == "RequiredGlobalKeys" || text4 == "ForbiddenGlobalKeys";
					if (text4 != "Item" && text4 != "Currency" && text4 != "SellCurrency" && text4 != "Amount" && text4 != "Price" && text4 != "CanSell" && text4 != "UnitSellPrice" && text4 != "Description" && !flag && text4 != "RequiredGlobalKeysMode")
					{
						throw new FormatException("Unknown field: " + text4);
					}
					if (row.ContainsKey(text4))
					{
						throw new FormatException("Duplicate field: " + text4);
					}
					row.Add(text4, flag ? text2.Substring(num5 + 1).Trim() : YamlScalar.Read(text2.Substring(num5 + 1), allowEmpty: true));
				}
				action();
				array = Traders;
				foreach (string item in array)
				{
					if (!hashSet.Contains(item))
					{
						result.TraderOrder.Add(item);
					}
				}
				foreach (KeyValuePair<string, List<Entry>> item3 in result.Stock)
				{
					Dictionary<string, Entry> dictionary = new Dictionary<string, Entry>(StringComparer.Ordinal);
					foreach (Entry item4 in item3.Value)
					{
						dictionary.Add(item4.Item, item4);
					}
					result.index.Add(item3.Key, dictionary);
				}
				return result;
			}
			catch (FormatException ex)
			{
				throw new FormatException("YAML line " + num + ": " + ex.Message);
			}
		}
	}
	internal static class CatalogComments
	{
		internal const string Header = "# FlexibleTraders inventory conditions (1.0.5)\n# Spawn prefab names are case-sensitive. Price buys Amount; UnitSellPrice pays for ONE item.\n# Empty, \"\", null, ~, or omitted fields use defaults. Empty Item entries are ignored.\n# Empty Description uses the original tooltip; empty Currency uses Coins.\n# Empty Amount, Price, UnitSellPrice use 0; empty CanSell uses false.\n# Selling requires CanSell: true and a positive UnitSellPrice.\n# Currency is the purchase currency. SellCurrency is the sale payout currency.\n# SellCurrency: empty, \"\", or omitted defaults to Currency. Save to reload automatically.\n# Amount: 0 makes an item sell-only: no purchase listing, including matching native stock.\n# Set CanSell: true and UnitSellPrice for sales; Price is ignored when Amount is 0.\n# Optional purchase conditions (selling still uses CanSell / UnitSellPrice):\n#   RequiredGlobalKeys: defeated_gdking,defeated_serpent\n#   RequiredGlobalKeysMode: All   # All or Any; default All\n#   ForbiddenGlobalKeys: defeated_bonemass\n# Empty keys impose no restriction, including RequiredGlobalKeysMode: Any.\n# Empty RequiredGlobalKeysMode uses All.\n# ANY forbidden key hides the item, even if all required conditions are met.\n# Keys use comma-separated names: key1,key2,key3 (spaces are optional).\n# Legacy [key1, key2] lists and quoted names remain accepted.\n# Keys are checked against the world's existing global keys; no personal progress is saved.\n# In multiplayer the server owns these rules and rechecks purchases. Client YAML is ignored.\n# Unknown but syntactically valid names are accepted (for custom keys); check spelling carefully.\n#\n# Vanilla keys used directly by trader inventories:\n# defeated_gdking          - The Elder defeated (Haldor / Bog Witch stock).\n# defeated_dragon          - Moder defeated (Haldor / Bog Witch stock).\n# defeated_goblinking      - Yagluth defeated (Haldor / Bog Witch stock).\n# defeated_queen           - The Queen defeated (Haldor / Bog Witch stock).\n# defeated_fader           - Fader defeated (Bog Witch stock).\n# defeated_serpent         - A Serpent killed (Bog Witch stock).\n# Hildir1                  - Hildir's Brass Chest returned.\n# Hildir2                  - Hildir's Silver Chest returned.\n# Hildir3                  - Hildir's Bronze Chest returned.\n#\n# Other built-in progression keys usable by the same checks (no extra tracking):\n# defeated_eikthyr         - Eikthyr defeated.\n# defeated_bonemass        - Bonemass defeated.\n# KilledTroll              - A Troll killed.\n# killed_surtling          - A Surtling killed.\n# KilledBat                - A Bat killed.\n# BossHildir1              - Brenna defeated; distinct from returning her chest.\n# BossHildir2              - Geirrhafa defeated; distinct from returning the chest.\n# BossHildir3              - Zil & Thungr encounter defeat; distinct from chest return.\n# Key names are normalized to lowercase; use these spellings in your configuration.\n# Deep North keys are intentionally omitted to avoid spoilers. Look them up online yourself if needed.\n# Version note: vendor mappings verified from Valheim 1.0.12 extracted data.\n# Older game builds may never set newer keys. Internal/legacy keys are not listed here.\n#\n# Examples:\n# Elder defeated, until Bonemass: RequiredGlobalKeys: defeated_gdking\n#                               ForbiddenGlobalKeys: defeated_bonemass\n# Any Hildir chest: RequiredGlobalKeys: Hildir1,Hildir2,Hildir3\n#                  RequiredGlobalKeysMode: Any\n# Leave both lists empty to preserve the original always-available custom stock.\n\n";

		internal static string Add(string yaml)
		{
			if (yaml.StartsWith("# FlexibleTraders inventory conditions (1.0.5)\n# Spawn prefab names are case-sensitive. Price buys Amount; UnitSellPrice pays for ONE item.\n# Empty, \"\", null, ~, or omitted fields use defaults. Empty Item entries are ignored.\n# Empty Description uses the original tooltip; empty Currency uses Coins.\n# Empty Amount, Price, UnitSellPrice use 0; empty CanSell uses false.\n# Selling requires CanSell: true and a positive UnitSellPrice.\n# Currency is the purchase currency. SellCurrency is the sale payout currency.\n# SellCurrency: empty, \"\", or omitted defaults to Currency. Save to reload automatically.\n# Amount: 0 makes an item sell-only: no purchase listing, including matching native stock.\n# Set CanSell: true and UnitSellPrice for sales; Price is ignored when Amount is 0.\n# Optional purchase conditions (selling still uses CanSell / UnitSellPrice):\n#   RequiredGlobalKeys: defeated_gdking,defeated_serpent\n#   RequiredGlobalKeysMode: All   # All or Any; default All\n#   ForbiddenGlobalKeys: defeated_bonemass\n# Empty keys impose no restriction, including RequiredGlobalKeysMode: Any.\n# Empty RequiredGlobalKeysMode uses All.\n# ANY forbidden key hides the item, even if all required conditions are met.\n# Keys use comma-separated names: key1,key2,key3 (spaces are optional).\n# Legacy [key1, key2] lists and quoted names remain accepted.\n# Keys are checked against the world's existing global keys; no personal progress is saved.\n# In multiplayer the server owns these rules and rechecks purchases. Client YAML is ignored.\n# Unknown but syntactically valid names are accepted (for custom keys); check spelling carefully.\n#\n# Vanilla keys used directly by trader inventories:\n# defeated_gdking          - The Elder defeated (Haldor / Bog Witch stock).\n# defeated_dragon          - Moder defeated (Haldor / Bog Witch stock).\n# defeated_goblinking      - Yagluth defeated (Haldor / Bog Witch stock).\n# defeated_queen           - The Queen defeated (Haldor / Bog Witch stock).\n# defeated_fader           - Fader defeated (Bog Witch stock).\n# defeated_serpent         - A Serpent killed (Bog Witch stock).\n# Hildir1                  - Hildir's Brass Chest returned.\n# Hildir2                  - Hildir's Silver Chest returned.\n# Hildir3                  - Hildir's Bronze Chest returned.\n#\n# Other built-in progression keys usable by the same checks (no extra tracking):\n# defeated_eikthyr         - Eikthyr defeated.\n# defeated_bonemass        - Bonemass defeated.\n# KilledTroll              - A Troll killed.\n# killed_surtling          - A Surtling killed.\n# KilledBat                - A Bat killed.\n# BossHildir1              - Brenna defeated; distinct from returning her chest.\n# BossHildir2              - Geirrhafa defeated; distinct from returning the chest.\n# BossHildir3              - Zil & Thungr encounter defeat; distinct from chest return.\n# Key names are normalized to lowercase; use these spellings in your configuration.\n# Deep North keys are intentionally omitted to avoid spoilers. Look them up online yourself if needed.\n# Version note: vendor mappings verified from Valheim 1.0.12 extracted data.\n# Older game builds may never set newer keys. Internal/legacy keys are not listed here.\n#\n# Examples:\n# Elder defeated, until Bonemass: RequiredGlobalKeys: defeated_gdking\n#                               ForbiddenGlobalKeys: defeated_bonemass\n# Any Hildir chest: RequiredGlobalKeys: Hildir1,Hildir2,Hildir3\n#                  RequiredGlobalKeysMode: Any\n# Leave both lists empty to preserve the original always-available custom stock.\n\n", StringComparison.Ordinal))
			{
				return yaml;
			}
			while (yaml.StartsWith("# FlexibleTraders inventory conditions (", StringComparison.Ordinal) || yaml.StartsWith("# Flexible Traders inventory conditions (", StringComparison.Ordinal))
			{
				int num = yaml.IndexOf("# Leave both lists empty to preserve the original always-available custom stock.", StringComparison.Ordinal);
				if (num < 0)
				{
					break;
				}
				int i = num + "# Leave both lists empty to preserve the original always-available custom stock.".Length;
				string text = yaml.Substring(0, i);
				bool flag = true;
				string[] array = text.Split('\n');
				foreach (string text2 in array)
				{
					if (text2.Trim().Length > 0 && !text2.StartsWith("#", StringComparison.Ordinal))
					{
						flag = false;
						break;
					}
				}
				if (!flag)
				{
					break;
				}
				for (; i < yaml.Length && (yaml[i] == '\r' || yaml[i] == '\n'); i++)
				{
				}
				yaml = yaml.Substring(i);
			}
			return "# FlexibleTraders inventory conditions (1.0.5)\n# Spawn prefab names are case-sensitive. Price buys Amount; UnitSellPrice pays for ONE item.\n# Empty, \"\", null, ~, or omitted fields use defaults. Empty Item entries are ignored.\n# Empty Description uses the original tooltip; empty Currency uses Coins.\n# Empty Amount, Price, UnitSellPrice use 0; empty CanSell uses false.\n# Selling requires CanSell: true and a positive UnitSellPrice.\n# Currency is the purchase currency. SellCurrency is the sale payout currency.\n# SellCurrency: empty, \"\", or omitted defaults to Currency. Save to reload automatically.\n# Amount: 0 makes an item sell-only: no purchase listing, including matching native stock.\n# Set CanSell: true and UnitSellPrice for sales; Price is ignored when Amount is 0.\n# Optional purchase conditions (selling still uses CanSell / UnitSellPrice):\n#   RequiredGlobalKeys: defeated_gdking,defeated_serpent\n#   RequiredGlobalKeysMode: All   # All or Any; default All\n#   ForbiddenGlobalKeys: defeated_bonemass\n# Empty keys impose no restriction, including RequiredGlobalKeysMode: Any.\n# Empty RequiredGlobalKeysMode uses All.\n# ANY forbidden key hides the item, even if all required conditions are met.\n# Keys use comma-separated names: key1,key2,key3 (spaces are optional).\n# Legacy [key1, key2] lists and quoted names remain accepted.\n# Keys are checked against the world's existing global keys; no personal progress is saved.\n# In multiplayer the server owns these rules and rechecks purchases. Client YAML is ignored.\n# Unknown but syntactically valid names are accepted (for custom keys); check spelling carefully.\n#\n# Vanilla keys used directly by trader inventories:\n# defeated_gdking          - The Elder defeated (Haldor / Bog Witch stock).\n# defeated_dragon          - Moder defeated (Haldor / Bog Witch stock).\n# defeated_goblinking      - Yagluth defeated (Haldor / Bog Witch stock).\n# defeated_queen           - The Queen defeated (Haldor / Bog Witch stock).\n# defeated_fader           - Fader defeated (Bog Witch stock).\n# defeated_serpent         - A Serpent killed (Bog Witch stock).\n# Hildir1                  - Hildir's Brass Chest returned.\n# Hildir2                  - Hildir's Silver Chest returned.\n# Hildir3                  - Hildir's Bronze Chest returned.\n#\n# Other built-in progression keys usable by the same checks (no extra tracking):\n# defeated_eikthyr         - Eikthyr defeated.\n# defeated_bonemass        - Bonemass defeated.\n# KilledTroll              - A Troll killed.\n# killed_surtling          - A Surtling killed.\n# KilledBat                - A Bat killed.\n# BossHildir1              - Brenna defeated; distinct from returning her chest.\n# BossHildir2              - Geirrhafa defeated; distinct from returning the chest.\n# BossHildir3              - Zil & Thungr encounter defeat; distinct from chest return.\n# Key names are normalized to lowercase; use these spellings in your configuration.\n# Deep North keys are intentionally omitted to avoid spoilers. Look them up online yourself if needed.\n# Version note: vendor mappings verified from Valheim 1.0.12 extracted data.\n# Older game builds may never set newer keys. Internal/legacy keys are not listed here.\n#\n# Examples:\n# Elder defeated, until Bonemass: RequiredGlobalKeys: defeated_gdking\n#                               ForbiddenGlobalKeys: defeated_bonemass\n# Any Hildir chest: RequiredGlobalKeys: Hildir1,Hildir2,Hildir3\n#                  RequiredGlobalKeysMode: Any\n# Leave both lists empty to preserve the original always-available custom stock.\n\n" + yaml;
		}
	}
	internal static class CatalogFile
	{
		internal static string Ensure(string configDirectory)
		{
			string text = Path.Combine(configDirectory, "FlexibleTraders");
			RenameDirectory(configDirectory, text);
			Directory.CreateDirectory(text);
			string text2 = Path.Combine(text, "inventory.yaml");
			if (File.Exists(text2))
			{
				AddHeader(text2);
				return text2;
			}
			string[] array = new string[3]
			{
				Path.Combine("flexibletraders", "inventory.yaml"),
				"FlexibleTraders.yaml",
				"TraderInventory.yaml"
			};
			foreach (string path in array)
			{
				string text3 = Path.Combine(configDirectory, path);
				if (File.Exists(text3))
				{
					File.Copy(text3, text2, overwrite: false);
					AddHeader(text2);
					return text2;
				}
			}
			using FileStream stream = new FileStream(text2, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
			using StreamWriter streamWriter = new StreamWriter(stream);
			streamWriter.Write("# FlexibleTraders inventory conditions (1.0.5)\n# Spawn prefab names are case-sensitive. Price buys Amount; UnitSellPrice pays for ONE item.\n# Empty, \"\", null, ~, or omitted fields use defaults. Empty Item entries are ignored.\n# Empty Description uses the original tooltip; empty Currency uses Coins.\n# Empty Amount, Price, UnitSellPrice use 0; empty CanSell uses false.\n# Selling requires CanSell: true and a positive UnitSellPrice.\n# Currency is the purchase currency. SellCurrency is the sale payout currency.\n# SellCurrency: empty, \"\", or omitted defaults to Currency. Save to reload automatically.\n# Amount: 0 makes an item sell-only: no purchase listing, including matching native stock.\n# Set CanSell: true and UnitSellPrice for sales; Price is ignored when Amount is 0.\n# Optional purchase conditions (selling still uses CanSell / UnitSellPrice):\n#   RequiredGlobalKeys: defeated_gdking,defeated_serpent\n#   RequiredGlobalKeysMode: All   # All or Any; default All\n#   ForbiddenGlobalKeys: defeated_bonemass\n# Empty keys impose no restriction, including RequiredGlobalKeysMode: Any.\n# Empty RequiredGlobalKeysMode uses All.\n# ANY forbidden key hides the item, even if all required conditions are met.\n# Keys use comma-separated names: key1,key2,key3 (spaces are optional).\n# Legacy [key1, key2] lists and quoted names remain accepted.\n# Keys are checked against the world's existing global keys; no personal progress is saved.\n# In multiplayer the server owns these rules and rechecks purchases. Client YAML is ignored.\n# Unknown but syntactically valid names are accepted (for custom keys); check spelling carefully.\n#\n# Vanilla keys used directly by trader inventories:\n# defeated_gdking          - The Elder defeated (Haldor / Bog Witch stock).\n# defeated_dragon          - Moder defeated (Haldor / Bog Witch stock).\n# defeated_goblinking      - Yagluth defeated (Haldor / Bog Witch stock).\n# defeated_queen           - The Queen defeated (Haldor / Bog Witch stock).\n# defeated_fader           - Fader defeated (Bog Witch stock).\n# defeated_serpent         - A Serpent killed (Bog Witch stock).\n# Hildir1                  - Hildir's Brass Chest returned.\n# Hildir2                  - Hildir's Silver Chest returned.\n# Hildir3                  - Hildir's Bronze Chest returned.\n#\n# Other built-in progression keys usable by the same checks (no extra tracking):\n# defeated_eikthyr         - Eikthyr defeated.\n# defeated_bonemass        - Bonemass defeated.\n# KilledTroll              - A Troll killed.\n# killed_surtling          - A Surtling killed.\n# KilledBat                - A Bat killed.\n# BossHildir1              - Brenna defeated; distinct from returning her chest.\n# BossHildir2              - Geirrhafa defeated; distinct from returning the chest.\n# BossHildir3              - Zil & Thungr encounter defeat; distinct from chest return.\n# Key names are normalized to lowercase; use these spellings in your configuration.\n# Deep North keys are intentionally omitted to avoid spoilers. Look them up online yourself if needed.\n# Version note: vendor mappings verified from Valheim 1.0.12 extracted data.\n# Older game builds may never set newer keys. Internal/legacy keys are not listed here.\n#\n# Examples:\n# Elder defeated, until Bonemass: RequiredGlobalKeys: defeated_gdking\n#                               ForbiddenGlobalKeys: defeated_bonemass\n# Any Hildir chest: RequiredGlobalKeys: Hildir1,Hildir2,Hildir3\n#                  RequiredGlobalKeysMode: Any\n# Leave both lists empty to preserve the original always-available custom stock.\n\nHaldor:\n  - Item: Wood\n    Description:\n    Amount: 20\n    Currency: Coins\n    Price: 20\n    CanSell: false\n    SellCurrency:\n    UnitSellPrice: 1\n    RequiredGlobalKeys:\n    RequiredGlobalKeysMode: All\n    ForbiddenGlobalKeys:\nHildir: []\nBogWitch: []\n");
			return text2;
		}

		private static void RenameDirectory(string configDirectory, string directory)
		{
			if (!Directory.Exists(configDirectory))
			{
				return;
			}
			string text = null;
			string[] directories = Directory.GetDirectories(configDirectory);
			foreach (string text2 in directories)
			{
				string fileName = Path.GetFileName(text2);
				if (fileName == "FlexibleTraders")
				{
					return;
				}
				if (fileName == "flexibletraders")
				{
					text = text2;
				}
			}
			if (text == null)
			{
				return;
			}
			if (!Directory.Exists(directory))
			{
				Directory.Move(text, directory);
				return;
			}
			string text3 = Path.Combine(configDirectory, "FlexibleTraders-migrate-" + Guid.NewGuid().ToString("N"));
			Directory.Move(text, text3);
			try
			{
				Directory.Move(text3, directory);
			}
			catch
			{
				Directory.Move(text3, text);
				throw;
			}
		}

		private static void AddHeader(string path)
		{
			if (new FileInfo(path).Length <= 262144)
			{
				string text = File.ReadAllText(path);
				string text2 = CatalogComments.Add(text);
				if (!(text2 == text) && Encoding.UTF8.GetByteCount(text2) <= 262144)
				{
					File.WriteAllText(path, text2);
				}
			}
		}
	}
	internal static class ConfigMigration
	{
		internal static string RenameSellPrice(string text)
		{
			string[] array = text.Split('\n');
			bool flag = false;
			bool flag2 = false;
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.StartsWith("[", StringComparison.Ordinal))
				{
					flag = text2 == "[UI]";
				}
				else if (flag && Key(text2) == "Show tooltip sell price")
				{
					flag2 = true;
				}
			}
			flag = false;
			for (int j = 0; j < array.Length; j++)
			{
				string text3 = array[j].Trim();
				if (text3.StartsWith("[", StringComparison.Ordinal))
				{
					flag = text3 == "[UI]";
				}
				else if (flag && !(Key(text3) != "Show sale prices"))
				{
					array[j] = (flag2 ? "" : ("Show tooltip sell price " + array[j].Substring(array[j].IndexOf('='))));
					flag2 = true;
				}
			}
			return string.Join("\n", array);
		}

		private static string Key(string line)
		{
			if (line.StartsWith("#", StringComparison.Ordinal))
			{
				return null;
			}
			int num = line.IndexOf('=');
			if (num >= 0)
			{
				return line.Substring(0, num).Trim();
			}
			return null;
		}
	}
	internal sealed class Slot
	{
		public string Key;

		public int Count;

		public int Max;

		public int X;

		public int Y;

		public bool Payable;

		public bool Mergeable;

		public Slot Copy()
		{
			return (Slot)MemberwiseClone();
		}
	}
	internal static class InventoryPlan
	{
		public static List<Slot> Exchange(List<Slot> source, int width, int height, string payment, int cost, string reward, int amount, int maxStack)
		{
			if (width < 1 || height < 1 || cost < 0 || amount < 1 || maxStack < 1)
			{
				return null;
			}
			List<Slot> list = new List<Slot>();
			foreach (Slot item in source)
			{
				list.Add(item.Copy());
			}
			int num = cost;
			foreach (Slot item2 in list)
			{
				if (item2.Payable && item2.Key == payment)
				{
					int num2 = Math.Min(num, item2.Count);
					item2.Count -= num2;
					num -= num2;
					if (num == 0)
					{
						break;
					}
				}
			}
			if (num != 0)
			{
				return null;
			}
			num = amount;
			foreach (Slot item3 in list)
			{
				if (item3.Count > 0 && item3.Mergeable && item3.Key == reward)
				{
					int num3 = Math.Min(num, Math.Max(0, item3.Max - item3.Count));
					item3.Count += num3;
					num -= num3;
					if (num == 0)
					{
						break;
					}
				}
			}
			HashSet<long> hashSet = new HashSet<long>();
			foreach (Slot item4 in list)
			{
				if (item4.Count > 0)
				{
					hashSet.Add(((long)item4.Y << 32) | (uint)item4.X);
				}
			}
			for (int i = 0; i < height; i++)
			{
				if (num <= 0)
				{
					break;
				}
				for (int j = 0; j < width; j++)
				{
					if (num <= 0)
					{
						break;
					}
					if (!hashSet.Contains(((long)i << 32) | (uint)j))
					{
						int num4 = Math.Min(num, maxStack);
						list.Add(new Slot
						{
							Key = reward,
							Count = num4,
							Max = maxStack,
							X = j,
							Y = i
						});
						num -= num4;
					}
				}
			}
			if (num != 0)
			{
				return null;
			}
			return list;
		}
	}
	[BepInPlugin("dannanboi.flexibletraders", "FlexibleTraders", "1.0.5")]
	public sealed class Plugin : BaseUnityPlugin
	{
		private sealed class Connection
		{
			internal ZNetPeer Peer;

			internal string Version;

			internal float StartedAt;

			internal bool Rejected;

			internal float CloseAt;

			internal int SentRevision;

			internal readonly RequestGate Trades = new RequestGate();
		}

		[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_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;
					}
				}
			}
		}

		private sealed class PendingTrade
		{
			internal int Id;

			internal int Revision;

			internal int Quantity;

			internal ZRpc Rpc;

			internal ZDOID TraderId;

			internal StoreGui Gui;

			internal Offer Offer;

			internal Player Player;

			internal float Deadline;
		}

		private const string HelloRpc = "dannanboi.flexibletraders.Hello";

		private const string SettingsRpc = "dannanboi.flexibletraders.Settings";

		private const string RejectedRpc = "dannanboi.flexibletraders.Rejected";

		private const int Protocol = 2;

		private int catalogRevision;

		private byte[] settingsBytes;

		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.flexibletraders";

		public const string ModName = "FlexibleTraders";

		public const string OfficialVersion = "1.0.5";

		public const string PluginVersion = "1.0.5";

		public const string ModVersion = "1.0.5";

		internal static Plugin Instance;

		internal ConfigEntry<bool> ShowTooltipSellPrice;

		internal ConfigEntry<bool> ShowTooltipCost;

		internal ConfigEntry<bool> SellWindowEnabled;

		internal ConfigEntry<bool> HideOnEmpty;

		internal ConfigEntry<string> SellWindowTitle;

		internal ConfigEntry<string> SellWindowButton;

		internal ConfigEntry<string> CostString;

		internal ConfigEntry<string> SalePriceString;

		internal Catalog Current = new Catalog();

		private string localYaml = "Haldor: []\n";

		private string currentYaml = "Haldor: []\n";

		private string yamlPath;

		private DateTime lastWrite;

		private DateTime configWrite;

		private long lastLength = -1L;

		private string lastError;

		private Harmony harmony;

		private ZNet network;

		private bool authority = true;

		private bool settingsReady = true;

		private float nextPoll;

		private const string TradeRequestRpc = "dannanboi.flexibletraders.TradeRequest";

		private const string TradeReplyRpc = "dannanboi.flexibletraders.TradeReply";

		private const float TradeTimeout = 8f;

		private const float ServerTradeDistance = 10f;

		private static readonly int[] TraderHashes = Array.ConvertAll(Catalog.Traders, (string name) => StringExtensionMethods.GetStableHashCode(name));

		private int nextTradeId;

		private PendingTrade pending;

		internal bool Ready => settingsReady;

		internal bool TradePending => pending != null;

		internal static bool VersionMatches(string received)
		{
			return string.Equals(received, "1.0.5", StringComparison.Ordinal);
		}

		private void RegisterConnection(ZNet net, ZNetPeer peer)
		{
			BeginNetwork(net);
			if (!net.IsServer())
			{
				connectionError = null;
				settingsReady = false;
				catalogRevision = 0;
				CancelTrade();
				Current = new Catalog();
				Runtime.Clear();
				Store.Reset();
			}
			Connection value = new Connection
			{
				Peer = peer,
				StartedAt = Time.realtimeSinceStartup
			};
			connections[peer.m_rpc] = value;
			peer.m_rpc.Register<string>("dannanboi.flexibletraders.Hello", (Action<ZRpc, string>)ReceiveHello);
			peer.m_rpc.Register<ZPackage>("dannanboi.flexibletraders.Settings", (Action<ZRpc, ZPackage>)ReceiveSettings);
			peer.m_rpc.Register<string>("dannanboi.flexibletraders.Rejected", (Action<ZRpc, string>)ReceiveRejection);
			peer.m_rpc.Register<ZPackage>("dannanboi.flexibletraders.TradeRequest", (Action<ZRpc, ZPackage>)ReceiveTradeRequest);
			peer.m_rpc.Register<ZPackage>("dannanboi.flexibletraders.TradeReply", (Action<ZRpc, ZPackage>)ReceiveTradeReply);
			peer.m_rpc.Invoke("dannanboi.flexibletraders.Hello", new object[1] { "1.0.5" });
		}

		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 ?? "";
			if (!VersionMatches(version))
			{
				if (network.IsServer())
				{
					Reject(rpc, value, Requirement(version));
				}
				else if (IsServerConnection(rpc))
				{
					FailClient("This server requires FlexibleTraders " + SafeVersion(version) + ". You have 1.0.5. 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 FlexibleTraders 1.0.5. " + (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);
				if (connection.Version != null)
				{
					rpc.Invoke("dannanboi.flexibletraders.Rejected", new object[1] { message });
				}
				rpc.Invoke("Error", new object[1] { 3 });
			}
		}

		private void ReceiveRejection(ZRpc rpc, string message)
		{
			if (IsServerConnection(rpc))
			{
				if (string.IsNullOrEmpty(message) || message.Length > 512)
				{
					message = Requirement(null);
				}
				FailClient(message.Replace("<", "").Replace(">", ""));
			}
		}

		private void FailClient(string message)
		{
			connectionError = message;
			settingsReady = false;
			CancelTrade();
			Runtime.Clear();
			Store.Reset();
			foreach (Connection value in connections.Values)
			{
				value.Rejected = true;
			}
			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())
				{
					rpc.Invoke("Error", new object[1] { 3 });
				}
				else
				{
					FailClient("The connection could not verify FlexibleTraders 1.0.5.");
				}
				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 FlexibleTraders 1.0.5 and send its settings before you can join.");
			}
			return false;
		}

		private void SendSettings(ZRpc rpc)
		{
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Expected O, but got Unknown
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Expected O, but got Unknown
			if (connections.TryGetValue(rpc, out var value) && value.SentRevision != catalogRevision)
			{
				if (settingsBytes == null)
				{
					ZPackage val = new ZPackage();
					val.Write(2);
					val.Write(catalogRevision);
					val.Write(currentYaml);
					settingsBytes = val.GetArray();
				}
				rpc.Invoke("dannanboi.flexibletraders.Settings", new object[1] { (object)new ZPackage(settingsBytes) });
				value.SentRevision = catalogRevision;
			}
		}

		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 == null || data.Size() > 262176)
				{
					throw new FormatException("Settings packet is too large.");
				}
				if (data.ReadInt() != 2)
				{
					throw new FormatException("Settings protocol mismatch.");
				}
				int num = data.ReadInt();
				if (num < 1 || num < catalogRevision)
				{
					throw new FormatException("Invalid catalogue revision.");
				}
				string text = data.ReadString();
				if (text.Length > 262144)
				{
					throw new FormatException("Server inventory is too large.");
				}
				if (!settingsReady || currentYaml != text || num != catalogRevision)
				{
					Apply(text);
				}
				catalogRevision = num;
			}
			catch (Exception ex)
			{
				FailClient("FlexibleTraders 1.0.5: " + ex.Message);
			}
		}

		private void PumpConnections(float now)
		{
			if (connections.Count == 0)
			{
				return;
			}
			removedConnections.Clear();
			foreach (KeyValuePair<ZRpc, Connection> connection in connections)
			{
				Connection value = connection.Value;
				if (!connection.Key.IsConnected())
				{
					removedConnections.Add(connection.Key);
				}
				else if (network.IsServer())
				{
					if (!value.Rejected && value.Version == null && now - value.StartedAt > 30f)
					{
						Reject(connection.Key, value, Requirement(null));
					}
					if (value.Rejected && now >= value.CloseAt)
					{
						network.Disconnect(value.Peer);
						removedConnections.Add(connection.Key);
					}
				}
				else if (!settingsReady && connectionError == null && now - value.StartedAt > 30f)
				{
					FailClient("The server did not complete the FlexibleTraders 1.0.5 handshake.");
				}
			}
			foreach (ZRpc removedConnection in removedConnections)
			{
				connections.Remove(removedConnection);
			}
			removedConnections.Clear();
		}

		private void Awake()
		{
			//IL_02c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_02cb: Expected O, but got Unknown
			Instance = this;
			string text = Path.Combine(Paths.ConfigPath, "se.dannan.traderinventory.cfg");
			if (!File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath) && File.Exists(text))
			{
				File.Copy(text, ((BaseUnityPlugin)this).Config.ConfigFilePath, overwrite: false);
				((BaseUnityPlugin)this).Config.Reload();
			}
			if (File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath))
			{
				string text2 = File.ReadAllText(((BaseUnityPlugin)this).Config.ConfigFilePath);
				string text3 = ConfigMigration.RenameSellPrice(text2);
				if (text3 != text2)
				{
					File.WriteAllText(((BaseUnityPlugin)this).Config.ConfigFilePath, text3);
					((BaseUnityPlugin)this).Config.Reload();
				}
			}
			SellWindowEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "Sell Window", true, "Show a separate sell window to the right of the trader and hide the original Sell button. Lists whole inventory stacks: vanilla valuables first, then additional YAML sales for the current trader. Select a stack and press Sell. Local option; not synced with the server.");
			HideOnEmpty = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "Hide on empty", true, "Hide the Sell Window when the current trader accepts no items from your inventory. It reappears when sellable items become available. Local option; not synced with the server.");
			SellWindowTitle = ((BaseUnityPlugin)this).Config.Bind<string>("UI", "Sell Window Title", "INVENTORY", "Text at the top of the Sell Window. Supports existing $localization keys. Empty hides the text. Local option; not synced with the server.");
			SellWindowButton = ((BaseUnityPlugin)this).Config.Bind<string>("UI", "Sell Window Button", "Sell", "Label on the Sell Window button. Supports existing $localization keys. Empty hides the label. Local option; not synced with the server.");
			ShowTooltipSellPrice = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "Show tooltip sell price", true, "Show the first sellable YAML entry as price per unit and current stack total, without trader names. When disabled, preserve the original item tooltip including its vanilla Value field. Local setting; not synced with the server. Show tooltip cost remains a separate trader-window option.");
			SalePriceString = ((BaseUnityPlugin)this).Config.Bind<string>("UI", "Sale price string", "Sale Price:<#FFA000> #amount# #currency# per unit (Total: #total#)</color>", "Local sale tooltip format. #amount# = price per unit; #currency# = localized payout currency; #total# = current stack payout; #quantity# = current stack size. Supports <#RRGGBB>, <#RRGGBBAA>, normal rich-text tags, \\n line breaks and existing $localization keys. Empty hides the row and preserves the original tooltip. Not synced with the server.");
			ShowTooltipCost = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "Show tooltip cost", true, "Append the full purchase cost and currency name to trader item tooltips, for example Cost: 100 Coins. This is a local display preference and is not synced with the server.");
			CostString = ((BaseUnityPlugin)this).Config.Bind<string>("UI", "Cost string", "Cost:<#FFFF00> #amount# #currency#</color>", "Local trader tooltip format. #amount# and #total# = cost for the entire purchase bundle; #currency# = localized purchase currency; #quantity# = bundle size. Supports <#RRGGBB>, <#RRGGBBAA>, normal rich-text tags, \\n line breaks and existing $localization keys. Empty hides the row. Not synced with the server.");
			HideOnEmpty.SettingChanged += delegate
			{
				SellWindow.InvalidateUi();
			};
			SellWindowTitle.SettingChanged += delegate
			{
				SellWindow.InvalidateUi();
			};
			SellWindowButton.SettingChanged += delegate
			{
				SellWindow.InvalidateUi();
			};
			ShowTooltipSellPrice.SettingChanged += RefreshTooltips;
			SalePriceString.SettingChanged += RefreshTooltips;
			ShowTooltipCost.SettingChanged += RefreshTooltips;
			CostString.SettingChanged += RefreshTooltips;
			SellWindowEnabled.SettingChanged += delegate(object sender, EventArgs args)
			{
				CancelTrade();
				SellWindow.Destroy();
				RefreshTooltips(sender, args);
			};
			yamlPath = CatalogFile.Ensure(Paths.ConfigPath);
			ReadLocal();
			harmony = new Harmony("dannanboi.flexibletraders");
			try
			{
				harmony.PatchAll(typeof(Plugin).Assembly);
			}
			catch (Exception ex)
			{
				harmony.UnpatchSelf();
				((BaseUnityPlugin)this).Logger.LogError((object)("Unsupported game API; patches removed: " + ex));
				((Behaviour)this).enabled = false;
				return;
			}
			((BaseUnityPlugin)this).Logger.LogInfo((object)("FlexibleTraders 1.0.5 loaded. YAML: " + yamlPath));
		}

		private void RefreshTooltips(object sender, EventArgs args)
		{
			Store.Refresh(StoreGui.instance);
		}

		private void BeginNetwork(ZNet value)
		{
			if (network != value)
			{
				CancelTrade();
				connections.Clear();
				catalogRevision = 0;
				network = value;
				authority = !Object.op_Implicit((Object)(object)value) || value.IsServer();
				settingsReady = authority;
				if (authority)
				{
					Apply(localYaml);
					return;
				}
				Current = new Catalog();
				Runtime.Clear();
				Store.Reset();
			}
		}

		private void Update()
		{
			if (network != ZNet.instance)
			{
				BeginNetwork(ZNet.instance);
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			PumpTrade(realtimeSinceStartup);
			if (realtimeSinceStartup < nextPoll)
			{
				return;
			}
			nextPoll = realtimeSinceStartup + 1f;
			if (authority)
			{
				ReadLocal();
			}
			if (!Application.isBatchMode)
			{
				Store.PollConditions();
				try
				{
					FileInfo fileInfo = new FileInfo(((BaseUnityPlugin)this).Config.ConfigFilePath);
					if (fileInfo.Exists && fileInfo.LastWriteTimeUtc != configWrite)
					{
						configWrite = fileInfo.LastWriteTimeUtc;
						((BaseUnityPlugin)this).Config.Reload();
					}
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)("Cannot reload UI config: " + ex.Message));
				}
			}
			if (Object.op_Implicit((Object)(object)network))
			{
				PumpConnections(realtimeSinceStartup);
			}
		}

		private void ReadLocal()
		{
			if (!authority)
			{
				return;
			}
			try
			{
				FileInfo fileInfo = new FileInfo(yamlPath);
				if (!fileInfo.Exists)
				{
					throw new FileNotFoundException("YAML file is missing; previous inventory retained.");
				}
				if (fileInfo.LastWriteTimeUtc == lastWrite && fileInfo.Length == lastLength)
				{
					return;
				}
				lastWrite = fileInfo.LastWriteTimeUtc;
				lastLength = fileInfo.Length;
				if (fileInfo.Length > 262144)
				{
					throw new FormatException("YAML exceeds 256 KiB.");
				}
				string text = File.ReadAllText(yamlPath);
				if (text == localYaml && Runtime.Generation > 0)
				{
					lastError = null;
					return;
				}
				Catalog parsed = Catalog.Parse(text);
				string text2 = Catalog.Compact(text);
				localYaml = text;
				lastError = null;
				if (currentYaml != text2 || Runtime.Generation == 0)
				{
					Apply(text2, parsed);
					BroadcastSettings();
				}
			}
			catch (Exception ex)
			{
				if (ex is IOException)
				{
					lastLength = -1L;
				}
				if (lastError != ex.Message)
				{
					lastError = ex.Message;
					((BaseUnityPlugin)this).Logger.LogWarning((object)("Inventory not changed: " + ex.Message));
				}
			}
		}

		private void Apply(string yaml, Catalog parsed = null)
		{
			parsed = parsed ?? Catalog.Parse(yaml);
			CancelTrade();
			if (authority)
			{
				catalogRevision++;
			}
			Current = parsed;
			currentYaml = Catalog.Compact(yaml);
			settingsBytes = null;
			settingsReady = true;
			Runtime.Clear();
			Store.Reset();
			Store.Refresh(StoreGui.instance);
		}

		internal void Warn(string text)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)text);
		}

		internal void Error(Exception e)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)e);
		}

		private void OnDestroy()
		{
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			SellWindow.Destroy();
			CancelTrade();
			connections.Clear();
			Runtime.Clear();
			Store.Reset();
			if ((Object)(object)Instance == (Object)(object)this)
			{
				Instance = null;
			}
		}

		internal void CancelTrade()
		{
			pending = null;
			Store.Invalidate();
		}

		internal void CancelPurchase()
		{
			if (pending != null && !pending.Offer.IsSale)
			{
				CancelTrade();
			}
		}

		private void PumpTrade(float now)
		{
			if (pending == null)
			{
				return;
			}
			if (!Object.op_Implicit((Object)(object)pending.Gui) || !StoreGui.IsVisible() || (Object)(object)pending.Player != (Object)(object)Player.m_localPlayer || !Ready)
			{
				CancelTrade();
			}
			else if (!(now < pending.Deadline))
			{
				CancelTrade();
				if (Object.op_Implicit((Object)(object)Player.m_localPlayer))
				{
					((Character)Player.m_localPlayer).Message((MessageType)2, "The trader did not respond. No items were exchanged.", 0, (Sprite)null, false);
				}
			}
		}

		internal void RequestTrade(StoreGui gui, Offer offer)
		{
			//IL_01a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01aa: Unknown result type (might be due to invalid IL or missing references)
			//IL_01de: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e5: Expected O, but got Unknown
			//IL_020c: Unknown result type (might be due to invalid IL or missing references)
			if (!Ready || pending != null || !Object.op_Implicit((Object)(object)gui) || !Object.op_Implicit((Object)(object)Player.m_localPlayer) || offer == null || (!offer.IsSale && !WorldConditions.Allows(offer.Rule)))
			{
				return;
			}
			if (authority)
			{
				Store.Complete(gui, offer);
				return;
			}
			ZRpc val = null;
			foreach (KeyValuePair<ZRpc, Connection> connection in connections)
			{
				if (connection.Value.Peer.m_server && !connection.Value.Rejected && connection.Key.IsConnected() && VersionMatches(connection.Value.Version))
				{
					val = connection.Key;
					break;
				}
			}
			Trader val2 = Store.Trader(gui);
			ZNetView val3 = (Object.op_Implicit((Object)(object)val2) ? ((Component)val2).GetComponent<ZNetView>() : null);
			ZDO val4 = (Object.op_Implicit((Object)(object)val3) ? val3.GetZDO() : null);
			if (val == null || val4 == null)
			{
				return;
			}
			int num = (offer.IsSale ? offer.SaleItem.m_stack : offer.Rule.Amount);
			if (!Transactions.Execute(offer, ((Humanoid)Player.m_localPlayer).GetInventory(), commit: false, out var error))
			{
				((Character)Player.m_localPlayer).Message((MessageType)2, error, 0, (Sprite)null, false);
				return;
			}
			if (nextTradeId == int.MaxValue)
			{
				((Character)Player.m_localPlayer).Message((MessageType)2, "Reconnect before trading again.", 0, (Sprite)null, false);
				return;
			}
			pending = new PendingTrade
			{
				Id = ++nextTradeId,
				Revision = catalogRevision,
				Quantity = num,
				Rpc = val,
				TraderId = val4.m_uid,
				Gui = gui,
				Offer = offer,
				Player = Player.m_localPlayer,
				Deadline = Time.realtimeSinceStartup + 8f
			};
			ZPackage val5 = new ZPackage();
			val5.Write(pending.Id);
			val5.Write(pending.Revision);
			val5.Write(val4.m_uid);
			val5.Write(offer.Rule.Item);
			val5.Write(offer.IsSale);
			val5.Write(num);
			try
			{
				val.Invoke("dannanboi.flexibletraders.TradeRequest", new object[1] { val5 });
				Store.Invalidate();
			}
			catch (Exception e)
			{
				CancelTrade();
				Error(e);
			}
		}

		private void ReceiveTradeRequest(ZRpc rpc, ZPackage data)
		{
			//IL_0223: Unknown result type (might be due to invalid IL or missing references)
			//IL_0229: Expected O, but got Unknown
			//IL_008a: Unknown result type (might be due to invalid IL or missing references)
			//IL_008f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00da: Unknown result type (might be due to invalid IL or missing references)
			//IL_00eb: Unknown result type (might be due to invalid IL or missing references)
			//IL_011b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0122: Unknown result type (might be due to invalid IL or missing references)
			//IL_0127: Unknown result type (might be due to invalid IL or missing references)
			//IL_012c: Unknown result type (might be due to invalid IL or missing references)
			if (!Object.op_Implicit((Object)(object)network) || !authority || !connections.TryGetValue(rpc, out var value) || value.Rejected || !VersionMatches(value.Version) || value.Peer.m_uid == 0L)
			{
				return;
			}
			int num = 0;
			bool flag = false;
			try
			{
				if (data == null || data.Size() > 256)
				{
					return;
				}
				num = data.ReadInt();
				if (!value.Trades.Accept(num, Time.realtimeSinceStartup))
				{
					return;
				}
				int num2 = data.ReadInt();
				ZDOID val = data.ReadZDOID();
				string text = data.ReadString();
				bool flag2 = data.ReadBool();
				int quantity = data.ReadInt();
				if (text.Length > 128 || num2 != catalogRevision || ZDOMan.instance == null)
				{
					flag = false;
				}
				else
				{
					ZDO zDO = ZDOMan.instance.GetZDO(value.Peer.m_characterID);
					ZDO zDO2 = ZDOMan.instance.GetZDO(val);
					if (zDO != null && zDO2 != null && zDO.GetOwner() == value.Peer.m_uid)
					{
						Vector3 val2 = zDO.GetPosition() - zDO2.GetPosition();
						if (((Vector3)(ref val2)).sqrMagnitude <= 100f)
						{
							string text2 = null;
							int prefab = zDO2.GetPrefab();
							for (int i = 0; i < TraderHashes.Length; i++)
							{
								if (prefab == TraderHashes[i])
								{
									text2 = Catalog.Traders[i];
									break;
								}
							}
							ItemDrop val3 = ((text2 != null) ? Runtime.Find(text) : null);
							if (Object.op_Implicit((Object)(object)val3))
							{
								Entry entry = Current.Find(text2, text);
								flag = TradePolicy.Allowed(entry, flag2, quantity, val3.m_itemData.m_shared.m_maxStackSize, val3.m_itemData.m_shared.m_value, text == "Coins") && (entry == null || Object.op_Implicit((Object)(object)Runtime.Find(flag2 ? entry.PayoutCurrency : entry.Currency))) && (flag2 || WorldConditions.Allows(entry));
							}
						}
					}
				}
			}
			catch (Exception)
			{
				flag = false;
			}
			if (num > 0)
			{
				ZPackage val4 = new ZPackage();
				val4.Write(num);
				val4.Write(catalogRevision);
				val4.Write(flag);
				rpc.Invoke("dannanboi.flexibletraders.TradeReply", new object[1] { val4 });
			}
		}

		private void ReceiveTradeReply(ZRpc rpc, ZPackage data)
		{
			//IL_0102: Unknown result type (might be due to invalid IL or missing references)
			//IL_0108: Unknown result type (might be due to invalid IL or missing references)
			if (!IsServerConnection(rpc) || pending == null || pending.Rpc != rpc)
			{
				return;
			}
			try
			{
				if (data == null || data.Size() > 32)
				{
					return;
				}
				int num = data.ReadInt();
				int num2 = data.ReadInt();
				bool flag = data.ReadBool();
				if (num != pending.Id)
				{
					return;
				}
				PendingTrade pendingTrade = pending;
				CancelTrade();
				if (Time.realtimeSinceStartup >= pendingTrade.Deadline)
				{
					return;
				}
				if (!flag || num2 != catalogRevision || pendingTrade.Revision != catalogRevision)
				{
					if (Object.op_Implicit((Object)(object)Player.m_localPlayer))
					{
						((Character)Player.m_localPlayer).Message((MessageType)2, "The server refused this trade. Reopen the trader and try again.", 0, (Sprite)null, false);
					}
					return;
				}
				Trader val = Store.Trader(pendingTrade.Gui);
				ZNetView val2 = (Object.op_Implicit((Object)(object)val) ? ((Component)val).GetComponent<ZNetView>() : null);
				ZDO val3 = (Object.op_Implicit((Object)(object)val2) ? val2.GetZDO() : null);
				if (!((Object)(object)pendingTrade.Player != (Object)(object)Player.m_localPlayer) && val3 != null && !(val3.m_uid != pendingTrade.TraderId) && (!pendingTrade.Offer.IsSale || pendingTrade.Offer.SaleItem.m_stack == pendingTrade.Quantity) && (pendingTrade.Offer.IsSale || WorldConditions.Allows(pendingTrade.Offer.Rule)))
				{
					Store.Complete(pendingTrade.Gui, pendingTrade.Offer);
				}
			}
			catch (Exception e)
			{
				CancelTrade();
				Error(e);
			}
		}
	}
	internal static class GameAccess
	{
		private static readonly Dictionary<Type, Dictionary<string, FieldInfo>> fields = new Dictionary<Type, Dictionary<string, FieldInfo>>();

		private static readonly Dictionary<Type, Dictionary<string, MethodInfo>> methods = new Dictionary<Type, Dictionary<string, MethodInfo>>();

		public static T Get<T>(object target, string name)
		{
			Type type = target.GetType();
			if (!fields.TryGetValue(type, out var value))
			{
				value = new Dictionary<string, FieldInfo>();
				fields.Add(type, value);
			}
			if (!value.TryGetValue(name, out var value2))
			{
				value2 = AccessTools.Field(type, name);
				if (value2 == null)
				{
					throw new MissingFieldException(type.FullName, name);
				}
				value.Add(name, value2);
			}
			return (T)value2.GetValue(target);
		}

		public static void Set(object target, string name, object value)
		{
			FieldInfo fieldInfo = AccessTools.Field(target.GetType(), name);
			if (fieldInfo == null)
			{
				throw new MissingFieldException(name);
			}
			fieldInfo.SetValue(target, value);
		}

		public static object Call(object target, string name, params object[] args)
		{
			Type type = target.GetType();
			if (!methods.TryGetValue(type, out var value))
			{
				value = new Dictionary<string, MethodInfo>();
				methods.Add(type, value);
			}
			if (!value.TryGetValue(name, out var value2))
			{
				value2 = AccessTools.Method(type, name, (Type[])null, (Type[])null);
				if (value2 == null)
				{
					throw new MissingMethodException(type.FullName, name);
				}
				value.Add(name, value2);
			}
			return value2.Invoke(target, args);
		}
	}
	internal static class PriceText
	{
		internal const string CostTemplate = "Cost:<#FFFF00> #amount# #currency#</color>";

		internal const string SaleTemplate = "Sale Price:<#FFA000> #amount# #currency# per unit (Total: #total#)</color>";

		private static readonly Regex LeadingTags = new Regex("^(?:<[^>]+>\\s*)+", RegexOptions.CultureInvariant);

		private static readonly Regex Variables = new Regex("#(amount|total|quantity|currency)#", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

		private static readonly Regex Colors = new Regex("<(#[0-9a-f]{6}(?:[0-9a-f]{2})?)>|<color=[^>]+>|</color>", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

		public static string Format(string template, int amount, long total, int quantity, string currency)
		{
			if (string.IsNullOrWhiteSpace(template))
			{
				return "";
			}
			int openColors = 0;
			string text = Colors.Replace(template.Replace("\\n", "\n"), delegate(Match match)
			{
				if (match.Value.Equals("</color>", StringComparison.OrdinalIgnoreCase))
				{
					if (openColors > 0)
					{
						openColors--;
					}
					return match.Value;
				}
				openColors++;
				return (!match.Groups[1].Success) ? match.Value : ("<color=" + match.Groups[1].Value + ">");
			});
			if (openColors > 0)
			{
				StringBuilder stringBuilder = new StringBuilder(text);
				while (openColors-- > 0)
				{
					stringBuilder.Append("</color>");
				}
				text = stringBuilder.ToString();
			}
			return Variables.Replace(text, (Match match) => match.Groups[1].Value.ToLowerInvariant() switch
			{
				"amount" => amount.ToString(CultureInfo.CurrentCulture), 
				"total" => total.ToString(CultureInfo.CurrentCulture), 
				"quantity" => quantity.ToString(CultureInfo.CurrentCulture), 
				_ => currency ?? "", 
			});
		}

		public static string RemoveValue(string tooltip, string localizedLabel)
		{
			if (string.IsNullOrEmpty(tooltip))
			{
				return tooltip;
			}
			string[] array = tooltip.Replace("\r\n", "\n").Split('\n');
			StringBuilder stringBuilder = new StringBuilder();
			string[] array2 = array;
			foreach (string text in array2)
			{
				string text2 = LeadingTags.Replace(text.TrimStart(), "");
				if (!IsField(text2, "$item_value") && (string.IsNullOrEmpty(localizedLabel) || !IsField(text2, localizedLabel)))
				{
					if (stringBuilder.Length > 0)
					{
						stringBuilder.Append('\n');
					}
					stringBuilder.Append(text);
				}
			}
			return stringBuilder.ToString();
		}

		private static bool IsField(string text, string label)
		{
			if (!text.StartsWith(label, StringComparison.Ordinal))
			{
				return false;
			}
			string text2 = text.Substring(label.Length).TrimStart();
			while (text2.StartsWith("</", StringComparison.Ordinal))
			{
				int num = text2.IndexOf('>');
				if (num < 0)
				{
					break;
				}
				text2 = text2.Substring(num + 1).TrimStart();
			}
			return text2.StartsWith(":", StringComparison.Ordinal);
		}

		public static bool SaleQuote(int unitPrice, int stack, out int total)
		{
			long num = (long)unitPrice * (long)stack;
			total = 0;
			if (unitPrice <= 0 || stack <= 0 || num > int.MaxValue)
			{
				return false;
			}
			total = (int)num;
			return true;
		}
	}
	internal enum KeyMatch
	{
		All,
		Any
	}
	internal static class PurchaseConditions
	{
		private static readonly Regex KeyName = new Regex("^[A-Za-z0-9_.-]+$", RegexOptions.CultureInvariant);

		internal static string[] ParseKeys(string value, string field)
		{
			value = value.Trim();
			if (value.StartsWith("[", StringComparison.Ordinal))
			{
				if (!value.EndsWith("]", StringComparison.Ordinal))
				{
					throw new FormatException(field + " has an unclosed key list.");
				}
				value = value.Substring(1, value.Length - 2).Trim();
			}
			else if (value.StartsWith("\"", StringComparison.Ordinal) || value.StartsWith("'", StringComparison.Ordinal))
			{
				try
				{
					value = YamlScalar.Read(value, allowEmpty: true);
				}
				catch (FormatException)
				{
				}
			}
			else
			{
				value = YamlScalar.Read(value, allowEmpty: true);
			}
			string text = value.Trim();
			if (text.Length == 0)
			{
				return new string[0];
			}
			string[] array = text.Split(',');
			List<string> list = new List<string>();
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			string[] array2 = array;
			for (int i = 0; i < array2.Length; i++)
			{
				string text2 = YamlScalar.Read(array2[i], allowEmpty: true).Trim();
				if (text2.Length == 0)
				{
					continue;
				}
				if (text2.Length > 128 || !KeyName.IsMatch(text2))
				{
					throw new FormatException(field + " contains an invalid global key.");
				}
				if (hashSet.Add(text2))
				{
					if (list.Count == 32)
					{
						throw new FormatException(field + " supports at most 32 keys.");
					}
					list.Add(text2.ToLowerInvariant());
				}
			}
			return list.ToArray();
		}

		internal static KeyMatch ParseMode(string value)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return KeyMatch.All;
			}
			if (string.Equals(value, "All", StringComparison.OrdinalIgnoreCase))
			{
				return KeyMatch.All;
			}
			if (string.Equals(value, "Any", StringComparison.OrdinalIgnoreCase))
			{
				return KeyMatch.Any;
			}
			throw new FormatException("RequiredGlobalKeysMode must be All or Any.");
		}

		internal static bool HasConditions(Entry rule)
		{
			if (rule != null)
			{
				if (rule.RequiredGlobalKeys.Length == 0)
				{
					return rule.ForbiddenGlobalKeys.Length != 0;
				}
				return true;
			}
			return false;
		}

		internal static bool Allows(Entry rule, Func<string, bool> hasKey)
		{
			if (rule == null)
			{
				return false;
			}
			string[] forbiddenGlobalKeys = rule.ForbiddenGlobalKeys;
			foreach (string arg in forbiddenGlobalKeys)
			{
				if (hasKey(arg))
				{
					return false;
				}
			}
			if (rule.RequiredGlobalKeys.Length == 0)
			{
				return true;
			}
			bool flag = rule.RequiredGlobalKeysMode == KeyMatch.Any;
			forbiddenGlobalKeys = rule.RequiredGlobalKeys;
			foreach (string arg2 in forbiddenGlobalKeys)
			{
				bool flag2 = hasKey(arg2);
				if (flag && flag2)
				{
					return true;
				}
				if (!flag && !flag2)
				{
					return false;
				}
			}
			return !flag;
		}
	}
	internal sealed class ConditionWatch
	{
		private readonly string[] keys;

		private readonly bool[] values;

		internal ConditionWatch(IEnumerable<Entry> rules, Func<string, bool> hasKey)
		{
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (Entry rule in rules)
			{
				if (rule.Amount != 0)
				{
					string[] requiredGlobalKeys = rule.RequiredGlobalKeys;
					foreach (string item in requiredGlobalKeys)
					{
						hashSet.Add(item);
					}
					requiredGlobalKeys = rule.ForbiddenGlobalKeys;
					foreach (string item2 in requiredGlobalKeys)
					{
						hashSet.Add(item2);
					}
				}
			}
			keys = new string[hashSet.Count];
			hashSet.CopyTo(keys);
			values = new bool[keys.Length];
			for (int j = 0; j < keys.Length; j++)
			{
				values[j] = hasKey(keys[j]);
			}
		}

		internal bool Changed(Func<string, bool> hasKey)
		{
			bool result = false;
			for (int i = 0; i < keys.Length; i++)
			{
				bool flag = hasKey(keys[i]);
				if (flag != values[i])
				{
					values[i] = flag;
					result = true;
				}
			}
			return result;
		}
	}
	internal sealed class Offer
	{
		public Entry Rule;

		public string Trader;

		public ItemDrop Item;

		public ItemDrop Currency;

		public TradeItem Trade;

		public ItemData SaleItem;

		public bool IsSale => SaleItem != null;
	}
	internal static class Runtime
	{
		public static int Generation;

		private static ObjectDB database;

		private static readonly Dictionary<string, List<Offer>> stock = new Dictionary<string, List<Offer>>(StringComparer.OrdinalIgnoreCase);

		private static readonly Dictionary<string, Dictionary<string, Entry>> rules = new Dictionary<string, Dictionary<string, Entry>>(StringComparer.OrdinalIgnoreCase);

		public static void Clear()
		{
			Generation++;
			database = null;
			stock.Clear();
			rules.Clear();
		}

		public static string TraderKey(Trader trader)
		{
			if (!Object.op_Implicit((Object)(object)trader))
			{
				return null;
			}
			string a = ((Object)((Component)trader).gameObject).name.Replace("(Clone)", "").Trim();
			string[] traders = Catalog.Traders;
			foreach (string text in traders)
			{
				if (string.Equals(a, text, StringComparison.OrdinalIgnoreCase))
				{
					return text;
				}
			}
			return trader.m_name switch
			{
				"$npc_haldor" => "Haldor", 
				"$npc_hildir" => "Hildir", 
				"$npc_bogwitch" => "BogWitch", 
				_ => null, 
			};
		}

		public static string Prefab(ItemData item)
		{
			if (item == null || !Object.op_Implicit((Object)(object)item.m_dropPrefab))
			{
				return null;
			}
			return ((Object)item.m_dropPrefab).name;
		}

		public static ItemDrop Find(string name)
		{
			GameObject val = (Object.op_Implicit((Object)(object)ObjectDB.instance) ? ObjectDB.instance.GetItemPrefab(name) : null);
			if (!Object.op_Implicit((Object)(object)val))
			{
				return null;
			}
			return val.GetComponent<ItemDrop>();
		}

		public static string Name(ItemDrop item)
		{
			if (!Object.op_Implicit((Object)(object)item))
			{
				return "?";
			}
			return Localization.instance.Localize(item.m_itemData.m_shared.m_name);
		}

		public static void Ensure()
		{
			//IL_025f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0264: Unknown result type (might be due to invalid IL or missing references)
			//IL_026c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0279: Unknown result type (might be due to invalid IL or missing references)
			//IL_0286: Unknown result type (might be due to invalid IL or missing references)
			//IL_0290: Expected O, but got Unknown
			if (!Object.op_Implicit((Object)(object)ObjectDB.instance) || database == ObjectDB.instance || !Object.op_Implicit((Object)(object)Find("Coins")))
			{
				return;
			}
			database = ObjectDB.instance;
			stock.Clear();
			rules.Clear();
			string[] traders = Catalog.Traders;
			foreach (string text in traders)
			{
				List<Offer> list = new List<Offer>();
				Dictionary<string, Entry> dictionary = new Dictionary<string, Entry>(StringComparer.Ordinal);
				stock.Add(text, list);
				rules.Add(text, dictionary);
				foreach (Entry item in Plugin.Instance.Current.Stock[text])
				{
					ItemDrop val = Find(item.Item);
					ItemDrop val2 = Find(item.Currency);
					ItemDrop val3 = (item.CanSell ? Find(item.PayoutCurrency) : val2);
					if (!Object.op_Implicit((Object)(object)val) || !Object.op_Implicit((Object)(object)val2) || !Object.op_Implicit((Object)(object)val3))
					{
						Plugin.Instance.Warn(text + ": skipped " + item.Item + "; unknown Item, Currency (" + item.Currency + ") or SellCurrency (" + item.PayoutCurrency + ") spawn name.");
						continue;
					}
					if (item.Amount > val.m_itemData.m_shared.m_maxStackSize)
					{
						Plugin.Instance.Warn(text + ": skipped " + item.Item + "; Amount exceeds its maximum stack size " + val.m_itemData.m_shared.m_maxStackSize + ".");
						continue;
					}
					Sprite icon;
					try
					{
						icon = val.m_itemData.GetIcon();
						if (!Object.op_Implicit((Object)(object)icon) || !Object.op_Implicit((Object)(object)val2.m_itemData.GetIcon()) || !Object.op_Implicit((Object)(object)val3.m_itemData.GetIcon()))
						{
							throw new InvalidOperationException("missing icon");
						}
					}
					catch (Exception ex)
					{
						Plugin.Instance.Warn(text + ": skipped " + item.Item + "; invalid item/currency icon: " + ex.Message);
						continue;
					}
					TradeItem trade = new TradeItem
					{
						m_prefab = val,
						m_stack = item.Amount,
						m_price = item.Price,
						m_icon = icon
					};
					TradeDefaults.Initialize(trade, val.m_itemData.m_shared.m_name, item.Description);
					dictionary.Add(item.Item, item);
					list.Add(new Offer
					{
						Rule = item,
						Trader = text,
						Item = val,
						Currency = val2,
						Trade = trade
					});
				}
			}
		}

		public static List<Offer> Stock(string key)
		{
			Ensure();
			if (key == null || !stock.TryGetValue(key, out var value))
			{
				return new List<Offer>();
			}
			return value;
		}

		internal static bool AddPurchaseOffer(List<TradeItem> list, Offer offer)
		{
			list.RemoveAll((TradeItem t) => t != null && Object.op_Implicit((Object)(object)t.m_prefab) && ((Object)((Component)t.m_prefab).gameObject).name == offer.Rule.Item);
			if (!WorldConditions.Allows(offer.Rule))
			{
				return false;
			}
			list.Add(offer.Trade);
			return true;
		}

		public static Entry Rule(string key, string prefab)
		{
			Ensure();
			if (key == null || prefab == null || !rules.TryGetValue(key, out var value) || !value.TryGetValue(prefab, out var value2))
			{
				return null;
			}
			return value2;
		}

		public static bool Eligible(string key, ItemData item)
		{
			if (item == null || item.m_shared == null || item.m_equipped || item.m_stack <= 0)
			{
				return false;
			}
			string text = Prefab(item);
			if (text == null)
			{
				return false;
			}
			return SaleRules.Allowed(Rule(key, text), item.m_shared.m_value, text == "Coins", item.m_equipped, item.m_stack);
		}

		public static Offer Sale(string key, ItemData item)
		{
			if (!Eligible(key, item))
			{
				return null;
			}
			string text = Prefab(item);
			if (text == null)
			{
				return null;
			}
			Entry entry = Rule(key, text);
			if (entry != null && !entry.CanSell)
			{
				return null;
			}
			if (entry == null)
			{
				if (item.m_shared.m_value <= 0 || text == "Coins")
				{
					return null;
				}
				entry = new Entry
				{
					Item = text,
					Currency = "Coins",
					CanSell = true,
					UnitSellPrice = item.m_shared.m_value,
					Amount = 1
				};
			}
			ItemDrop val = Find(entry.PayoutCurrency);
			ItemDrop val2 = Find(text);
			if (!Object.op_Implicit((Object)(object)val) || !Object.op_Implicit((Object)(object)val2))
			{
				return null;
			}
			return new Offer
			{
				Rule = entry,
				Trader = key,
				Item = val2,
				Currency = val,
				SaleItem = item
			};
		}

		internal static List<Offer> Sales(string key, Inventory inventory)
		{
			List<Offer> list = new List<Offer>();
			if (key == null || inventory == null || !Plugin.Instance.Ready)
			{
				return list;
			}
			Ensure();
			Dictionary<string, List<Offer>> dictionary = new Dictionary<string, List<Offer>>(StringComparer.Ordinal);
			foreach (ItemData allItem in inventory.GetAllItems())
			{
				string text = Prefab(allItem);
				if (text == null || (Plugin.Instance.Current.Find(key, text) != null && Rule(key, text) == null))
				{
					continue;
				}
				Offer offer = Sale(key, allItem);
				if (offer == null || !PriceText.SaleQuote(offer.Rule.UnitSellPrice, allItem.m_stack, out var _))
				{
					continue;
				}
				if (allItem.m_shared.m_value > 0 && text != "Coins")
				{
					list.Add(offer);
					continue;
				}
				if (!dictionary.TryGetValue(text, out var value))
				{
					value = new List<Offer>();
					dictionary.Add(text, value);
				}
				value.Add(offer);
			}
			if (Plugin.Instance.Current.Stock.TryGetValue(key, out var value2))
			{
				foreach (Entry item in value2)
				{
					if (dictionary.TryGetValue(item.Item, out var value3))
					{
						list.AddRange(value3);
					}
				}
			}
			return list;
		}
	}
	internal static class SaleRules
	{
		public static int DisplayPrice(IEnumerable<string> traderOrder, Func<string, Entry> ruleAt, int vanillaValue, bool isCoins)
		{
			string currency;
			return DisplayPrice(traderOrder, ruleAt, vanillaValue, isCoins, out currency);
		}

		public static int DisplayPrice(IEnumerable<string> traderOrder, Func<string, Entry> ruleAt, int vanillaValue, bool isCoins, out string currency)
		{
			currency = "Coins";
			bool flag = false;
			foreach (string item in traderOrder)
			{
				Entry entry = ruleAt(item);
				if (entry == null)
				{
					flag = true;
				}
				else if (entry.CanSell && entry.UnitSellPrice > 0)
				{
					currency = entry.PayoutCurrency;
					return entry.UnitSellPrice;
				}
			}
			if (!flag || isCoins || vanillaValue <= 0)
			{
				return 0;
			}
			return vanillaValue;
		}

		public static bool Allowed(Entry rule, int vanillaValue, bool isCoins, bool equipped, int stack)
		{
			if (equipped || stack <= 0)
			{
				return false;
			}
			if (rule == null)
			{
				if (vanillaValue > 0)
				{
					return !isCoins;
				}
				return false;
			}
			if (rule.CanSell)
			{
				return rule.UnitSellPrice > 0;
			}
			return false;
		}

		public static T Select<T>(T vanilla, IEnumerable<T> inventory, Func<T, bool> eligible) where T : class
		{
			if (vanilla != null && eligible(vanilla))
			{
				return vanilla;
			}
			foreach (T item in inventory)
			{
				if (eligible(item))
				{
					return item;
				}
			}
			return null;
		}
	}
	internal static class Store
	{
		[HarmonyPatch(typeof(StoreGui), "Show")]
		private static class ShowPatch
		{
			private static void Prefix()
			{
				SellWindow.Hide();
				Plugin.Instance.CancelTrade();
				Reset();
			}
		}

		[HarmonyPatch(typeof(StoreGui), "Hide")]
		private static class HidePatch
		{
			private static void Postfix()
			{
				SellWindow.Hide();
				Plugin.Instance.CancelTrade();
				Reset();
			}
		}

		[HarmonyPatch(typeof(StoreGui), "FillList")]
		private static class FillPatch
		{
			private static bool Prefix(StoreGui __instance, out bool __state)
			{
				__state = StoreUi.Ready(__instance) && Object.op_Implicit((Object)(object)Trader(__instance));
				if (__state)
				{
					Prepare(__instance);
				}
				return __state;
			}

			private static void Postfix(StoreGui __instance, bool __state)
			{
				if (__state && StoreUi.Ready(__instance))
				{
					Decorate(__instance);
				}
			}
		}

		[HarmonyPatch(typeof(Trader), "GetAvailableItems")]
		private static class StockPatch
		{
			private static void Postfix(Trader __instance, ref List<TradeItem> __result)
			{
				if (!Plugin.Instance.Ready)
				{
					return;
				}
				string text = Runtime.TraderKey(__instance);
				if (text == null)
				{
					return;
				}
				StoreGui instance = StoreGui.instance;
				bool flag = StoreUi.Ready(instance) && Trader(instance) == __instance;
				List<TradeItem> list = ((__result == null) ? new List<TradeItem>() : new List<TradeItem>(__result));
				foreach (Offer item in Runtime.Stock(text))
				{
					if (Runtime.AddPurchaseOffer(list, item) && flag)
					{
						offers[item.Trade] = item;
					}
				}
				__result = list;
				if (flag)
				{
					rendered = list;
				}
			}
		}

		[HarmonyPatch(typeof(StoreGui), "CanAfford")]
		private static class AffordPatch
		{
			private static bool Prefix(TradeItem item, ref bool __result)
			{
				if (item == null || !offers.TryGetValue(item, out var value))
				{
					return true;
				}
				__result = Plugin.Instance.Ready && Object.op_Implicit((Object)(object)Player.m_localPlayer) && WorldConditions.Allows(value.Rule) && Transactions.Count(((Humanoid)Player.m_localPlayer).GetInventory(), value.Rule.Currency) >= value.Rule.Price;
				return false;
			}
		}

		[HarmonyPatch(typeof(StoreGui), "BuySelectedItem")]
		private static class BuyPatch
		{
			private static bool Prefix(StoreGui __instance, out TradeNotifications.Scope __state)
			{
				__state = null;
				if ((!Plugin.Instance.Ready || Plugin.Instance.TradePending) && Supported(__instance))
				{
					return false;
				}
				if (Supported(__instance) && !CanInteract(__instance))
				{
					return false;
				}
				Offer offer = Selected(__instance);
				if (offer != null)
				{
					Perform(__instance, offer);
					return false;
				}
				TradeItem val = GameAccess.Get<TradeItem>(__instance, "m_selectedItem");
				if (val != null && Object.op_Implicit((Object)(object)val.m_prefab) && Runtime.Rule(Runtime.TraderKey(Trader(__instance)), ((Object)((Component)val.m_prefab).gameObject).name) != null)
				{
					Refresh(__instance);
					return false;
				}
				if (val != null && Object.op_Implicit((Object)(object)val.m_prefab) && CanInteract(__instance))
				{
					__state = TradeNotifications.Begin(val.m_prefab.m_itemData, Math.Min(val.m_stack, val.m_prefab.m_itemData.m_shared.m_maxStackSize));
				}
				return true;
			}

			private static void Postfix(TradeNotifications.Scope __state)
			{
				TradeNotifications.End(__state);
			}

			private static Exception Finalizer(Exception __exception, TradeNotifications.Scope __state)
			{
				TradeNotifications.End(__state);
				return __exception;
			}
		}

		[HarmonyPatch(typeof(StoreGui), "GetSellableItem")]
		private static class CandidatePatch
		{
			private static bool Prefix(StoreGui __instance, ref ItemData __result)
			{
				if (!StoreUi.Ready(__instance))
				{
					__result = null;
					return false;
				}
				if (SellWindow.Active(__instance))
				{
					__result = SellWindow.Selected(__instance);
					return false;
				}
				if (!Supported(__instance) || candidateDirty)
				{
					return true;
				}
				__result = candidate;
				return false;
			}

			private static void Postfix(StoreGui __instance, ref ItemData __result)
			{
				string key = Runtime.TraderKey(Trader(__instance));
				if (key == null)
				{
					return;
				}
				if (!Plugin.Instance.Ready || !StoreUi.Ready(__instance))
				{
					__result = null;
					return;
				}
				if (SellWindow.Active(__instance))
				{
					__result = SellWindow.Selected(__instance);
					return;
				}
				if (!candidateDirty)
				{
					__result = candidate;
					return;
				}
				__result = SaleRules.Select(__result, ((Humanoid)Player.m_localPlayer).GetInventory().GetAllItems(), (ItemData item) => Runtime.Eligible(key, item));
				candidate = __result;
				candidateDirty = false;
			}
		}

		[HarmonyPatch(typeof(StoreGui), "SellItem")]
		private static class SellPatch
		{
			private static bool Prefix(StoreGui __instance)
			{
				string text = Runtime.TraderKey(Trader(__instance));
				if (text == null)
				{
					return true;
				}
				if (!CanInteract(__instance) || Plugin.Instance.TradePending)
				{
					return false;
				}
				if (SellWindow.Active(__instance))
				{
					SellSelection(__instance, SellWindow.Selected(__instance));
					return false;
				}
				object obj = GameAccess.Call(__instance, "GetSellableItem");
				ItemData val = (ItemData)((obj is ItemData) ? obj : null);
				if (val == null)
				{
					return false;
				}
				Offer offer = Runtime.Sale(text, val);
				if (offer != null)
				{
					Perform(__instance, offer);
				}
				return false;
			}
		}

		[HarmonyPatch(typeof(StoreGui), "UpdateSellButton")]
		private static class SellButtonPatch
		{
			private static void Postfix(StoreGui __instance)
			{
				if (!StoreUi.Ready(__instance))
				{
					return;
				}
				string text = Runtime.TraderKey(Trader(__instance));
				if (text == null)
				{
					return;
				}
				if (SellWindow.Active(__instance))
				{
					((Component)__instance.m_sellButton).gameObject.SetActive(false);
					return;
				}
				object obj = GameAccess.Call(__instance, "GetSellableItem");
				ItemData val = (ItemData)((obj is ItemData) ? obj : null);
				if (val != null)
				{
					if (saleDirty || cachedSale != val || cachedSaleStack != val.m_stack)
					{
						saleDirty = false;
						cachedSale = val;
						cachedSaleStack = val.m_stack;
						saleAllowed = Transactions.Execute(Runtime.Sale(text, val), ((Humanoid)Player.m_localPlayer).GetInventory(), commit: false, out saleError);
					}
					((Selectable)__instance.m_sellButton).interactable = Plugin.Instance.Ready && !Plugin.Instance.TradePending && saleAllowed;
				}
			}
		}

		[HarmonyPatch(typeof(StoreGui), "UpdateBuyButton")]
		private static class BuyButtonPatch
		{
			private static void Prefix(StoreGui __instance)
			{
				if (StoreUi.Ready(__instance))
				{
					UpdateRows();
				}
			}

			private static void Postfix(StoreGui __instance)
			{
				if (!StoreUi.Ready(__instance))
				{
					return;
				}
				Offer offer = Selected(__instance);
				if (offer != null)
				{
					if (dirty || cachedOffer != offer)
					{
						cachedOffer = offer;
						dirty = false;
						cachedAllowed = Object.op_Implicit((Object)(object)Player.m_localPlayer) && Transactions.Execute(offer, ((Humanoid)Player.m_localPlayer).GetInventory(), commit: false, out cachedError);
					}
					((Selectable)__instance.m_buyButton).interactable = Plugin.Instance.Ready && !Plugin.Instance.TradePending && cachedAllowed;
				}
				if (Supported(__instance) && (!Plugin.Instance.Ready || Plugin.Instance.TradePending))
				{
					((Selectable)__instance.m_buyButton).interactable = false;
				}
			}
		}

		[HarmonyPatch]
		private static class InventoryChangedPatch
		{
			private static MethodBase TargetMethod()
			{
				return AccessTools.Method(typeof(Inventory), "Changed", (Type[])null, (Type[])null);
			}

			private static void Postfix(Inventory __instance)
			{
				if (Object.op_Implicit((Object)(object)Player.m_localPlayer) && __instance == ((Humanoid)Player.m_localPlayer).GetInventory())
				{
					Invalidate();
				}
			}
		}

		[HarmonyPatch(typeof(StoreGui), "SelectItem")]
		private static class SelectPatch
		{
			private static void Postfix()
			{
				dirty = true;
			}
		}

		private static readonly Dictionary<TradeItem, Offer> offers = new Dictionary<TradeItem, Offer>();

		private static readonly List<StoreRow> customRows = new List<StoreRow>();

		private static readonly Dictionary<string, int> currencyCounts = new Dictionary<string, int>(StringComparer.Ordinal);

		private static bool rowsDirty = true;

		private static ConditionWatch conditionWatch;

		private static ZoneSystem watchedWorld;

		private static List<TradeItem> rendered;

		private static Offer cachedOffer;

		private static bool dirty = true;

		private static bool cachedAllowed;

		private static bool saleDirty = true;

		private static bool saleAllowed;

		private static bool candidateDirty = true;

		private static ItemData candidate;

		private static ItemData cachedSale;

		private static int cachedSaleStack;

		private static string cachedError = "";

		private static string saleError = "";

		public static Trader Trader(StoreGui gui)
		{
			if (!Object.op_Implicit((Object)(object)gui))
			{
				return null;
			}
			return GameAccess.Get<Trader>(gui, "m_trader");
		}

		private static bool Supported(StoreGui gui)
		{
			return Runtime.TraderKey(Trader(gui)) != null;
		}

		public static void Invalidate()
		{
			dirty = true;
			saleDirty = true;
			candidateDirty = true;
			rowsDirty = true;
			SellWindow.Invalidate();
		}

		public static void Reset()
		{
			offers.Clear();
			customRows.Clear();
			currencyCounts.Clear();
			conditionWatch = null;
			watchedWorld = null;
			rendered = null;
			cachedOffer = null;
			cachedSale = null;
			Invalidate();
		}

		public static void Refresh(StoreGui gui)
		{
			Invalidate();
			StoreUi.Refresh(gui);
		}

		internal static void PollConditions()
		{
			StoreGui instance = StoreGui.instance;
			if (StoreUi.CanRefresh(instance) && Supported(instance) && conditionWatch != null && (!((Object)(object)watchedWorld == (Object)(object)ZoneSystem.instance) || conditionWatch.Changed(WorldConditions.HasKey)))
			{
				Plugin.Instance.CancelPurchase();
				Offer offer = Selected(instance);
				if (offer != null && !WorldConditions.Allows(offer.Rule))
				{
					GameAccess.Set(instance, "m_selectedItem", null);
				}
				Refresh(instance);
			}
		}

		private static Offer Selected(StoreGui gui)
		{
			TradeItem val = GameAccess.Get<TradeItem>(gui, "m_selectedItem");
			if (val == null || !offers.TryGetValue(val, out var value))
			{
				return null;
			}
			return value;
		}

		private static void Prepare(StoreGui gui)
		{
			offers.Clear();
			customRows.Clear();
			conditionWatch = null;
			rendered = null;
			cachedOffer = null;
			dirty = true;
			saleDirty = true;
			rowsDirty = true;
			string text = Runtime.TraderKey(Trader(gui));
			if (text == null)
			{
				return;
			}
			conditionWatch = new ConditionWatch(Plugin.Instance.Current.Stock[text], WorldConditions.HasKey);
			watchedWorld = ZoneSystem.instance;
			foreach (Offer item in Runtime.Stock(text))
			{
				if (item.Rule.Amount != 0)
				{
					TradeDefaults.Initialize(item.Trade, item.Item.m_itemData.m_shared.m_name, Tooltips.ForOffer(item));
					offers[item.Trade] = item;
				}
			}
		}

		private static void Decorate(StoreGui gui)
		{
			if (rendered == null)
			{
				return;
			}
			List<GameObject> list = GameAccess.Get<List<GameObject>>(gui, "m_itemList");
			if (list == null)
			{
				return;
			}
			for (int i = 0; i < list.Count && i < rendered.Count; i++)
			{
				TradeItem val = rendered[i];
				if (!Object.op_Implicit((Object)(object)list[i]) || val == null)
				{
					continue;
				}
				UITooltip component = list[i].GetComponent<UITooltip>();
				if (Object.op_Implicit((Object)(object)component) && Object.op_Implicit((Object)(object)val.m_prefab))
				{
					component.m_topic = Localization.instance.Localize(string.IsNullOrEmpty(val.m_name) ? val.m_prefab.m_itemData.m_shared.m_name : val.m_name);
				}
				if (!offers.TryGetValue(val, out var value))
				{
					if (Object.op_Implicit((Object)(object)component))
					{
						component.m_text = Tooltips.WithCost(component.m_text, val, gui.m_coinPrefab);
					}
					continue;
				}
				Image[] componentsInChildren = list[i].GetComponentsInChildren<Image>(true);
				foreach (Image val2 in componentsInChildren)
				{
					string name = ((Object)((Component)val2).gameObject).name;
					if (name.IndexOf("coin", StringComparison.OrdinalIgnoreCase) >= 0 && name.IndexOf("bkg", StringComparison.OrdinalIgnoreCase) < 0)
					{
						val2.sprite = value.Currency.m_itemData.GetIcon();
					}
				}
				if (Object.op_Implicit((Object)(object)component))
				{
					component.m_text = Tooltips.WithCost(value.Trade.m_tooltip, val, value.Currency);
				}
				customRows.Add(new StoreRow(list[i], gui.m_listElement, value.Rule));
			}
			UpdateRows();
		}

		private static void UpdateRows()
		{
			if (!rowsDirty)
			{
				return;
			}
			rowsDirty = false;
			currencyCounts.Clear();
			if (customRows.Count == 0)
			{
				return;
			}
			if (Object.op_Implicit((Object)(object)Player.m_localPlayer))
			{
				foreach (ItemData allItem in ((Humanoid)Player.m_localPlayer).GetInventory().GetAllItems())
				{
					if (!allItem.m_equipped)
					{
						string text = Runtime.Prefab(allItem);
						if (text != null)
						{
							currencyCounts.TryGetValue(text, out var value);
							currencyCounts[text] = (int)Math.Min(2147483647L, (long)value + (long)allItem.m_stack);
						}
					}
				}
			}
			foreach (StoreRow customRow in customRows)
			{
				customRow.Update(currencyCounts, Plugin.Instance.Ready && Object.op_Implicit((Object)(object)Player.m_localPlayer));
			}
		}

		internal static bool CanInteract(StoreGui gui)
		{
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			Trader val = Trader(gui);
			if (Plugin.Instance.Ready && StoreUi.CanRefresh(gui) && Object.op_Implicit((Object)(object)val))
			{
				return Vector3.Distance(((Component)val).transform.position, ((Component)Player.m_localPlayer).transform.position) <= gui.m_hideDistance;
			}
			return false;
		}

		private static void Perform(StoreGui gui, Offer offer)
		{
			if (CanInteract(gui) && !Plugin.Instance.TradePending)
			{
				Plugin.Instance.RequestTrade(gui, offer);
			}
		}

		internal static void SellSelection(StoreGui gui, ItemData item)
		{
			if (!CanInteract(gui) || Plugin.Instance.TradePending)
			{
				return;
			}
			Inventory inventory = ((Humanoid)Player.m_localPlayer).GetInventory();
			if (item != null && inventory.GetAllItems().Contains(item))
			{
				Offer offer = Runtime.Sale(Runtime.TraderKey(Trader(gui)), item);
				if (offer != null)
				{
					Perform(gui, offer);
				}
			}
		}

		internal static void Complete(StoreGui gui, Offer offer)
		{
			//IL_0108: Unknown result type (might be due to invalid IL or missing references)
			//IL_010d: Unknown result type (might be due to invalid IL or missing references)
			//IL_011b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0121: Unknown result type (might be due to invalid IL or missing references)
			if (offer == null || !CanInteract(gui) || offer.Trader != Runtime.TraderKey(Trader(gui)))
			{
				return;
			}
			int quantity = (offer.IsSale ? offer.SaleItem.m_stack : offer.Rule.Amount);
			try
			{
				if (!Transactions.Execute(offer, ((Humanoid)Player.m_localPlayer).GetInventory(), commit: true, out var error))
				{
					((Character)Player.m_localPlayer).Message((MessageType)2, error, 0, (Sprite)null, false);
					return;
				}
				if (offer.IsSale)
				{
					TradeNotifications.ShowSale(offer.SaleItem, quantity, offer.Rule.UnitSellPrice, offer.Currency);
				}
				else
				{
					TradeNotifications.Show(sale: false, offer.Item.m_itemData, quantity);
				}
				try
				{
					GameAccess.Call(Trader(gui), offer.IsSale ? "OnSold" : "OnBought", offer.IsSale ? new object[0] : new object[1] { offer.Trade });
					EffectList val = (offer.IsSale ? gui.m_sellEffects : gui.m_buyEffects);
					if (val != null)
					{
						val.Create(((Component)gui).transform.position, Quaternion.identity, (Transform)null, 1f, -1, default(ZDOID));
					}
				}
				catch (Exception e)
				{
					Plugin.Instance.Error(e);
				}
				Refresh(gui);
			}
			catch (Exception e2)
			{
				Plugin.Instance.Error(e2);
			}
		}
	}
	internal sealed class StoreRow
	{
		private readonly Entry rule;

		private readonly List<Graphic> graphics = new List<Graphic>();

		private readonly List<Color> normalColors = new List<Color>();

		internal StoreRow(GameObject row, GameObject template, Entry entry)
		{
			//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_0117: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f3: Unknown result type (might be due to invalid IL or missing references)
			rule = entry;
			Graphic[] array = (Graphic[])(Object.op_Implicit((Object)(object)template) ? ((Array)template.GetComponentsInChildren<Graphic>(true)) : ((Array)new Graphic[0]));
			Graphic[] componentsInChildren = row.GetComponentsInChildren<Graphic>(true);
			foreach (Graphic val in componentsInChildren)
			{
				string name = ((Object)((Component)val).gameObject).name;
				bool flag = name.IndexOf("coin", StringComparison.OrdinalIgnoreCase) >= 0 && name.IndexOf("bkg", StringComparison.OrdinalIgnoreCase) < 0;
				if (!(val is TMP_Text) && !(val is Text) && (!(val is Image) || !(name.Equals("icon", StringComparison.OrdinalIgnoreCase) || flag)))
				{
					continue;
				}
				Color item = Color.white;
				Graphic[] array2 = array;
				foreach (Graphic val2 in array2)
				{
					if (((Object)((Component)val2).gameObject).name == name && ((object)val2).GetType() == ((object)val).GetType())
					{
						item = val2.color;
						break;
					}
				}
				graphics.Add(val);
				normalColors.Add(item);
			}
		}

		internal void Update(Dictionary<string, int> counts, bool ready)
		{
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			counts.TryGetValue(rule.Currency, out var value);
			bool flag = ready && value >= rule.Price;
			for (int i = 0; i < graphics.Count; i++)
			{
				if (Object.op_Implicit((Object)(object)graphics[i]))
				{
					Color val = normalColors[i];
					graphics[i].color = (Color)(flag ? val : new Color(0.5f, 0.5f, 0.5f, val.a));
				}
			}
		}
	}
	[HarmonyPatch(typeof(ItemData), "GetTooltip", new Type[]
	{
		typeof(ItemData),
		typeof(int),
		typeof(bool),
		typeof(float),
		typeof(int),
		typeof(bool)
	})]
	internal static class Tooltips
	{
		private static Offer saleContext;

		internal static string ForSale(Offer offer)
		{
			Offer offer2 = saleContext;
			saleContext =