Decompiled source of GroupRecipes with Popups v1.0.2

plugins/GroupRecipesWithPopups.dll

Decompiled a week ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using GroupRecipesWithPopups.Core;
using GroupRecipesWithPopups.Game;
using GroupRecipesWithPopups.Net;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("555maggot666")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright (c) 2026 555maggot666")]
[assembly: AssemblyDescription("Shares recipe, material, piece and crafting-station discoveries with every player in a Valheim world and shows the original unlock popups.")]
[assembly: AssemblyFileVersion("1.0.2.0")]
[assembly: AssemblyInformationalVersion("1.0.2+06394aaec1ffe4f68c5d1db7696aa7bede0559f9")]
[assembly: AssemblyProduct("GroupRecipes with Popups")]
[assembly: AssemblyTitle("GroupRecipesWithPopups")]
[assembly: AssemblyVersion("1.0.2.0")]
namespace GroupRecipesWithPopups
{
	internal sealed class ModSettings
	{
		public ConfigEntry<bool> ShowNotifications { get; }

		public ConfigEntry<bool> SyncItems { get; }

		public ConfigEntry<bool> SyncRecipesAndPieces { get; }

		public ConfigEntry<bool> SyncCraftingStations { get; }

		public ConfigEntry<bool> EnablePopups { get; }

		public ConfigEntry<int> MaxPopupsOnJoin { get; }

		public ConfigEntry<float> PopupIntervalSeconds { get; }

		public ModSettings(ConfigFile config)
		{
			//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bf: Expected O, but got Unknown
			//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f8: Expected O, but got Unknown
			ShowNotifications = config.Bind<bool>("General", "ShowNotifications", true, "Show the summary line (for example '12 more unlocks were synchronised') when entries were applied without a popup. The original Valheim unlock popups are not affected by this setting.");
			SyncItems = config.Bind<bool>("Sync", "Items", true, "Share discovered materials and items. Applies to sending and receiving.");
			SyncRecipesAndPieces = config.Bind<bool>("Sync", "RecipesAndPieces", true, "Share learned recipes and build pieces. Applies to sending and receiving.");
			SyncCraftingStations = config.Bind<bool>("Sync", "CraftingStations", true, "Share crafting station levels (only ever raised, never lowered). Applies to sending and receiving.");
			EnablePopups = config.Bind<bool>("Display", "EnablePopups", true, "Show the original Valheim unlock popup (icon plus 'New material' / 'Recipe learned' ...) for every entry received from other players. When disabled, received entries are still applied, just silently.");
			MaxPopupsOnJoin = config.Bind<int>("Display", "MaxPopupsOnJoin", 25, new ConfigDescription("Maximum number of popups when a large batch arrives (joining a world, or another player uploading their whole knowledge). Further entries are applied silently, nothing is lost.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 500), Array.Empty<object>()));
			PopupIntervalSeconds = config.Bind<float>("Display", "PopupIntervalSeconds", 0.3f, new ConfigDescription("Delay in seconds between two applied entries, so the popups appear one after another.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 5f), Array.Empty<object>()));
		}

		public bool IsKindEnabled(KnowledgeKind kind)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Expected I4, but got Unknown
			switch (kind - 1)
			{
			case 0:
				return SyncItems.Value;
			case 1:
			case 2:
				return SyncRecipesAndPieces.Value;
			case 3:
				return SyncCraftingStations.Value;
			default:
				return false;
			}
		}
	}
	[BepInPlugin("555maggot666.GroupRecipesWithPopups", "GroupRecipes with Popups", "1.0.2")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "555maggot666.GroupRecipesWithPopups";

		public const string PluginName = "GroupRecipes with Popups";

		public const string PluginVersion = "1.0.2";

		private static readonly HashSet<string> WarnedKeys = new HashSet<string>(StringComparer.Ordinal);

		internal static ManualLogSource Log { get; private set; }

		internal static ModSettings Settings { get; private set; }

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			Settings = new ModSettings(((BaseUnityPlugin)this).Config);
			((Component)this).gameObject.AddComponent<SyncDriver>();
			Log.LogInfo((object)"GroupRecipes with Popups 1.0.2 loaded.");
		}

		internal static void WarnOnce(string key, string message)
		{
			if (WarnedKeys.Add(key))
			{
				ManualLogSource log = Log;
				if (log != null)
				{
					log.LogWarning((object)message);
				}
			}
		}
	}
	internal sealed class SyncDriver : MonoBehaviour
	{
		private const float TickIntervalSeconds = 1f;

		private const int StableTicksBeforeFirstSync = 3;

		private GameCatalog _catalog;

		private PlayerKnowledgeBridge _bridge;

		private VanillaUnlocker _unlocker;

		private ZRoutedRpc _rpc;

		private ZNet _znet;

		private Ledger _ledger;

		private SyncHost _host;

		private SyncClient _client;

		private bool _helloDone;

		private float _tickTimer;

		private int _playerTicks;

		private void Awake()
		{
			_catalog = new GameCatalog();
			_bridge = new PlayerKnowledgeBridge();
			_unlocker = new VanillaUnlocker(_catalog, _bridge);
		}

		private void Update()
		{
			try
			{
				ZRoutedRpc instance = ZRoutedRpc.instance;
				ZNet instance2 = ZNet.instance;
				if (instance == null || (Object)(object)instance2 == (Object)null)
				{
					if (_rpc != null)
					{
						EndSession();
					}
					return;
				}
				if (instance != _rpc || instance2 != _znet)
				{
					BeginSession(instance, instance2);
				}
				_host?.EnsureOpen(instance2);
				float unscaledDeltaTime = Time.unscaledDeltaTime;
				_client?.Update(unscaledDeltaTime);
				_tickTimer += unscaledDeltaTime;
				if (_tickTimer >= 1f)
				{
					_tickTimer = 0f;
					SyncTick();
				}
			}
			catch (Exception ex)
			{
				Failed("update", ex);
			}
		}

		private void BeginSession(ZRoutedRpc rpc, ZNet znet)
		{
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Expected O, but got Unknown
			EndSession();
			_rpc = rpc;
			_znet = znet;
			_ledger = new Ledger();
			_host = (znet.IsServer() ? new SyncHost(_ledger) : null);
			_client = (znet.IsDedicated() ? null : new SyncClient(_ledger, _catalog, _bridge, _unlocker, Plugin.Settings, SendNew));
			_helloDone = false;
			_tickTimer = 0f;
			_playerTicks = 0;
			try
			{
				rpc.Register<ZPackage>("GRWP_Hello", (Action<long, ZPackage>)OnHello);
				rpc.Register<ZPackage>("GRWP_New", (Action<long, ZPackage>)OnNew);
				rpc.Register<ZPackage>("GRWP_CatchUp", (Action<long, ZPackage>)OnCatchUp);
			}
			catch (Exception ex)
			{
				Failed("rpc registration", ex);
			}
			Plugin.Log.LogInfo((object)("Sync session started (" + ((_host != null) ? "server role" : "client role") + ((_client == null) ? ", no local player" : string.Empty) + ")."));
		}

		private void EndSession()
		{
			_client?.Reset();
			_client = null;
			_host = null;
			_ledger = null;
			_rpc = null;
			_znet = null;
			_helloDone = false;
		}

		private void SyncTick()
		{
			Player localPlayer = Player.m_localPlayer;
			if (_client == null || (Object)(object)localPlayer == (Object)null)
			{
				_playerTicks = 0;
			}
			else
			{
				if (++_playerTicks < 3 || !_catalog.Refresh())
				{
					return;
				}
				if (!_helloDone)
				{
					_helloDone = true;
					if (_host != null)
					{
						_host.EnsureOpen(_znet);
						_client.Receive(new List<KnowledgeEntry>(_ledger.Entries), ReceiveMode.CatchUp, final: true, 0L);
					}
					else
					{
						_rpc.InvokeRoutedRPC("GRWP_Hello", new object[1] { SyncProtocol.Encode(new List<KnowledgeEntry>(), 0, 0, 2) });
					}
				}
				_client.Tick(localPlayer);
			}
		}

		private void SendNew(List<KnowledgeEntry> changed, bool bulk)
		{
			if (_host != null)
			{
				_host.EnsureOpen(_znet);
				_host.Persist(changed);
			}
			foreach (ZPackage item in SyncProtocol.EncodeChunks(changed, bulk))
			{
				_rpc.InvokeRoutedRPC(0L, "GRWP_New", new object[1] { item });
			}
		}

		private void OnHello(long sender, ZPackage package)
		{
			try
			{
				if (_host != null && _rpc != null && sender != ZNet.GetUID())
				{
					_host.EnsureOpen(_znet);
					_host.SendCatchUp(_rpc, sender);
				}
			}
			catch (Exception ex)
			{
				Failed("HELLO handler", ex);
			}
		}

		private void OnNew(long sender, ZPackage package)
		{
			HandleEntries(sender, package, isCatchUp: false);
		}

		private void OnCatchUp(long sender, ZPackage package)
		{
			HandleEntries(sender, package, isCatchUp: true);
		}

		private void HandleEntries(long sender, ZPackage package, bool isCatchUp)
		{
			string text = (isCatchUp ? "CATCHUP handler" : "NEW handler");
			try
			{
				if (_ledger == null || sender == ZNet.GetUID() || (isCatchUp && _host != null))
				{
					return;
				}
				if (!SyncProtocol.TryDecode(package, out var entries, out var flags, out var error))
				{
					Plugin.WarnOnce(text + "-decode-" + error, text + ": ignoring bad package from " + sender + " (" + error + ")");
				}
				else
				{
					List<KnowledgeEntry> list = _ledger.AddRange((IEnumerable<KnowledgeEntry>)entries);
					if (_host != null)
					{
						_host.EnsureOpen(_znet);
						_host.Persist(list);
					}
					ReceiveMode mode = (isCatchUp ? ReceiveMode.CatchUp : (((flags & 1) != 0) ? ReceiveMode.Bulk : ReceiveMode.Live));
					_client?.Receive(list, mode, (flags & 2) != 0, sender);
				}
			}
			catch (Exception ex)
			{
				Failed(text, ex);
			}
		}

		private static void Failed(string where, Exception ex)
		{
			Plugin.WarnOnce("failure-" + where + "-" + ex.GetType().Name + "-" + ex.Message, "GroupRecipes with Popups: " + where + " failed: " + ex.GetType().Name + ": " + ex.Message);
		}
	}
}
namespace GroupRecipesWithPopups.Net
{
	internal sealed class LedgerStore
	{
		private readonly LedgerFile _file;

		public string Path => _file.Path;

		public LedgerStore(string worldName, long worldUid)
		{
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Expected O, but got Unknown
			string text = System.IO.Path.Combine(Paths.ConfigPath, "GroupRecipesWithPopups", GrwpPaths.LedgerFileName(worldName, worldUid));
			_file = new LedgerFile(text);
		}

		public void LoadInto(Ledger ledger)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			try
			{
				Ledger val = new Ledger();
				LedgerParseResult val2 = _file.Load(val);
				foreach (string warning in val2.Warnings)
				{
					Plugin.Log.LogWarning((object)("Ledger file " + _file.Path + ": " + warning));
				}
				if (!val2.IsSupported)
				{
					Plugin.Log.LogWarning((object)("Ledger file " + _file.Path + " has an unsupported version; it is left untouched and not used for persistence."));
				}
				List<KnowledgeEntry> list = val.AddRange((IEnumerable<KnowledgeEntry>)ledger.Entries);
				ledger.AddRange((IEnumerable<KnowledgeEntry>)val.Entries);
				if (list.Count > 0)
				{
					Append(list);
				}
				Plugin.Log.LogInfo((object)("Ledger " + _file.Path + " loaded: " + ledger.Count + " entries."));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Could not read ledger file " + _file.Path + ": " + ex.GetType().Name + ": " + ex.Message));
			}
		}

		public void Append(IReadOnlyCollection<KnowledgeEntry> entries)
		{
			if (entries.Count == 0)
			{
				return;
			}
			try
			{
				_file.Append((IEnumerable<KnowledgeEntry>)entries);
			}
			catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
			{
				Plugin.WarnOnce("ledger-write-" + ex.GetType().Name, "Could not write ledger file " + _file.Path + ": " + ex.GetType().Name + ": " + ex.Message);
			}
			catch (Exception ex2)
			{
				Plugin.WarnOnce("ledger-write-other", "Unexpected error while writing " + _file.Path + ": " + ex2.GetType().Name + ": " + ex2.Message);
			}
		}
	}
	internal enum ReceiveMode
	{
		Live,
		Bulk,
		CatchUp
	}
	internal sealed class SyncClient
	{
		private readonly struct Pending
		{
			public List<KnowledgeEntry> Entries { get; }

			public ReceiveMode Mode { get; }

			public bool Final { get; }

			public long Sender { get; }

			public Pending(List<KnowledgeEntry> entries, ReceiveMode mode, bool final, long sender)
			{
				Entries = entries;
				Mode = mode;
				Final = final;
				Sender = sender;
			}
		}

		private readonly Ledger _ledger;

		private readonly GameCatalog _catalog;

		private readonly PlayerKnowledgeBridge _bridge;

		private readonly VanillaUnlocker _unlocker;

		private readonly ModSettings _settings;

		private readonly Action<List<KnowledgeEntry>, bool> _send;

		private readonly KnowledgeSnapshot _snapshot = new KnowledgeSnapshot();

		private readonly List<Pending> _backlog = new List<Pending>();

		private readonly Dictionary<long, int> _popupBudgets = new Dictionary<long, int>();

		private PopupPump _pump;

		private double _pumpInterval;

		private bool _uploadedOnce;

		private int _hiddenTotal;

		private bool _noticeReady;

		public SyncClient(Ledger ledger, GameCatalog catalog, PlayerKnowledgeBridge bridge, VanillaUnlocker unlocker, ModSettings settings, Action<List<KnowledgeEntry>, bool> send)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Expected O, but got Unknown
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0078: Expected O, but got Unknown
			_ledger = ledger;
			_catalog = catalog;
			_bridge = bridge;
			_unlocker = unlocker;
			_settings = settings;
			_send = send;
			_pumpInterval = settings.PopupIntervalSeconds.Value;
			_pump = new PopupPump(_pumpInterval);
		}

		public void Tick(Player player)
		{
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_006a: 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)
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)player == (Object)null || !_catalog.Refresh())
			{
				return;
			}
			DrainBacklog();
			List<KnowledgeEntry> list = new List<KnowledgeEntry>();
			if (!_bridge.TryRead(player, _catalog, list))
			{
				Plugin.WarnOnce("read-knowledge", "Could not read the player's knowledge; nothing is sent.");
				return;
			}
			List<KnowledgeEntry> list2 = new List<KnowledgeEntry>();
			foreach (KnowledgeEntry item in _snapshot.Diff((IEnumerable<KnowledgeEntry>)list))
			{
				KnowledgeEntry current = item;
				if (_settings.IsKindEnabled(((KnowledgeEntry)(ref current)).Kind))
				{
					list2.Add(current);
				}
			}
			bool flag = !_uploadedOnce;
			_uploadedOnce = true;
			if (list2.Count != 0)
			{
				_snapshot.RecordRange((IEnumerable<KnowledgeEntry>)list2);
				List<KnowledgeEntry> list3 = _ledger.AddRange((IEnumerable<KnowledgeEntry>)list2);
				if (list3.Count > 0)
				{
					bool arg = flag || list3.Count > _settings.MaxPopupsOnJoin.Value;
					_send(list3, arg);
				}
			}
		}

		public void Receive(List<KnowledgeEntry> entries, ReceiveMode mode, bool final, long sender)
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				_backlog.Add(new Pending(entries, mode, final, sender));
				return;
			}
			List<KnowledgeEntry> list = new List<KnowledgeEntry>();
			foreach (KnowledgeEntry entry in entries)
			{
				KnowledgeEntry current = entry;
				_snapshot.Record(current);
				if (_settings.IsKindEnabled(((KnowledgeEntry)(ref current)).Kind) && !_bridge.IsKnown(localPlayer, current))
				{
					list.Add(current);
				}
			}
			int num = int.MaxValue;
			if (!_settings.EnablePopups.Value)
			{
				num = 0;
			}
			else if (mode != ReceiveMode.Live)
			{
				if (!_popupBudgets.TryGetValue(sender, out var value))
				{
					value = Math.Max(0, _settings.MaxPopupsOnJoin.Value);
				}
				num = value;
			}
			PopupPlan val = PopupPlanner.BuildPlan((IEnumerable<KnowledgeEntry>)list, num);
			if (mode != ReceiveMode.Live && _settings.EnablePopups.Value)
			{
				if (final)
				{
					_popupBudgets.Remove(sender);
				}
				else
				{
					_popupBudgets[sender] = Math.Max(0, num - val.PopupCount);
				}
			}
			_pump.Enqueue(val);
			_hiddenTotal += val.HiddenCount;
			if (final && _hiddenTotal > 0)
			{
				_noticeReady = true;
			}
		}

		public void Update(float deltaSeconds)
		{
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Expected O, but got Unknown
			//IL_00d7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0094: Unknown result type (might be due to invalid IL or missing references)
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return;
			}
			if (_pump.IsEmpty && Math.Abs(_pumpInterval - (double)_settings.PopupIntervalSeconds.Value) > 1E-06)
			{
				_pumpInterval = _settings.PopupIntervalSeconds.Value;
				_pump = new PopupPump(_pumpInterval);
			}
			_pump.Tick((double)deltaSeconds);
			_unlocker.Tick(localPlayer);
			int num = 0;
			KnowledgeEntry entry = default(KnowledgeEntry);
			for (int i = 0; i < 500; i++)
			{
				if (!_pump.TryDequeueSilent(ref entry))
				{
					break;
				}
				if (_unlocker.Apply(localPlayer, entry, showPopup: false))
				{
					num++;
				}
			}
			bool showPopup = default(bool);
			if (_pump.TryDequeue(ref entry, ref showPopup) && _unlocker.Apply(localPlayer, entry, showPopup))
			{
				num++;
			}
			if (num > 0)
			{
				_unlocker.Refresh(localPlayer);
			}
			if (_noticeReady && _pump.IsEmpty)
			{
				ShowNotice(localPlayer);
			}
		}

		public void Reset()
		{
			_snapshot.Clear();
			_backlog.Clear();
			_popupBudgets.Clear();
			_pump.Clear();
			_unlocker.Reset();
			_uploadedOnce = false;
			_hiddenTotal = 0;
			_noticeReady = false;
		}

		private void DrainBacklog()
		{
			if (_backlog.Count == 0 || (Object)(object)Player.m_localPlayer == (Object)null)
			{
				return;
			}
			List<Pending> list = new List<Pending>(_backlog);
			_backlog.Clear();
			foreach (Pending item in list)
			{
				Receive(item.Entries, item.Mode, item.Final, item.Sender);
			}
		}

		private void ShowNotice(Player player)
		{
			int hiddenTotal = _hiddenTotal;
			_hiddenTotal = 0;
			_noticeReady = false;
			if (hiddenTotal > 0 && _settings.ShowNotifications.Value)
			{
				string text = ((hiddenTotal == 1) ? "unlock was" : "unlocks were");
				((Character)player).Message((MessageType)1, "GroupRecipes: " + hiddenTotal + " more " + text + " synchronised without popup", 0, (Sprite)null, false);
			}
		}
	}
	internal sealed class SyncHost
	{
		private readonly Ledger _ledger;

		private LedgerStore _store;

		public bool IsOpen => _store != null;

		public SyncHost(Ledger ledger)
		{
			_ledger = ledger;
		}

		public void EnsureOpen(ZNet znet)
		{
			if (_store == null && !((Object)(object)znet == (Object)null))
			{
				string worldName = znet.GetWorldName();
				if (!string.IsNullOrEmpty(worldName))
				{
					_store = new LedgerStore(worldName, znet.GetWorldUID());
					_store.LoadInto(_ledger);
				}
			}
		}

		public void Persist(IReadOnlyCollection<KnowledgeEntry> changed)
		{
			if (_store != null && changed.Count > 0)
			{
				_store.Append(changed);
			}
		}

		public void SendCatchUp(ZRoutedRpc rpc, long target)
		{
			foreach (ZPackage item in SyncProtocol.EncodeChunks(new List<KnowledgeEntry>(_ledger.Entries), bulk: false))
			{
				rpc.InvokeRoutedRPC(target, "GRWP_CatchUp", new object[1] { item });
			}
		}
	}
	internal static class SyncProtocol
	{
		public const string RpcHello = "GRWP_Hello";

		public const string RpcNew = "GRWP_New";

		public const string RpcCatchUp = "GRWP_CatchUp";

		public const byte Version = 1;

		public const byte FlagBulk = 1;

		public const byte FlagFinal = 2;

		public const int MaxEntriesPerPackage = 500;

		private const int MaxAcceptedCount = 100000;

		public static List<ZPackage> EncodeChunks(IReadOnlyList<KnowledgeEntry> entries, bool bulk)
		{
			List<ZPackage> list = new List<ZPackage>();
			int num = 0;
			do
			{
				int num2 = Math.Min(500, entries.Count - num);
				bool flag = num + num2 >= entries.Count;
				byte flags = (byte)((bulk ? 1u : 0u) | (uint)(flag ? 2 : 0));
				list.Add(Encode(entries, num, num2, flags));
				num += num2;
			}
			while (num < entries.Count);
			return list;
		}

		public static ZPackage Encode(IReadOnlyList<KnowledgeEntry> entries, int start, int count, byte flags)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			ZPackage val = new ZPackage();
			val.Write((byte)1);
			val.Write(flags);
			val.Write(count);
			for (int i = start; i < start + count; i++)
			{
				KnowledgeEntry val2 = entries[i];
				val.Write((byte)((KnowledgeEntry)(ref val2)).Kind);
				val.Write(((KnowledgeEntry)(ref val2)).Key);
				val.Write(((KnowledgeEntry)(ref val2)).Level);
			}
			return val;
		}

		public static bool TryDecode(ZPackage package, out List<KnowledgeEntry> entries, out byte flags, out string error)
		{
			//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
			entries = new List<KnowledgeEntry>();
			flags = 0;
			error = null;
			if (package == null)
			{
				error = "package is null";
				return false;
			}
			try
			{
				byte b = package.ReadByte();
				if (b != 1)
				{
					error = "unsupported protocol version " + b;
					return false;
				}
				flags = package.ReadByte();
				int num = package.ReadInt();
				if (num < 0 || num > 100000)
				{
					error = "implausible entry count " + num;
					return false;
				}
				for (int i = 0; i < num; i++)
				{
					byte b2 = package.ReadByte();
					string text = package.ReadString();
					int num2 = package.ReadInt();
					if (b2 < 1 || b2 > 4 || string.IsNullOrEmpty(text))
					{
						error = "invalid entry #" + i;
						entries.Clear();
						return false;
					}
					entries.Add(new KnowledgeEntry((KnowledgeKind)b2, text, num2));
				}
				return true;
			}
			catch (Exception ex)
			{
				entries.Clear();
				error = "malformed package: " + ex.GetType().Name + ": " + ex.Message;
				return false;
			}
		}
	}
}
namespace GroupRecipesWithPopups.Game
{
	internal sealed class GameCatalog
	{
		private readonly Dictionary<string, Recipe> _recipes = new Dictionary<string, Recipe>(StringComparer.Ordinal);

		private readonly Dictionary<string, Piece> _pieces = new Dictionary<string, Piece>(StringComparer.Ordinal);

		private readonly Dictionary<string, ItemData> _items = new Dictionary<string, ItemData>(StringComparer.Ordinal);

		private readonly Dictionary<string, CraftingStation> _stations = new Dictionary<string, CraftingStation>(StringComparer.Ordinal);

		private ObjectDB _db;

		private int _dbItemCount = -1;

		private int _dbRecipeCount = -1;

		private ZNetScene _scene;

		private int _sceneCount = -1;

		private float _lastForcedRebuild = float.NegativeInfinity;

		public bool TryRebuildAfterMiss()
		{
			ObjectDB instance = ObjectDB.instance;
			if ((Object)(object)instance == (Object)null || instance.m_items == null || instance.m_recipes == null || instance.m_items.Count == 0)
			{
				return false;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (realtimeSinceStartup - _lastForcedRebuild < 2f)
			{
				return false;
			}
			_lastForcedRebuild = realtimeSinceStartup;
			RebuildObjectTables(instance);
			return true;
		}

		public bool Refresh()
		{
			ObjectDB instance = ObjectDB.instance;
			if ((Object)(object)instance == (Object)null || instance.m_items == null || instance.m_recipes == null || instance.m_items.Count == 0 || instance.m_recipes.Count == 0)
			{
				return false;
			}
			if (instance != _db || instance.m_items.Count != _dbItemCount || instance.m_recipes.Count != _dbRecipeCount)
			{
				RebuildObjectTables(instance);
			}
			ZNetScene instance2 = ZNetScene.instance;
			if ((Object)(object)instance2 != (Object)null && instance2.m_prefabs != null && (instance2 != _scene || instance2.m_prefabs.Count != _sceneCount))
			{
				RebuildStationTable(instance2);
			}
			return true;
		}

		public bool TryGetRecipe(string key, out Recipe recipe)
		{
			if (_recipes.TryGetValue(key, out recipe))
			{
				return (Object)(object)recipe != (Object)null;
			}
			return false;
		}

		public bool TryGetPiece(string key, out Piece piece)
		{
			if (_pieces.TryGetValue(key, out piece))
			{
				return (Object)(object)piece != (Object)null;
			}
			return false;
		}

		public bool TryGetItem(string key, out ItemData item)
		{
			if (_items.TryGetValue(key, out item))
			{
				return item != null;
			}
			return false;
		}

		public bool TryGetStation(string key, out CraftingStation station)
		{
			if (_stations.TryGetValue(key, out station))
			{
				return (Object)(object)station != (Object)null;
			}
			return false;
		}

		public Sprite GetStationIcon(string key)
		{
			if (!TryGetStation(key, out var station))
			{
				return null;
			}
			return station.m_icon;
		}

		public KnowledgeKind ClassifyKnownRecipeKey(string key)
		{
			if (!_recipes.ContainsKey(key))
			{
				if (_pieces.ContainsKey(key))
				{
					return (KnowledgeKind)3;
				}
				return (KnowledgeKind)2;
			}
			return (KnowledgeKind)2;
		}

		private void RebuildObjectTables(ObjectDB db)
		{
			_items.Clear();
			_recipes.Clear();
			_pieces.Clear();
			foreach (GameObject item in db.m_items)
			{
				if ((Object)(object)item == (Object)null)
				{
					continue;
				}
				ItemDrop component = item.GetComponent<ItemDrop>();
				if (!((Object)(object)component == (Object)null) && component.m_itemData != null && component.m_itemData.m_shared != null)
				{
					string name = component.m_itemData.m_shared.m_name;
					if (!string.IsNullOrEmpty(name))
					{
						_items.TryAdd(name, component.m_itemData);
					}
				}
			}
			foreach (Recipe recipe in db.m_recipes)
			{
				if (!((Object)(object)recipe == (Object)null) && !((Object)(object)recipe.m_item == (Object)null) && recipe.m_item.m_itemData != null && recipe.m_item.m_itemData.m_shared != null)
				{
					string name2 = recipe.m_item.m_itemData.m_shared.m_name;
					if (!string.IsNullOrEmpty(name2))
					{
						_recipes.TryAdd(name2, recipe);
					}
				}
			}
			List<Piece> allBuildPieces = db.GetAllBuildPieces(true);
			if (allBuildPieces != null)
			{
				foreach (Piece item2 in allBuildPieces)
				{
					if ((Object)(object)item2 != (Object)null && !string.IsNullOrEmpty(item2.m_name))
					{
						_pieces.TryAdd(item2.m_name, item2);
					}
				}
			}
			_db = db;
			_dbItemCount = db.m_items.Count;
			_dbRecipeCount = db.m_recipes.Count;
		}

		private void RebuildStationTable(ZNetScene scene)
		{
			_stations.Clear();
			foreach (GameObject prefab in scene.m_prefabs)
			{
				if (!((Object)(object)prefab == (Object)null))
				{
					CraftingStation component = prefab.GetComponent<CraftingStation>();
					if ((Object)(object)component != (Object)null && !string.IsNullOrEmpty(component.m_name))
					{
						_stations.TryAdd(component.m_name, component);
					}
				}
			}
			_scene = scene;
			_sceneCount = scene.m_prefabs.Count;
		}
	}
	internal sealed class PlayerKnowledgeBridge
	{
		private const BindingFlags InstanceNonPublic = BindingFlags.Instance | BindingFlags.NonPublic;

		private readonly FieldInfo _knownMaterial;

		private readonly FieldInfo _knownRecipes;

		private readonly FieldInfo _knownStations;

		private readonly MethodInfo _addKnownRecipe;

		private readonly MethodInfo _addKnownPiece;

		private readonly MethodInfo _updateKnownRecipesList;

		private readonly MethodInfo _updateAvailablePiecesList;

		public bool CanAccessKnowledge
		{
			get
			{
				if (_knownMaterial != null && _knownRecipes != null)
				{
					return _knownStations != null;
				}
				return false;
			}
		}

		public bool HasAddKnownRecipe => _addKnownRecipe != null;

		public bool HasAddKnownPiece => _addKnownPiece != null;

		public PlayerKnowledgeBridge()
		{
			Type typeFromHandle = typeof(Player);
			_knownMaterial = FindField(typeFromHandle, "m_knownMaterial");
			_knownRecipes = FindField(typeFromHandle, "m_knownRecipes");
			_knownStations = FindField(typeFromHandle, "m_knownStations");
			_addKnownRecipe = FindMethod(typeFromHandle, "AddKnownRecipe", typeof(Recipe));
			_addKnownPiece = FindMethod(typeFromHandle, "AddKnownPiece", typeof(Piece));
			_updateKnownRecipesList = FindMethod(typeFromHandle, "UpdateKnownRecipesList");
			_updateAvailablePiecesList = FindMethod(typeFromHandle, "UpdateAvailablePiecesList");
		}

		public bool TryRead(Player player, GameCatalog catalog, List<KnowledgeEntry> sink)
		{
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Unknown result type (might be due to invalid IL or missing references)
			//IL_0112: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)player == (Object)null || !CanAccessKnowledge)
			{
				return false;
			}
			HashSet<string> hashSet = _knownMaterial.GetValue(player) as HashSet<string>;
			HashSet<string> hashSet2 = _knownRecipes.GetValue(player) as HashSet<string>;
			Dictionary<string, int> dictionary = _knownStations.GetValue(player) as Dictionary<string, int>;
			if (hashSet == null || hashSet2 == null || dictionary == null)
			{
				return false;
			}
			foreach (string item in hashSet)
			{
				if (!string.IsNullOrEmpty(item))
				{
					sink.Add(new KnowledgeEntry((KnowledgeKind)1, item, 0));
				}
			}
			foreach (string item2 in hashSet2)
			{
				if (!string.IsNullOrEmpty(item2))
				{
					sink.Add(new KnowledgeEntry(catalog.ClassifyKnownRecipeKey(item2), item2, 0));
				}
			}
			foreach (KeyValuePair<string, int> item3 in dictionary)
			{
				if (!string.IsNullOrEmpty(item3.Key))
				{
					sink.Add(new KnowledgeEntry((KnowledgeKind)4, item3.Key, item3.Value));
				}
			}
			return true;
		}

		public bool IsKnown(Player player, KnowledgeEntry entry)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Expected I4, but got Unknown
			if ((Object)(object)player == (Object)null)
			{
				return false;
			}
			KnowledgeKind kind = ((KnowledgeEntry)(ref entry)).Kind;
			switch (kind - 1)
			{
			case 0:
				return player.IsMaterialKnown(((KnowledgeEntry)(ref entry)).Key);
			case 1:
			case 2:
				return player.IsRecipeKnown(((KnowledgeEntry)(ref entry)).Key);
			case 3:
			{
				if (TryGetStationLevel(player, ((KnowledgeEntry)(ref entry)).Key, out var level))
				{
					return level >= ((KnowledgeEntry)(ref entry)).Level;
				}
				return false;
			}
			default:
				return true;
			}
		}

		public bool TryGetStationLevel(Player player, string station, out int level)
		{
			level = 0;
			if (_knownStations?.GetValue(player) is Dictionary<string, int> dictionary)
			{
				return dictionary.TryGetValue(station, out level);
			}
			return false;
		}

		public bool TryWrite(Player player, KnowledgeEntry entry)
		{
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Expected I4, but got Unknown
			if ((Object)(object)player == (Object)null || !CanAccessKnowledge)
			{
				Plugin.WarnOnce("bridge-fields", "Player knowledge fields were not found in this game version; cannot apply entries.");
				return false;
			}
			KnowledgeKind kind = ((KnowledgeEntry)(ref entry)).Kind;
			switch (kind - 1)
			{
			case 0:
				return (_knownMaterial.GetValue(player) as HashSet<string>)?.Add(((KnowledgeEntry)(ref entry)).Key) ?? false;
			case 1:
			case 2:
				return (_knownRecipes.GetValue(player) as HashSet<string>)?.Add(((KnowledgeEntry)(ref entry)).Key) ?? false;
			case 3:
			{
				if (_knownStations.GetValue(player) is Dictionary<string, int> dictionary && (!dictionary.TryGetValue(((KnowledgeEntry)(ref entry)).Key, out var value) || value < ((KnowledgeEntry)(ref entry)).Level))
				{
					dictionary[((KnowledgeEntry)(ref entry)).Key] = ((KnowledgeEntry)(ref entry)).Level;
					return true;
				}
				return false;
			}
			default:
				return false;
			}
		}

		public bool TryInvokeAddKnownRecipe(Player player, Recipe recipe)
		{
			return TryInvoke(_addKnownRecipe, "AddKnownRecipe", player, recipe);
		}

		public bool TryInvokeAddKnownPiece(Player player, Piece piece)
		{
			return TryInvoke(_addKnownPiece, "AddKnownPiece", player, piece);
		}

		public void Refresh(Player player)
		{
			TryInvoke(_updateKnownRecipesList, "UpdateKnownRecipesList", player);
			TryInvoke(_updateAvailablePiecesList, "UpdateAvailablePiecesList", player);
		}

		public void RefreshKnownRecipes(Player player)
		{
			TryInvoke(_updateKnownRecipesList, "UpdateKnownRecipesList", player);
		}

		private static bool TryInvoke(MethodInfo method, string name, Player player, params object[] args)
		{
			if (method == null || (Object)(object)player == (Object)null)
			{
				return false;
			}
			try
			{
				method.Invoke(player, args);
				return true;
			}
			catch (Exception ex)
			{
				Exception ex2 = ((ex is TargetInvocationException && ex.InnerException != null) ? ex.InnerException : ex);
				Plugin.WarnOnce("invoke-" + name, "Player." + name + " failed: " + ex2.GetType().Name + ": " + ex2.Message);
				return false;
			}
		}

		private static FieldInfo FindField(Type type, string name)
		{
			FieldInfo? field = type.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
			if (field == null)
			{
				Plugin.WarnOnce("field-" + name, "Player." + name + " was not found in this game version.");
			}
			return field;
		}

		private static MethodInfo FindMethod(Type type, string name, params Type[] parameters)
		{
			MethodInfo? method = type.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic, null, parameters, null);
			if (method == null)
			{
				Plugin.WarnOnce("method-" + name, "Player." + name + " was not found in this game version; falling back to direct writes.");
			}
			return method;
		}
	}
	internal sealed class VanillaUnlocker
	{
		private const int MaxDeferred = 200;

		private readonly GameCatalog _catalog;

		private readonly PlayerKnowledgeBridge _bridge;

		private readonly List<KnowledgeEntry> _deferredPieces = new List<KnowledgeEntry>();

		private readonly HashSet<string> _deferredKeys = new HashSet<string>(StringComparer.Ordinal);

		public VanillaUnlocker(GameCatalog catalog, PlayerKnowledgeBridge bridge)
		{
			_catalog = catalog;
			_bridge = bridge;
		}

		private static bool IsBuildHudReady()
		{
			Hud instance = Hud.instance;
			if ((Object)(object)instance != (Object)null)
			{
				return (Object)(object)instance.m_buildUi != (Object)null;
			}
			return false;
		}

		private bool DeferPiece(KnowledgeEntry entry)
		{
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			if (_deferredPieces.Count >= 200)
			{
				return false;
			}
			if (_deferredKeys.Add(((KnowledgeEntry)(ref entry)).Key))
			{
				_deferredPieces.Add(entry);
			}
			return true;
		}

		public void Reset()
		{
			_deferredPieces.Clear();
			_deferredKeys.Clear();
		}

		public void Tick(Player player)
		{
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)player == (Object)null || _deferredPieces.Count == 0 || !IsBuildHudReady())
			{
				return;
			}
			KnowledgeEntry[] array = _deferredPieces.ToArray();
			_deferredPieces.Clear();
			_deferredKeys.Clear();
			for (int i = 0; i < array.Length; i++)
			{
				if (!_bridge.IsKnown(player, array[i]))
				{
					try
					{
						Apply(player, array[i], showPopup: true);
					}
					catch (Exception ex)
					{
						Plugin.Log.LogWarning((object)("Retrying the held back piece " + ((object)Unsafe.As<KnowledgeEntry, KnowledgeEntry>(ref array[i])/*cast due to .constrained prefix*/).ToString() + " failed: " + ex.GetType().Name));
					}
				}
			}
		}

		public unsafe bool Apply(Player player, KnowledgeEntry entry, bool showPopup)
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Expected I4, but got Unknown
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)player == (Object)null)
			{
				return false;
			}
			try
			{
				if (_bridge.IsKnown(player, entry))
				{
					return false;
				}
				KnowledgeKind kind = ((KnowledgeEntry)(ref entry)).Kind;
				return (kind - 1) switch
				{
					0 => ApplyMaterial(player, entry, showPopup), 
					1 => ApplyRecipe(player, entry, showPopup), 
					2 => ApplyPiece(player, entry, showPopup), 
					3 => ApplyStation(player, entry, showPopup), 
					_ => false, 
				};
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Applying " + ((object)(*(KnowledgeEntry*)(&entry))/*cast due to .constrained prefix*/).ToString() + " failed: " + ex.GetType().Name + ": " + ex.Message));
				return false;
			}
		}

		public void Refresh(Player player)
		{
			try
			{
				_bridge.Refresh(player);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Refreshing the player's known lists failed: " + ex.GetType().Name + ": " + ex.Message));
			}
		}

		private bool ApplyMaterial(Player player, KnowledgeEntry entry, bool showPopup)
		{
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_0078: Unknown result type (might be due to invalid IL or missing references)
			if (!_catalog.TryGetItem(((KnowledgeEntry)(ref entry)).Key, out var item))
			{
				Plugin.WarnOnce("missing-item-" + ((KnowledgeEntry)(ref entry)).Key, "Item '" + ((KnowledgeEntry)(ref entry)).Key + "' does not exist in this game; adding it to the known materials directly.");
				bool num = _bridge.TryWrite(player, entry);
				if (num && showPopup)
				{
					QueuePopup(null, "$msg_newmaterial", ((KnowledgeEntry)(ref entry)).Key);
				}
				return num;
			}
			if (showPopup)
			{
				player.AddKnownItem(item);
				return _bridge.IsKnown(player, entry);
			}
			return _bridge.TryWrite(player, entry);
		}

		private bool ApplyRecipe(Player player, KnowledgeEntry entry, bool showPopup)
		{
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			if (!_catalog.TryGetRecipe(((KnowledgeEntry)(ref entry)).Key, out var recipe))
			{
				Plugin.WarnOnce("unmapped-recipe-" + ((KnowledgeEntry)(ref entry)).Key, "'" + ((KnowledgeEntry)(ref entry)).Key + "' matches no recipe or piece in this game; adding it to the known recipes silently.");
				return _bridge.TryWrite(player, entry);
			}
			if (!showPopup)
			{
				return _bridge.TryWrite(player, entry);
			}
			if (_bridge.TryInvokeAddKnownRecipe(player, recipe) && _bridge.IsKnown(player, entry))
			{
				return true;
			}
			Plugin.WarnOnce("fallback-recipe", "Vanilla AddKnownRecipe is unavailable; using the direct write plus manual popup fallback.");
			if (!_bridge.TryWrite(player, entry))
			{
				return false;
			}
			QueuePopup(((Object)(object)recipe.m_item != (Object)null && recipe.m_item.m_itemData != null) ? recipe.m_item.m_itemData.GetIcon() : null, "$msg_newrecipe", ((KnowledgeEntry)(ref entry)).Key);
			return true;
		}

		private unsafe bool ApplyPiece(Player player, KnowledgeEntry entry, bool showPopup)
		{
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
			//IL_011f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ff: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_012a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0130: Invalid comparison between Unknown and I4
			//IL_0133: Unknown result type (might be due to invalid IL or missing references)
			//IL_0139: Invalid comparison between Unknown and I4
			//IL_013c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0142: Invalid comparison between Unknown and I4
			if (!_catalog.TryGetPiece(((KnowledgeEntry)(ref entry)).Key, out var piece) && _catalog.TryRebuildAfterMiss())
			{
				_catalog.TryGetPiece(((KnowledgeEntry)(ref entry)).Key, out piece);
			}
			if ((Object)(object)piece == (Object)null)
			{
				Plugin.WarnOnce("missing-piece-" + ((KnowledgeEntry)(ref entry)).Key, "Piece '" + ((KnowledgeEntry)(ref entry)).Key + "' does not exist in this game; adding it to the known recipes directly.");
				bool num = _bridge.TryWrite(player, entry);
				if (num && showPopup)
				{
					QueuePopup(null, "$msg_newpiece", ((KnowledgeEntry)(ref entry)).Key);
				}
				return num;
			}
			if (!showPopup)
			{
				return _bridge.TryWrite(player, entry);
			}
			if (!IsBuildHudReady())
			{
				if (DeferPiece(entry))
				{
					return false;
				}
				Plugin.Log.LogWarning((object)("Build ui is not ready yet and the hold back list is full; applying " + ((object)(*(KnowledgeEntry*)(&entry))/*cast due to .constrained prefix*/).ToString() + " without a popup."));
				return _bridge.TryWrite(player, entry);
			}
			if (_bridge.TryInvokeAddKnownPiece(player, piece) && _bridge.IsKnown(player, entry))
			{
				return true;
			}
			Plugin.WarnOnce("fallback-piece", "Vanilla AddKnownPiece is unavailable; using the direct write plus manual popup fallback.");
			if (!_bridge.TryWrite(player, entry))
			{
				return false;
			}
			bool flag = (int)piece.m_category == 6 || (int)piece.m_category == 7 || (int)piece.m_category == 8;
			QueuePopup(piece.m_icon, flag ? "$msg_newdish" : "$msg_newpiece", ((KnowledgeEntry)(ref entry)).Key);
			return true;
		}

		private bool ApplyStation(Player player, KnowledgeEntry entry, bool showPopup)
		{
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			int level;
			bool flag = _bridge.TryGetStationLevel(player, ((KnowledgeEntry)(ref entry)).Key, out level);
			if (flag && level >= ((KnowledgeEntry)(ref entry)).Level)
			{
				return false;
			}
			if (!_bridge.TryWrite(player, entry))
			{
				return false;
			}
			if (showPopup)
			{
				Sprite stationIcon = _catalog.GetStationIcon(((KnowledgeEntry)(ref entry)).Key);
				if (flag)
				{
					QueuePopup(stationIcon, "$msg_newstation_level", ((KnowledgeEntry)(ref entry)).Key + " $msg_level " + ((KnowledgeEntry)(ref entry)).Level);
				}
				else
				{
					QueuePopup(stationIcon, "$msg_newstation", ((KnowledgeEntry)(ref entry)).Key);
				}
			}
			_bridge.RefreshKnownRecipes(player);
			return true;
		}

		private static void QueuePopup(Sprite icon, string topic, string description)
		{
			MessageHud instance = MessageHud.instance;
			if ((Object)(object)instance != (Object)null)
			{
				instance.QueueUnlockMsg(icon, topic, description);
			}
		}
	}
}

plugins/GroupRecipesWithPopups.Core.dll

Decompiled a week ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("555maggot666")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright (c) 2026 555maggot666")]
[assembly: AssemblyDescription("Engine-independent logic for GroupRecipes with Popups (ledger, snapshot diff, popup planning and pacing).")]
[assembly: AssemblyFileVersion("1.0.2.0")]
[assembly: AssemblyInformationalVersion("1.0.2+06394aaec1ffe4f68c5d1db7696aa7bede0559f9")]
[assembly: AssemblyProduct("GroupRecipes with Popups")]
[assembly: AssemblyTitle("GroupRecipesWithPopups.Core")]
[assembly: AssemblyVersion("1.0.2.0")]
namespace GroupRecipesWithPopups.Core;

public static class GrwpPaths
{
	private const int MaxComponentLength = 64;

	private const string FallbackName = "world";

	public static string SanitizeFileComponent(string name)
	{
		if (string.IsNullOrWhiteSpace(name))
		{
			return "world";
		}
		StringBuilder stringBuilder = new StringBuilder(name.Length);
		string text = name.Trim();
		foreach (char c in text)
		{
			bool flag = c < ' ' || c == '\u007f' || c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|' || char.IsControl(c);
			stringBuilder.Append(flag ? '_' : c);
		}
		string text2 = stringBuilder.ToString().Trim().TrimEnd('.', ' ');
		while (text2.StartsWith(".", StringComparison.Ordinal))
		{
			text2 = text2.Substring(1);
		}
		if (text2.Length > 64)
		{
			text2 = text2.Substring(0, 64);
			if (text2.Length > 0 && char.IsHighSurrogate(text2[text2.Length - 1]))
			{
				text2 = text2.Substring(0, text2.Length - 1);
			}
			text2 = text2.TrimEnd('.', ' ');
		}
		if (text2.Length != 0)
		{
			return text2;
		}
		return "world";
	}

	public static string LedgerFileName(string worldName, long worldUid)
	{
		return SanitizeFileComponent(worldName) + "_" + worldUid.ToString(CultureInfo.InvariantCulture) + ".txt";
	}
}
public readonly struct KnowledgeEntry : IEquatable<KnowledgeEntry>
{
	private sealed class IdentityEqualityComparer : IEqualityComparer<KnowledgeEntry>
	{
		public bool Equals(KnowledgeEntry x, KnowledgeEntry y)
		{
			return x.HasSameIdentity(y);
		}

		public int GetHashCode(KnowledgeEntry obj)
		{
			return HashCode.Combine((int)obj.Kind, (obj.Key != null) ? StringComparer.Ordinal.GetHashCode(obj.Key) : 0);
		}
	}

	public KnowledgeKind Kind { get; }

	public string Key { get; }

	public int Level { get; }

	public bool IsEmpty => Key == null;

	public static IEqualityComparer<KnowledgeEntry> IdentityComparer { get; } = new IdentityEqualityComparer();

	public KnowledgeEntry(KnowledgeKind kind, string key, int level = 0)
	{
		if (key == null)
		{
			throw new ArgumentNullException("key");
		}
		Kind = kind;
		Key = key;
		Level = ((kind == KnowledgeKind.Station) ? Math.Max(0, level) : 0);
	}

	public bool HasSameIdentity(KnowledgeEntry other)
	{
		if (Kind == other.Kind)
		{
			return string.Equals(Key, other.Key, StringComparison.Ordinal);
		}
		return false;
	}

	public bool Equals(KnowledgeEntry other)
	{
		if (HasSameIdentity(other))
		{
			return Level == other.Level;
		}
		return false;
	}

	public override bool Equals(object obj)
	{
		if (obj is KnowledgeEntry other)
		{
			return Equals(other);
		}
		return false;
	}

	public override int GetHashCode()
	{
		return HashCode.Combine((int)Kind, (Key != null) ? StringComparer.Ordinal.GetHashCode(Key) : 0, Level);
	}

	public static bool operator ==(KnowledgeEntry left, KnowledgeEntry right)
	{
		return left.Equals(right);
	}

	public static bool operator !=(KnowledgeEntry left, KnowledgeEntry right)
	{
		return !left.Equals(right);
	}

	public override string ToString()
	{
		if (Kind != KnowledgeKind.Station)
		{
			return Kind.ToString() + ":" + Key;
		}
		return Kind.ToString() + ":" + Key + "@" + Level;
	}
}
public enum KnowledgeKind
{
	Material = 1,
	Recipe,
	Piece,
	Station
}
public sealed class KnowledgeSnapshot
{
	private readonly HashSet<string> _materials = new HashSet<string>(StringComparer.Ordinal);

	private readonly HashSet<string> _recipes = new HashSet<string>(StringComparer.Ordinal);

	private readonly HashSet<string> _pieces = new HashSet<string>(StringComparer.Ordinal);

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

	public bool Knows(KnowledgeEntry entry)
	{
		if (entry.IsEmpty)
		{
			return true;
		}
		switch (entry.Kind)
		{
		case KnowledgeKind.Material:
			return _materials.Contains(entry.Key);
		case KnowledgeKind.Recipe:
			return _recipes.Contains(entry.Key);
		case KnowledgeKind.Piece:
			return _pieces.Contains(entry.Key);
		case KnowledgeKind.Station:
		{
			if (_stations.TryGetValue(entry.Key, out var value))
			{
				return value >= entry.Level;
			}
			return false;
		}
		default:
			return true;
		}
	}

	public void Record(KnowledgeEntry entry)
	{
		if (entry.IsEmpty)
		{
			return;
		}
		switch (entry.Kind)
		{
		case KnowledgeKind.Material:
			_materials.Add(entry.Key);
			break;
		case KnowledgeKind.Recipe:
			_recipes.Add(entry.Key);
			break;
		case KnowledgeKind.Piece:
			_pieces.Add(entry.Key);
			break;
		case KnowledgeKind.Station:
		{
			if (!_stations.TryGetValue(entry.Key, out var value) || value < entry.Level)
			{
				_stations[entry.Key] = entry.Level;
			}
			break;
		}
		}
	}

	public void RecordRange(IEnumerable<KnowledgeEntry> entries)
	{
		if (entries == null)
		{
			throw new ArgumentNullException("entries");
		}
		foreach (KnowledgeEntry entry in entries)
		{
			Record(entry);
		}
	}

	public void Clear()
	{
		_materials.Clear();
		_recipes.Clear();
		_pieces.Clear();
		_stations.Clear();
	}

	public List<KnowledgeEntry> Diff(IEnumerable<KnowledgeEntry> current)
	{
		if (current == null)
		{
			throw new ArgumentNullException("current");
		}
		List<KnowledgeEntry> list = new List<KnowledgeEntry>();
		Dictionary<KnowledgeEntry, int> dictionary = new Dictionary<KnowledgeEntry, int>(KnowledgeEntry.IdentityComparer);
		foreach (KnowledgeEntry item in current)
		{
			if (Knows(item))
			{
				continue;
			}
			if (dictionary.TryGetValue(item, out var value))
			{
				if (item.Level > list[value].Level)
				{
					list[value] = item;
				}
			}
			else
			{
				dictionary.Add(item, list.Count);
				list.Add(item);
			}
		}
		return list;
	}
}
public sealed class Ledger
{
	private readonly List<KnowledgeEntry> _entries = new List<KnowledgeEntry>();

	private readonly Dictionary<KnowledgeEntry, int> _indexByIdentity = new Dictionary<KnowledgeEntry, int>(KnowledgeEntry.IdentityComparer);

	public int Count => _entries.Count;

	public IReadOnlyList<KnowledgeEntry> Entries => _entries;

	public bool Contains(KnowledgeEntry entry)
	{
		if (entry.IsEmpty || !_indexByIdentity.TryGetValue(entry, out var value))
		{
			return false;
		}
		if (entry.Kind == KnowledgeKind.Station)
		{
			return _entries[value].Level >= entry.Level;
		}
		return true;
	}

	public bool TryGetStationLevel(string stationKey, out int level)
	{
		if (stationKey != null && _indexByIdentity.TryGetValue(new KnowledgeEntry(KnowledgeKind.Station, stationKey), out var value))
		{
			level = _entries[value].Level;
			return true;
		}
		level = 0;
		return false;
	}

	public bool Add(KnowledgeEntry entry)
	{
		if (entry.IsEmpty || entry.Key.Length == 0)
		{
			return false;
		}
		if (_indexByIdentity.TryGetValue(entry, out var value))
		{
			if (entry.Kind == KnowledgeKind.Station && entry.Level > _entries[value].Level)
			{
				_entries[value] = entry;
				return true;
			}
			return false;
		}
		_indexByIdentity.Add(entry, _entries.Count);
		_entries.Add(entry);
		return true;
	}

	public List<KnowledgeEntry> AddRange(IEnumerable<KnowledgeEntry> entries)
	{
		if (entries == null)
		{
			throw new ArgumentNullException("entries");
		}
		List<KnowledgeEntry> list = new List<KnowledgeEntry>();
		foreach (KnowledgeEntry entry in entries)
		{
			if (Add(entry))
			{
				list.Add(entry);
			}
		}
		return list;
	}
}
public sealed class LedgerFile
{
	private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

	private bool _readOnly;

	public string Path { get; }

	public bool IsReadOnly => _readOnly;

	public LedgerFile(string path)
	{
		if (string.IsNullOrEmpty(path))
		{
			throw new ArgumentException("Path must not be empty.", "path");
		}
		Path = path;
	}

	public LedgerParseResult Load(Ledger ledger)
	{
		if (ledger == null)
		{
			throw new ArgumentNullException("ledger");
		}
		if (!File.Exists(Path))
		{
			return new LedgerParseResult();
		}
		LedgerParseResult ledgerParseResult = LedgerFileFormat.Parse(File.ReadAllLines(Path, Encoding.UTF8));
		if (!ledgerParseResult.IsSupported)
		{
			_readOnly = true;
			return ledgerParseResult;
		}
		ledger.AddRange(ledgerParseResult.Entries);
		return ledgerParseResult;
	}

	public bool Append(IEnumerable<KnowledgeEntry> entries)
	{
		if (entries == null)
		{
			throw new ArgumentNullException("entries");
		}
		if (_readOnly)
		{
			return false;
		}
		StringBuilder stringBuilder = new StringBuilder();
		int num = 0;
		foreach (KnowledgeEntry entry in entries)
		{
			if (!entry.IsEmpty && entry.Key.Length != 0)
			{
				stringBuilder.Append(LedgerFileFormat.FormatLine(entry)).Append('\n');
				num++;
			}
		}
		if (num == 0)
		{
			return false;
		}
		string directoryName = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(Path));
		if (!string.IsNullOrEmpty(directoryName))
		{
			Directory.CreateDirectory(directoryName);
		}
		using (FileStream fileStream = new FileStream(Path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read))
		{
			string text = string.Empty;
			if (fileStream.Length == 0L)
			{
				text = LedgerFileFormat.Header + "\n";
			}
			else
			{
				fileStream.Seek(-1L, SeekOrigin.End);
				if (fileStream.ReadByte() != 10)
				{
					text = "\n";
				}
			}
			fileStream.Seek(0L, SeekOrigin.End);
			byte[] bytes = Utf8NoBom.GetBytes(text + stringBuilder);
			fileStream.Write(bytes, 0, bytes.Length);
		}
		return true;
	}
}
public sealed class LedgerParseResult
{
	public List<KnowledgeEntry> Entries { get; } = new List<KnowledgeEntry>();

	public List<string> Warnings { get; } = new List<string>();

	public bool IsSupported { get; set; } = true;
}
public static class LedgerFileFormat
{
	public const int Version = 1;

	public const string HeaderPrefix = "#grwp-ledger";

	public static readonly string Header = "#grwp-ledger v" + 1.ToString(CultureInfo.InvariantCulture);

	public static string Escape(string value)
	{
		if (value == null)
		{
			throw new ArgumentNullException("value");
		}
		StringBuilder stringBuilder = null;
		for (int i = 0; i < value.Length; i++)
		{
			char c = value[i];
			string text = c switch
			{
				'\\' => "\\\\", 
				'\t' => "\\t", 
				'\n' => "\\n", 
				'\r' => "\\r", 
				_ => null, 
			};
			if (text == null)
			{
				stringBuilder?.Append(c);
				continue;
			}
			if (stringBuilder == null)
			{
				stringBuilder = new StringBuilder(value.Length + 8);
				stringBuilder.Append(value, 0, i);
			}
			stringBuilder.Append(text);
		}
		if (stringBuilder != null)
		{
			return stringBuilder.ToString();
		}
		return value;
	}

	public static bool TryUnescape(string value, out string result)
	{
		result = null;
		if (value == null)
		{
			return false;
		}
		if (value.IndexOf('\\') < 0)
		{
			result = value;
			return true;
		}
		StringBuilder stringBuilder = new StringBuilder(value.Length);
		for (int i = 0; i < value.Length; i++)
		{
			char c = value[i];
			if (c != '\\')
			{
				stringBuilder.Append(c);
				continue;
			}
			if (i + 1 >= value.Length)
			{
				return false;
			}
			i++;
			switch (value[i])
			{
			case '\\':
				stringBuilder.Append('\\');
				break;
			case 't':
				stringBuilder.Append('\t');
				break;
			case 'n':
				stringBuilder.Append('\n');
				break;
			case 'r':
				stringBuilder.Append('\r');
				break;
			default:
				return false;
			}
		}
		result = stringBuilder.ToString();
		return true;
	}

	public static string FormatLine(KnowledgeEntry entry)
	{
		if (entry.IsEmpty)
		{
			throw new ArgumentException("Cannot format an empty entry.", "entry");
		}
		return KindToText(entry.Kind) + "\t" + Escape(entry.Key) + "\t" + entry.Level.ToString(CultureInfo.InvariantCulture);
	}

	public static bool TryParseLine(string line, out KnowledgeEntry entry, out string error)
	{
		entry = default(KnowledgeEntry);
		error = null;
		if (line == null)
		{
			error = "line is null";
			return false;
		}
		if (line.EndsWith("\r", StringComparison.Ordinal))
		{
			line = line.Substring(0, line.Length - 1);
		}
		string[] array = line.Split('\t');
		if (array.Length != 3)
		{
			error = "expected 3 tab separated fields but found " + array.Length.ToString(CultureInfo.InvariantCulture);
			return false;
		}
		if (!TryParseKind(array[0], out var kind))
		{
			error = "unknown kind '" + array[0] + "'";
			return false;
		}
		if (!TryUnescape(array[1], out var result))
		{
			error = "invalid escape sequence in key";
			return false;
		}
		if (result.Length == 0)
		{
			error = "empty key";
			return false;
		}
		if (!int.TryParse(array[2], NumberStyles.None, CultureInfo.InvariantCulture, out var result2))
		{
			error = "invalid level '" + array[2] + "'";
			return false;
		}
		entry = new KnowledgeEntry(kind, result, result2);
		return true;
	}

	public static LedgerParseResult Parse(IEnumerable<string> lines)
	{
		if (lines == null)
		{
			throw new ArgumentNullException("lines");
		}
		LedgerParseResult ledgerParseResult = new LedgerParseResult();
		int num = 0;
		foreach (string line in lines)
		{
			string text = line;
			num++;
			if (text == null)
			{
				text = string.Empty;
			}
			string text2 = text;
			if (text2.Length > 0 && text2[0] == '\ufeff')
			{
				text2 = text2.Substring(1);
			}
			if (text2.Trim().Length == 0)
			{
				continue;
			}
			KnowledgeEntry entry;
			string error;
			if (text2[0] == '#')
			{
				if (text2.StartsWith("#grwp-ledger", StringComparison.Ordinal) && !IsSupportedHeader(text2))
				{
					ledgerParseResult.IsSupported = false;
					ledgerParseResult.Warnings.Add("line " + num.ToString(CultureInfo.InvariantCulture) + ": unsupported ledger header '" + text2 + "'");
					return ledgerParseResult;
				}
			}
			else if (TryParseLine(text2, out entry, out error))
			{
				ledgerParseResult.Entries.Add(entry);
			}
			else
			{
				ledgerParseResult.Warnings.Add("line " + num.ToString(CultureInfo.InvariantCulture) + ": " + error);
			}
		}
		return ledgerParseResult;
	}

	private static bool IsSupportedHeader(string line)
	{
		return string.Equals(line.TrimEnd(), Header, StringComparison.Ordinal);
	}

	private static string KindToText(KnowledgeKind kind)
	{
		return kind switch
		{
			KnowledgeKind.Material => "Material", 
			KnowledgeKind.Recipe => "Recipe", 
			KnowledgeKind.Piece => "Piece", 
			KnowledgeKind.Station => "Station", 
			_ => throw new ArgumentOutOfRangeException("kind", kind, "Unknown knowledge kind."), 
		};
	}

	private static bool TryParseKind(string text, out KnowledgeKind kind)
	{
		switch (text)
		{
		case "Material":
			kind = KnowledgeKind.Material;
			return true;
		case "Recipe":
			kind = KnowledgeKind.Recipe;
			return true;
		case "Piece":
			kind = KnowledgeKind.Piece;
			return true;
		case "Station":
			kind = KnowledgeKind.Station;
			return true;
		default:
			kind = (KnowledgeKind)0;
			return false;
		}
	}
}
public readonly struct PopupStep
{
	public KnowledgeEntry Entry { get; }

	public bool ShowPopup { get; }

	public PopupStep(KnowledgeEntry entry, bool showPopup)
	{
		Entry = entry;
		ShowPopup = showPopup;
	}
}
public sealed class PopupPlan
{
	public IReadOnlyList<PopupStep> Steps { get; }

	public int PopupCount { get; }

	public int HiddenCount { get; }

	public PopupPlan(IReadOnlyList<PopupStep> steps)
	{
		Steps = steps ?? throw new ArgumentNullException("steps");
		int num = 0;
		for (int i = 0; i < steps.Count; i++)
		{
			if (steps[i].ShowPopup)
			{
				num++;
			}
		}
		PopupCount = num;
		HiddenCount = steps.Count - num;
	}
}
public static class PopupPlanner
{
	public const int Unlimited = int.MaxValue;

	public static PopupPlan BuildPlan(IEnumerable<KnowledgeEntry> pending, int maxPopups)
	{
		if (pending == null)
		{
			throw new ArgumentNullException("pending");
		}
		List<KnowledgeEntry> list = new List<KnowledgeEntry>();
		Dictionary<KnowledgeEntry, int> dictionary = new Dictionary<KnowledgeEntry, int>(KnowledgeEntry.IdentityComparer);
		foreach (KnowledgeEntry item in pending)
		{
			if (item.IsEmpty)
			{
				continue;
			}
			if (dictionary.TryGetValue(item, out var value))
			{
				if (item.Level > list[value].Level)
				{
					list[value] = item;
				}
			}
			else
			{
				dictionary.Add(item, list.Count);
				list.Add(item);
			}
		}
		List<KnowledgeEntry> list2 = new List<KnowledgeEntry>(list.Count);
		for (int i = 0; i <= 2; i++)
		{
			foreach (KnowledgeEntry item2 in list)
			{
				if (RankOf(item2.Kind) == i)
				{
					list2.Add(item2);
				}
			}
		}
		int num = Math.Max(0, maxPopups);
		List<PopupStep> list3 = new List<PopupStep>(list2.Count);
		for (int j = 0; j < list2.Count; j++)
		{
			list3.Add(new PopupStep(list2[j], j < num));
		}
		return new PopupPlan(list3);
	}

	private static int RankOf(KnowledgeKind kind)
	{
		switch (kind)
		{
		case KnowledgeKind.Material:
			return 0;
		case KnowledgeKind.Recipe:
		case KnowledgeKind.Piece:
			return 1;
		default:
			return 2;
		}
	}
}
public sealed class PopupPump
{
	private const double Epsilon = 1E-06;

	private readonly Queue<PopupStep> _queue = new Queue<PopupStep>();

	private readonly double _interval;

	private double _elapsed;

	public int Remaining => _queue.Count;

	public bool IsEmpty => _queue.Count == 0;

	public PopupPump(PopupPlan plan, double intervalSeconds)
		: this(intervalSeconds)
	{
		if (plan == null)
		{
			throw new ArgumentNullException("plan");
		}
		Enqueue(plan);
	}

	public PopupPump(double intervalSeconds)
	{
		_interval = Math.Max(0.0, intervalSeconds);
	}

	public void Enqueue(PopupPlan plan)
	{
		if (plan == null)
		{
			throw new ArgumentNullException("plan");
		}
		foreach (PopupStep step in plan.Steps)
		{
			_queue.Enqueue(step);
		}
	}

	public void Clear()
	{
		_queue.Clear();
		_elapsed = 0.0;
	}

	public void Tick(double deltaSeconds)
	{
		if (!(deltaSeconds <= 0.0) && _queue.Count != 0)
		{
			_elapsed += deltaSeconds;
		}
	}

	public bool TryDequeue(out KnowledgeEntry entry, out bool showPopup)
	{
		entry = default(KnowledgeEntry);
		showPopup = false;
		if (_queue.Count == 0 || _elapsed + 1E-06 < _interval)
		{
			return false;
		}
		PopupStep popupStep = _queue.Dequeue();
		entry = popupStep.Entry;
		showPopup = popupStep.ShowPopup;
		_elapsed = Math.Min(Math.Max(0.0, _elapsed - _interval), _interval);
		if (_queue.Count == 0)
		{
			_elapsed = 0.0;
		}
		return true;
	}

	public bool TryDequeueSilent(out KnowledgeEntry entry)
	{
		entry = default(KnowledgeEntry);
		if (_queue.Count == 0 || _queue.Peek().ShowPopup)
		{
			return false;
		}
		entry = _queue.Dequeue().Entry;
		if (_queue.Count == 0)
		{
			_elapsed = 0.0;
		}
		return true;
	}
}