Decompiled source of GlobalRecipes v1.0.0

GlobalRecipes.dll

Decompiled 2 days 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 System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("GlobalRecipes")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+47036d11f0b25d9119f8dc5eb3771e4a645d82da")]
[assembly: AssemblyProduct("GlobalRecipes")]
[assembly: AssemblyTitle("GlobalRecipes")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace GlobalRecipes
{
	public class CatalogData
	{
		private HashSet<string> _materials = new HashSet<string>(StringComparer.Ordinal);

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

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

		private Dictionary<string, string> _texts = new Dictionary<string, string>(StringComparer.Ordinal);

		public HashSet<string> Materials
		{
			get
			{
				return _materials ?? (_materials = new HashSet<string>(StringComparer.Ordinal));
			}
			set
			{
				_materials = value ?? new HashSet<string>(StringComparer.Ordinal);
			}
		}

		public HashSet<string> Recipes
		{
			get
			{
				return _recipes ?? (_recipes = new HashSet<string>(StringComparer.Ordinal));
			}
			set
			{
				_recipes = value ?? new HashSet<string>(StringComparer.Ordinal);
			}
		}

		public Dictionary<string, int> Stations
		{
			get
			{
				return _stations ?? (_stations = new Dictionary<string, int>(StringComparer.Ordinal));
			}
			set
			{
				_stations = value ?? new Dictionary<string, int>(StringComparer.Ordinal);
			}
		}

		public Dictionary<string, string> Texts
		{
			get
			{
				return _texts ?? (_texts = new Dictionary<string, string>(StringComparer.Ordinal));
			}
			set
			{
				_texts = value ?? new Dictionary<string, string>(StringComparer.Ordinal);
			}
		}

		public bool IsEmpty
		{
			get
			{
				if (Materials.Count == 0 && Recipes.Count == 0 && Stations.Count == 0)
				{
					return Texts.Count == 0;
				}
				return false;
			}
		}

		public int TotalCount => Materials.Count + Recipes.Count + Stations.Count + Texts.Count;

		public void Clear()
		{
			Materials.Clear();
			Recipes.Clear();
			Stations.Clear();
			Texts.Clear();
		}

		public bool MergeFrom(CatalogData other)
		{
			if (other == null)
			{
				return false;
			}
			bool result = false;
			foreach (string material in other.Materials)
			{
				if (Materials.Add(material))
				{
					result = true;
				}
			}
			foreach (string recipe in other.Recipes)
			{
				if (Recipes.Add(recipe))
				{
					result = true;
				}
			}
			foreach (KeyValuePair<string, int> station in other.Stations)
			{
				if (!Stations.TryGetValue(station.Key, out var value) || station.Value > value)
				{
					Stations[station.Key] = station.Value;
					result = true;
				}
			}
			foreach (KeyValuePair<string, string> text in other.Texts)
			{
				if (!Texts.ContainsKey(text.Key))
				{
					Texts[text.Key] = text.Value;
					result = true;
				}
			}
			return result;
		}

		public CatalogData ComputeDelta(CatalogData baseline)
		{
			CatalogData catalogData = new CatalogData();
			foreach (string material in Materials)
			{
				if (baseline == null || !baseline.Materials.Contains(material))
				{
					catalogData.Materials.Add(material);
				}
			}
			foreach (string recipe in Recipes)
			{
				if (baseline == null || !baseline.Recipes.Contains(recipe))
				{
					catalogData.Recipes.Add(recipe);
				}
			}
			foreach (KeyValuePair<string, int> station in Stations)
			{
				if (baseline == null || !baseline.Stations.TryGetValue(station.Key, out var value) || station.Value > value)
				{
					catalogData.Stations[station.Key] = station.Value;
				}
			}
			foreach (KeyValuePair<string, string> text in Texts)
			{
				if (baseline == null || !baseline.Texts.ContainsKey(text.Key))
				{
					catalogData.Texts[text.Key] = text.Value;
				}
			}
			return catalogData;
		}
	}
	[BepInPlugin("com.tyler.globalrecipes", "GlobalRecipes", "1.0.0")]
	[BepInProcess("valheim.exe")]
	[BepInProcess("valheim_server.exe")]
	public class GlobalRecipesPlugin : BaseUnityPlugin
	{
		public const string ModGuid = "com.tyler.globalrecipes";

		public const string ModName = "GlobalRecipes";

		public const string ModVersion = "1.0.0";

		private Harmony _harmony;

		public static GlobalRecipesPlugin Instance { get; private set; }

		public ConfigEntry<bool> ModEnabled { get; private set; }

		public ConfigEntry<float> DebounceSeconds { get; private set; }

		public ConfigEntry<float> SyncIntervalSeconds { get; private set; }

		public ConfigEntry<int> SuppressThreshold { get; private set; }

		public ConfigEntry<bool> ShowSyncNotification { get; private set; }

		public ConfigEntry<bool> EnableSyncButton { get; private set; }

		public static void Log(string message)
		{
			if (Instance != null)
			{
				((BaseUnityPlugin)Instance).Logger.LogInfo((object)message);
			}
			else
			{
				Console.WriteLine("[GlobalRecipes] " + message);
			}
		}

		public static void LogWarning(string message)
		{
			if (Instance != null)
			{
				((BaseUnityPlugin)Instance).Logger.LogWarning((object)message);
			}
			else
			{
				Console.WriteLine("[GlobalRecipes:WARN] " + message);
			}
		}

		private void Awake()
		{
			Instance = this;
			ModEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enables or disables GlobalRecipes synchronization.");
			DebounceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Network", "DebounceSeconds", 2f, "Seconds to buffer and batch new discoveries before sending a network delta.");
			SyncIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Network", "SyncIntervalSeconds", 300f, "Interval in seconds for periodic background reconciliation checks. Set to 0 to disable.");
			SuppressThreshold = ((BaseUnityPlugin)this).Config.Bind<int>("UI", "SuppressThreshold", 5, "If incoming synced unlocks equal or exceed this count in one batch, individual unlock toasts are suppressed to avoid UI spam.");
			ShowSyncNotification = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "ShowSyncNotification", true, "Show a consolidated notification banner at top-left when recipe synchronization completes.");
			EnableSyncButton = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "EnableSyncButton", true, "Add a manual 'Sync' button to the crafting interface in InventoryGui.");
			if (ModEnabled.Value)
			{
				_harmony = Harmony.CreateAndPatchAll(typeof(GlobalRecipesPlugin).Assembly, "com.tyler.globalrecipes");
				((BaseUnityPlugin)this).Logger.LogInfo((object)"GlobalRecipes v1.0.0 loaded and patched.");
			}
		}

		private void OnDestroy()
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			ServerRegistry.ResetSession();
			RecipeSyncManager.ResetSession();
			GlobalRecipesUI.Reset();
		}

		private void Update()
		{
			if (ModEnabled.Value)
			{
				RecipeSyncManager.Update();
			}
		}
	}
	public static class GlobalRecipesUI
	{
		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static UnityAction <>9__1_0;

			internal void <InitializeButton>b__1_0()
			{
				RecipeSyncManager.InitiateFullSync();
			}
		}

		private static GameObject _syncButtonObject;

		public static void InitializeButton(InventoryGui gui)
		{
			//IL_00e9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fe: Unknown result type (might be due to invalid IL or missing references)
			//IL_0113: Unknown result type (might be due to invalid IL or missing references)
			//IL_0128: Unknown result type (might be due to invalid IL or missing references)
			//IL_013d: Unknown result type (might be due to invalid IL or missing references)
			//IL_019f: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a9: Expected O, but got Unknown
			//IL_01d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ce: Expected O, but got Unknown
			if ((Object)(object)gui == (Object)null || !GlobalRecipesPlugin.Instance.EnableSyncButton.Value)
			{
				return;
			}
			try
			{
				if ((Object)(object)_syncButtonObject != (Object)null || (Object)(object)gui.m_craftButton == (Object)null || (Object)(object)gui.m_crafting == (Object)null)
				{
					return;
				}
				_syncButtonObject = Object.Instantiate<GameObject>(((Component)gui.m_craftButton).gameObject, (Transform)(object)gui.m_crafting);
				((Object)_syncButtonObject).name = "GlobalRecipes_SyncButton";
				Localize[] componentsInChildren = _syncButtonObject.GetComponentsInChildren<Localize>(true);
				for (int i = 0; i < componentsInChildren.Length; i++)
				{
					Object.Destroy((Object)(object)componentsInChildren[i]);
				}
				UIGamePad[] componentsInChildren2 = _syncButtonObject.GetComponentsInChildren<UIGamePad>(true);
				for (int i = 0; i < componentsInChildren2.Length; i++)
				{
					Object.Destroy((Object)(object)componentsInChildren2[i]);
				}
				RectTransform component = _syncButtonObject.GetComponent<RectTransform>();
				if ((Object)(object)component != (Object)null)
				{
					component.anchorMin = new Vector2(1f, 1f);
					component.anchorMax = new Vector2(1f, 1f);
					component.pivot = new Vector2(1f, 1f);
					component.anchoredPosition = new Vector2(-15f, -10f);
					component.sizeDelta = new Vector2(70f, 28f);
				}
				TMP_Text componentInChildren = _syncButtonObject.GetComponentInChildren<TMP_Text>();
				if ((Object)(object)componentInChildren != (Object)null)
				{
					componentInChildren.text = "Sync";
				}
				else
				{
					Text componentInChildren2 = _syncButtonObject.GetComponentInChildren<Text>();
					if ((Object)(object)componentInChildren2 != (Object)null)
					{
						componentInChildren2.text = "Sync";
					}
				}
				Button component2 = _syncButtonObject.GetComponent<Button>();
				if ((Object)(object)component2 != (Object)null)
				{
					component2.onClick = new ButtonClickedEvent();
					ButtonClickedEvent onClick = component2.onClick;
					object obj = <>c.<>9__1_0;
					if (obj == null)
					{
						UnityAction val = delegate
						{
							RecipeSyncManager.InitiateFullSync();
						};
						<>c.<>9__1_0 = val;
						obj = (object)val;
					}
					((UnityEvent)onClick).AddListener((UnityAction)obj);
					Navigation navigation = default(Navigation);
					((Navigation)(ref navigation)).mode = (Mode)0;
					((Selectable)component2).navigation = navigation;
					((Selectable)component2).interactable = true;
				}
				_syncButtonObject.SetActive(true);
				GlobalRecipesPlugin.Log("Created GlobalRecipes Sync button in InventoryGui.");
			}
			catch (Exception ex)
			{
				GlobalRecipesPlugin.LogWarning("Could not instantiate Sync button in InventoryGui: " + ex.Message);
			}
		}

		public static void Reset()
		{
			if (_syncButtonObject != null)
			{
				Object.Destroy((Object)(object)_syncButtonObject);
				_syncButtonObject = null;
			}
		}
	}
	public static class Patches
	{
		[HarmonyPatch(typeof(ZNet), "Awake")]
		public static class ZNet_Awake_Patch
		{
			public static void Postfix(ZNet __instance)
			{
				RecipeSyncManager.RegisterRPCs();
				if ((Object)(object)__instance != (Object)null)
				{
					bool flag = __instance.IsServer();
					string worldName = __instance.GetWorldName();
					GlobalRecipesPlugin.Log($"[Lifecycle] ZNet.Awake postfix: IsServer={flag}, World='{worldName}'.");
					if (flag && !string.IsNullOrWhiteSpace(worldName))
					{
						ServerRegistry.Initialize(worldName);
					}
				}
			}
		}

		[HarmonyPatch(typeof(Player), "OnSpawned", new Type[] { typeof(bool) })]
		public static class Player_OnSpawned_Patch
		{
			public static void Postfix(Player __instance, bool firstSpawn)
			{
				if ((Object)(object)__instance != (Object)null && (Object)(object)__instance == (Object)(object)Player.m_localPlayer)
				{
					GlobalRecipesPlugin.Log($"[Lifecycle] Player.OnSpawned: LocalPlayer spawned (firstSpawn={firstSpawn}). Invoking HandleSpawnSync.");
					RecipeSyncManager.HandleSpawnSync();
				}
			}
		}

		[HarmonyPatch(typeof(Player), "AddKnownItem", new Type[] { typeof(ItemData) })]
		public static class Player_AddKnownItem_Patch
		{
			public static void Postfix(Player __instance, ItemData item)
			{
				if ((Object)(object)__instance != (Object)null && (Object)(object)__instance == (Object)(object)Player.m_localPlayer && item?.m_shared != null)
				{
					RecipeSyncManager.OnLocalDiscoveredMaterial(item.m_shared.m_name);
				}
			}
		}

		[HarmonyPatch(typeof(Player), "AddKnownRecipe", new Type[] { typeof(Recipe) })]
		public static class Player_AddKnownRecipe_Patch
		{
			public static void Postfix(Player __instance, Recipe recipe)
			{
				if ((Object)(object)__instance != (Object)null && (Object)(object)__instance == (Object)(object)Player.m_localPlayer && recipe?.m_item?.m_itemData?.m_shared != null)
				{
					RecipeSyncManager.OnLocalDiscoveredRecipe(recipe.m_item.m_itemData.m_shared.m_name);
				}
			}
		}

		[HarmonyPatch(typeof(Player), "AddKnownPiece", new Type[] { typeof(Piece) })]
		public static class Player_AddKnownPiece_Patch
		{
			public static void Postfix(Player __instance, Piece piece)
			{
				if ((Object)(object)__instance != (Object)null && (Object)(object)__instance == (Object)(object)Player.m_localPlayer && (Object)(object)piece != (Object)null)
				{
					RecipeSyncManager.OnLocalDiscoveredPiece(piece.m_name);
				}
			}
		}

		[HarmonyPatch(typeof(Player), "AddKnownStation", new Type[] { typeof(CraftingStation) })]
		public static class Player_AddKnownStation_Patch
		{
			public static void Postfix(Player __instance, CraftingStation station)
			{
				if ((Object)(object)__instance != (Object)null && (Object)(object)__instance == (Object)(object)Player.m_localPlayer && (Object)(object)station != (Object)null)
				{
					RecipeSyncManager.OnLocalDiscoveredStation(station.m_name, station.GetLevel(true));
				}
			}
		}

		[HarmonyPatch(typeof(Player), "AddKnownText", new Type[]
		{
			typeof(string),
			typeof(string)
		})]
		public static class Player_AddKnownText_Patch
		{
			public static void Postfix(Player __instance, string label, string text)
			{
				if ((Object)(object)__instance != (Object)null && (Object)(object)__instance == (Object)(object)Player.m_localPlayer && !string.IsNullOrEmpty(label))
				{
					RecipeSyncManager.OnLocalDiscoveredText(label, text);
				}
			}
		}

		[HarmonyPatch(typeof(MessageHud), "QueueUnlockMsg", new Type[]
		{
			typeof(Sprite),
			typeof(string),
			typeof(string)
		})]
		public static class MessageHud_QueueUnlockMsg_Patch
		{
			public static bool Prefix(Sprite icon, string topic, string description)
			{
				if (RecipeSyncManager.SuppressUnlockNotifications)
				{
					return false;
				}
				return true;
			}
		}

		[HarmonyPatch(typeof(ZNet), "SaveWorld", new Type[] { typeof(bool) })]
		public static class ZNet_SaveWorld_Patch
		{
			public static void Postfix()
			{
				if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
				{
					ServerRegistry.Save();
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "OnDestroy")]
		public static class ZNet_OnDestroy_Patch
		{
			public static void Prefix(ZNet __instance)
			{
				if ((Object)(object)__instance != (Object)null && __instance.IsServer())
				{
					ServerRegistry.Save(force: true);
					ServerRegistry.ResetSession();
				}
				RecipeSyncManager.ResetSession();
				GlobalRecipesUI.Reset();
			}
		}

		[HarmonyPatch(typeof(InventoryGui), "Awake")]
		public static class InventoryGui_Awake_Patch
		{
			public static void Postfix(InventoryGui __instance)
			{
				GlobalRecipesUI.InitializeButton(__instance);
			}
		}

		[HarmonyPatch(typeof(Terminal), "InitTerminal")]
		public static class Terminal_InitTerminal_Patch
		{
			[Serializable]
			[CompilerGenerated]
			private sealed class <>c
			{
				public static readonly <>c <>9 = new <>c();

				public static ConsoleEvent <>9__0_0;

				internal void <Postfix>b__0_0(ConsoleEventArgs args)
				{
					if (args.Length < 2)
					{
						args.Context.AddString("Usage: globalrecipes [sync|status]");
						return;
					}
					string text = args[1].ToLowerInvariant();
					if (text == "sync")
					{
						if ((Object)(object)Player.m_localPlayer == (Object)null)
						{
							args.Context.AddString("Cannot sync: No local player active.");
							return;
						}
						RecipeSyncManager.InitiateFullSync();
						args.Context.AddString("Triggered full recipe synchronization with server.");
					}
					else if (text == "status")
					{
						CatalogData localPlayerCatalog = RecipeSyncManager.GetLocalPlayerCatalog();
						args.Context.AddString($"[GlobalRecipes] Enabled: {GlobalRecipesPlugin.Instance.ModEnabled.Value}");
						args.Context.AddString($"[GlobalRecipes] Local Discoveries: {localPlayerCatalog.Materials.Count} mats, {localPlayerCatalog.Recipes.Count} recipes/pieces, {localPlayerCatalog.Stations.Count} stations, {localPlayerCatalog.Texts.Count} texts (Total: {localPlayerCatalog.TotalCount})");
						if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
						{
							CatalogData catalog = ServerRegistry.Catalog;
							args.Context.AddString("[GlobalRecipes:Server] World: '" + ServerRegistry.CurrentWorldName + "' File: " + ServerRegistry.FilePath);
							args.Context.AddString($"[GlobalRecipes:Server] World Registry: {catalog.Materials.Count} mats, {catalog.Recipes.Count} recipes/pieces, {catalog.Stations.Count} stations, {catalog.Texts.Count} texts (Total: {catalog.TotalCount})");
						}
					}
					else
					{
						args.Context.AddString("Unknown subcommand '" + args[1] + "'. Available: sync, status");
					}
				}
			}

			public static void Postfix()
			{
				//IL_0033: Unknown result type (might be due to invalid IL or missing references)
				//IL_001e: Unknown result type (might be due to invalid IL or missing references)
				//IL_0023: Unknown result type (might be due to invalid IL or missing references)
				//IL_0029: Expected O, but got Unknown
				object obj = <>c.<>9__0_0;
				if (obj == null)
				{
					ConsoleEvent val = delegate(ConsoleEventArgs args)
					{
						if (args.Length < 2)
						{
							args.Context.AddString("Usage: globalrecipes [sync|status]");
						}
						else
						{
							string text = args[1].ToLowerInvariant();
							if (text == "sync")
							{
								if ((Object)(object)Player.m_localPlayer == (Object)null)
								{
									args.Context.AddString("Cannot sync: No local player active.");
								}
								else
								{
									RecipeSyncManager.InitiateFullSync();
									args.Context.AddString("Triggered full recipe synchronization with server.");
								}
							}
							else if (text == "status")
							{
								CatalogData localPlayerCatalog = RecipeSyncManager.GetLocalPlayerCatalog();
								args.Context.AddString($"[GlobalRecipes] Enabled: {GlobalRecipesPlugin.Instance.ModEnabled.Value}");
								args.Context.AddString($"[GlobalRecipes] Local Discoveries: {localPlayerCatalog.Materials.Count} mats, {localPlayerCatalog.Recipes.Count} recipes/pieces, {localPlayerCatalog.Stations.Count} stations, {localPlayerCatalog.Texts.Count} texts (Total: {localPlayerCatalog.TotalCount})");
								if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
								{
									CatalogData catalog = ServerRegistry.Catalog;
									args.Context.AddString("[GlobalRecipes:Server] World: '" + ServerRegistry.CurrentWorldName + "' File: " + ServerRegistry.FilePath);
									args.Context.AddString($"[GlobalRecipes:Server] World Registry: {catalog.Materials.Count} mats, {catalog.Recipes.Count} recipes/pieces, {catalog.Stations.Count} stations, {catalog.Texts.Count} texts (Total: {catalog.TotalCount})");
								}
							}
							else
							{
								args.Context.AddString("Unknown subcommand '" + args[1] + "'. Available: sync, status");
							}
						}
					};
					<>c.<>9__0_0 = val;
					obj = (object)val;
				}
				new ConsoleCommand("globalrecipes", "GlobalRecipes commands (sync, status)", (ConsoleEvent)obj, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false);
			}
		}
	}
	public static class RecipeSyncManager
	{
		public const string RPC_RequestFullSync = "GlobalRecipes_RequestFullSync";

		public const string RPC_FullSync = "GlobalRecipes_FullSync";

		public const string RPC_DeltaSync = "GlobalRecipes_DeltaSync";

		private static readonly CatalogData _pendingDelta = new CatalogData();

		private static readonly object _pendingLock = new object();

		private static float _nextDebounceTime = -1f;

		private static float _deferredFullSyncTime = -1f;

		private static float _nextPeriodicSyncTime = -1f;

		private static bool _hasCompletedInitialSync;

		private static ZRoutedRpc _lastRegisteredRpc;

		public static bool IsApplyingSync { get; set; }

		public static bool SuppressUnlockNotifications { get; set; }

		public static void HandleSpawnSync()
		{
			if (!_hasCompletedInitialSync)
			{
				_hasCompletedInitialSync = true;
				InitiateFullSync();
			}
		}

		public static void RegisterRPCs()
		{
			if (ZRoutedRpc.instance != null && _lastRegisteredRpc != ZRoutedRpc.instance)
			{
				_lastRegisteredRpc = ZRoutedRpc.instance;
				ZRoutedRpc.instance.Register<ZPackage>("GlobalRecipes_RequestFullSync", (Action<long, ZPackage>)RPC_HandleRequestFullSync);
				ZRoutedRpc.instance.Register<ZPackage>("GlobalRecipes_FullSync", (Action<long, ZPackage>)RPC_HandleFullSync);
				ZRoutedRpc.instance.Register<ZPackage>("GlobalRecipes_DeltaSync", (Action<long, ZPackage>)RPC_HandleDeltaSync);
				GlobalRecipesPlugin.Log("Registered GlobalRecipes RPC handlers.");
			}
		}

		public static void InitiateFullSync()
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Expected O, but got Unknown
			if ((Object)(object)Player.m_localPlayer == (Object)null || ZRoutedRpc.instance == null)
			{
				return;
			}
			CatalogData localPlayerCatalog = GetLocalPlayerCatalog();
			ZPackage val = new ZPackage();
			ZPackageSerialization.Write(val, localPlayerCatalog);
			long num = ZRoutedRpc.instance.GetServerPeerID();
			if (num == 0L)
			{
				if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer())
				{
					GlobalRecipesPlugin.LogWarning("Server peer not ready; deferring full sync.");
					_deferredFullSyncTime = Time.time + 2.5f;
					return;
				}
				num = ZNet.GetUID();
			}
			_deferredFullSyncTime = -1f;
			ZRoutedRpc.instance.InvokeRoutedRPC(num, "GlobalRecipes_RequestFullSync", new object[1] { val });
			GlobalRecipesPlugin.Log($"[Client:FullSync] Dispatched full sync request with {localPlayerCatalog.TotalCount} local discoveries (mats:{localPlayerCatalog.Materials.Count}, recs:{localPlayerCatalog.Recipes.Count}, stations:{localPlayerCatalog.Stations.Count}, texts:{localPlayerCatalog.Texts.Count}) to server peer {num}.");
		}

		public static void Update()
		{
			if ((Object)(object)Player.m_localPlayer == (Object)null || ZRoutedRpc.instance == null)
			{
				return;
			}
			if (_deferredFullSyncTime > 0f && Time.time >= _deferredFullSyncTime)
			{
				_deferredFullSyncTime = -1f;
				GlobalRecipesPlugin.Log("[Client:FullSync] Executing deferred full sync now that network is active.");
				InitiateFullSync();
			}
			if (_nextDebounceTime > 0f && Time.time >= _nextDebounceTime)
			{
				CatalogData catalogData = null;
				lock (_pendingLock)
				{
					if (!_pendingDelta.IsEmpty)
					{
						catalogData = new CatalogData();
						catalogData.MergeFrom(_pendingDelta);
						_pendingDelta.Clear();
					}
					_nextDebounceTime = -1f;
				}
				if (catalogData != null && !catalogData.IsEmpty)
				{
					DispatchDelta(catalogData);
				}
			}
			float value = GlobalRecipesPlugin.Instance.SyncIntervalSeconds.Value;
			if (value > 0f)
			{
				if (_nextPeriodicSyncTime < 0f)
				{
					_nextPeriodicSyncTime = Time.time + value + Random.Range(0f, value * 0.2f);
				}
				else if (Time.time >= _nextPeriodicSyncTime)
				{
					_nextPeriodicSyncTime = Time.time + value + Random.Range((0f - value) * 0.1f, value * 0.1f);
					InitiateFullSync();
				}
			}
		}

		public static void OnLocalDiscoveredMaterial(string materialName)
		{
			if (string.IsNullOrEmpty(materialName) || IsApplyingSync)
			{
				return;
			}
			lock (_pendingLock)
			{
				_pendingDelta.Materials.Add(materialName);
				ScheduleDebounce();
			}
		}

		public static void OnLocalDiscoveredRecipe(string recipeName)
		{
			if (string.IsNullOrEmpty(recipeName) || IsApplyingSync)
			{
				return;
			}
			lock (_pendingLock)
			{
				_pendingDelta.Recipes.Add(recipeName);
				ScheduleDebounce();
			}
		}

		public static void OnLocalDiscoveredPiece(string pieceName)
		{
			if (string.IsNullOrEmpty(pieceName) || IsApplyingSync)
			{
				return;
			}
			lock (_pendingLock)
			{
				_pendingDelta.Recipes.Add(pieceName);
				ScheduleDebounce();
			}
		}

		public static void OnLocalDiscoveredStation(string stationName, int level)
		{
			if (string.IsNullOrEmpty(stationName) || IsApplyingSync)
			{
				return;
			}
			lock (_pendingLock)
			{
				if (!_pendingDelta.Stations.TryGetValue(stationName, out var value) || level > value)
				{
					_pendingDelta.Stations[stationName] = level;
				}
				ScheduleDebounce();
			}
		}

		public static void OnLocalDiscoveredText(string label, string text)
		{
			if (string.IsNullOrEmpty(label) || IsApplyingSync)
			{
				return;
			}
			lock (_pendingLock)
			{
				_pendingDelta.Texts[label] = text ?? string.Empty;
				ScheduleDebounce();
			}
		}

		private static void ScheduleDebounce()
		{
			float value = GlobalRecipesPlugin.Instance.DebounceSeconds.Value;
			_nextDebounceTime = Time.time + Mathf.Max(0.5f, value);
		}

		private static void DispatchDelta(CatalogData delta)
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Expected O, but got Unknown
			if (delta == null || delta.IsEmpty || ZRoutedRpc.instance == null)
			{
				return;
			}
			ZPackage val = new ZPackage();
			ZPackageSerialization.Write(val, delta);
			long num = ZRoutedRpc.instance.GetServerPeerID();
			if (num == 0L)
			{
				if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer())
				{
					GlobalRecipesPlugin.LogWarning("Server peer not ready; deferring delta dispatch.");
					lock (_pendingLock)
					{
						_pendingDelta.MergeFrom(delta);
						ScheduleDebounce();
						return;
					}
				}
				num = ZNet.GetUID();
			}
			ZRoutedRpc.instance.InvokeRoutedRPC(num, "GlobalRecipes_DeltaSync", new object[1] { val });
			GlobalRecipesPlugin.Log($"[Client:DeltaSync] Dispatched delta of {delta.TotalCount} discoveries (mats:{delta.Materials.Count}, recs:{delta.Recipes.Count}, stations:{delta.Stations.Count}, texts:{delta.Texts.Count}) to server peer {num}.");
		}

		private static void RPC_HandleRequestFullSync(long sender, ZPackage pkg)
		{
			//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fa: Expected O, but got Unknown
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
			{
				return;
			}
			if (sender != ZNet.GetUID() && ZNet.instance.GetPeer(sender) == null)
			{
				GlobalRecipesPlugin.LogWarning($"[Server:Security] Rejected unauthorized RPC_RequestFullSync from unauthenticated peer {sender}.");
				return;
			}
			ServerRegistry.Initialize(ZNet.instance.GetWorldName());
			CatalogData catalogData = ZPackageSerialization.Read(pkg);
			GlobalRecipesPlugin.Log($"[Server:FullSync] Received full sync handshake from peer {sender} with {catalogData.TotalCount} discoveries.");
			if (ServerRegistry.Merge(catalogData, out var newContributions))
			{
				ServerRegistry.RequestSave();
				GlobalRecipesPlugin.Log($"[Server:Registry] Merged {newContributions.TotalCount} new contributions from peer {sender}. Total world recipes: {ServerRegistry.Catalog.TotalCount}.");
				BroadcastDeltaToPeers(newContributions, sender);
				if ((Object)(object)Player.m_localPlayer != (Object)null && sender != ZNet.GetUID())
				{
					ApplyToLocalPlayer(newContributions);
				}
			}
			else
			{
				GlobalRecipesPlugin.Log($"[Server:Registry] Peer {sender} contributed 0 new items to world registry.");
			}
			ZPackage val = new ZPackage();
			CatalogData catalogSnapshot = ServerRegistry.GetCatalogSnapshot();
			ZPackageSerialization.Write(val, catalogSnapshot);
			ZRoutedRpc.instance.InvokeRoutedRPC(sender, "GlobalRecipes_FullSync", new object[1] { val });
			GlobalRecipesPlugin.Log($"[Server:FullSync] Dispatched authoritative catalog ({catalogSnapshot.TotalCount} items) to peer {sender}.");
		}

		private static void RPC_HandleDeltaSync(long sender, ZPackage pkg)
		{
			if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer())
			{
				long num = ((ZRoutedRpc.instance != null) ? ZRoutedRpc.instance.GetServerPeerID() : 0);
				if (num == 0L || sender != num)
				{
					GlobalRecipesPlugin.LogWarning($"[Client:Security] Rejected unauthorized RPC_DeltaSync from non-server peer {sender}.");
					return;
				}
			}
			CatalogData catalogData = ZPackageSerialization.Read(pkg);
			if (catalogData == null || catalogData.IsEmpty)
			{
				return;
			}
			if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
			{
				if (sender != ZNet.GetUID() && ZNet.instance.GetPeer(sender) == null)
				{
					GlobalRecipesPlugin.LogWarning($"[Server:Security] Rejected RPC_DeltaSync from unauthenticated/unknown peer {sender}.");
					return;
				}
				ServerRegistry.Initialize(ZNet.instance.GetWorldName());
				GlobalRecipesPlugin.Log($"[Server:DeltaSync] Received delta batch of {catalogData.TotalCount} items from peer {sender}.");
				if (ServerRegistry.Merge(catalogData, out var newContributions))
				{
					ServerRegistry.RequestSave();
					GlobalRecipesPlugin.Log($"[Server:Registry] Merged {newContributions.TotalCount} new items from peer {sender}. Broadcasting to peers.");
					BroadcastDeltaToPeers(newContributions, sender);
					if ((Object)(object)Player.m_localPlayer != (Object)null && sender != ZNet.GetUID())
					{
						ApplyToLocalPlayer(newContributions);
					}
				}
				else
				{
					GlobalRecipesPlugin.Log($"[Server:DeltaSync] Delta from peer {sender} contained already-known items. No broadcast required.");
				}
			}
			else
			{
				GlobalRecipesPlugin.Log($"[Client:DeltaSync] Received server delta broadcast of {catalogData.TotalCount} items from server {sender}.");
				ApplyToLocalPlayer(catalogData);
			}
		}

		private static void RPC_HandleFullSync(long sender, ZPackage pkg)
		{
			if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer())
			{
				long num = ((ZRoutedRpc.instance != null) ? ZRoutedRpc.instance.GetServerPeerID() : 0);
				if (num == 0L || sender != num)
				{
					GlobalRecipesPlugin.LogWarning($"[Client:Security] Rejected unauthorized RPC_FullSync from non-server peer {sender}.");
					return;
				}
			}
			CatalogData catalogData = ZPackageSerialization.Read(pkg);
			GlobalRecipesPlugin.Log($"[Client:FullSync] Received full catalog ({catalogData.TotalCount} items) from server peer {sender}.");
			ApplyToLocalPlayer(catalogData);
		}

		private static void BroadcastDeltaToPeers(CatalogData delta, long excludeSender)
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Expected O, but got Unknown
			if (delta == null || delta.IsEmpty || (Object)(object)ZNet.instance == (Object)null)
			{
				return;
			}
			List<ZNetPeer> peers = ZNet.instance.GetPeers();
			if (peers == null || peers.Count == 0)
			{
				return;
			}
			ZNetPeer[] array = peers.ToArray();
			ZPackage val = new ZPackage();
			ZPackageSerialization.Write(val, delta);
			int num = 0;
			ZNetPeer[] array2 = array;
			foreach (ZNetPeer val2 in array2)
			{
				if (val2 != null && val2.IsReady() && val2.m_uid != 0L && val2.m_uid != excludeSender)
				{
					ZRoutedRpc.instance.InvokeRoutedRPC(val2.m_uid, "GlobalRecipes_DeltaSync", new object[1] { val });
					num++;
				}
			}
			GlobalRecipesPlugin.Log($"[Server:Broadcast] Broadcasted delta ({delta.TotalCount} items) to {num} connected peer(s) (excluded sender: {excludeSender}).");
		}

		public static int ApplyToLocalPlayer(CatalogData catalog)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null || catalog == null || catalog.IsEmpty)
			{
				return 0;
			}
			int num = CountNewUnlocks(localPlayer, catalog);
			if (num == 0)
			{
				return 0;
			}
			int value = GlobalRecipesPlugin.Instance.SuppressThreshold.Value;
			SuppressUnlockNotifications = num >= value;
			IsApplyingSync = true;
			try
			{
				if (localPlayer.m_knownMaterial != null)
				{
					foreach (string material in catalog.Materials)
					{
						localPlayer.m_knownMaterial.Add(material);
					}
				}
				if (localPlayer.m_knownRecipes != null)
				{
					foreach (string recipe in catalog.Recipes)
					{
						localPlayer.m_knownRecipes.Add(recipe);
					}
				}
				if (localPlayer.m_knownStations != null)
				{
					foreach (KeyValuePair<string, int> station in catalog.Stations)
					{
						if (!localPlayer.m_knownStations.TryGetValue(station.Key, out var value2) || station.Value > value2)
						{
							localPlayer.m_knownStations[station.Key] = station.Value;
						}
					}
				}
				if (localPlayer.m_knownTexts != null)
				{
					foreach (KeyValuePair<string, string> text in catalog.Texts)
					{
						if (!localPlayer.m_knownTexts.ContainsKey(text.Key))
						{
							localPlayer.m_knownTexts[text.Key] = text.Value;
						}
					}
				}
				if ((Object)(object)ObjectDB.instance != (Object)null)
				{
					localPlayer.UpdateKnownRecipesList();
				}
				localPlayer.UpdateAvailablePiecesList();
				localPlayer.UpdateEvents();
			}
			finally
			{
				IsApplyingSync = false;
				SuppressUnlockNotifications = false;
			}
			if (InventoryGui.IsVisible() && (Object)(object)InventoryGui.instance != (Object)null)
			{
				InventoryGui.instance.UpdateCraftingPanel(false);
			}
			if (GlobalRecipesPlugin.Instance.ShowSyncNotification.Value && (Object)(object)MessageHud.instance != (Object)null)
			{
				MessageHud.instance.ShowMessage((MessageType)1, $"[GlobalRecipes] Synced {num} new discoveries", 0, (Sprite)null, false, true);
			}
			GlobalRecipesPlugin.Log($"Applied {num} new discoveries to local player.");
			return num;
		}

		public static int CountNewUnlocks(Player player, CatalogData catalog)
		{
			if ((Object)(object)player == (Object)null || catalog == null)
			{
				return 0;
			}
			int num = 0;
			if (player.m_knownMaterial != null)
			{
				foreach (string material in catalog.Materials)
				{
					if (!player.m_knownMaterial.Contains(material))
					{
						num++;
					}
				}
			}
			if (player.m_knownRecipes != null)
			{
				foreach (string recipe in catalog.Recipes)
				{
					if (!player.m_knownRecipes.Contains(recipe))
					{
						num++;
					}
				}
			}
			if (player.m_knownStations != null)
			{
				foreach (KeyValuePair<string, int> station in catalog.Stations)
				{
					if (!player.m_knownStations.TryGetValue(station.Key, out var value) || station.Value > value)
					{
						num++;
					}
				}
			}
			if (player.m_knownTexts != null)
			{
				foreach (KeyValuePair<string, string> text in catalog.Texts)
				{
					if (!player.m_knownTexts.ContainsKey(text.Key))
					{
						num++;
					}
				}
			}
			return num;
		}

		public static CatalogData GetLocalPlayerCatalog()
		{
			CatalogData catalogData = new CatalogData();
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return catalogData;
			}
			if (localPlayer.m_knownMaterial != null)
			{
				foreach (string item in localPlayer.m_knownMaterial)
				{
					catalogData.Materials.Add(item);
				}
			}
			if (localPlayer.m_knownRecipes != null)
			{
				foreach (string knownRecipe in localPlayer.m_knownRecipes)
				{
					catalogData.Recipes.Add(knownRecipe);
				}
			}
			if (localPlayer.m_knownStations != null)
			{
				foreach (KeyValuePair<string, int> knownStation in localPlayer.m_knownStations)
				{
					catalogData.Stations[knownStation.Key] = knownStation.Value;
				}
			}
			if (localPlayer.m_knownTexts != null)
			{
				foreach (KeyValuePair<string, string> knownText in localPlayer.m_knownTexts)
				{
					catalogData.Texts[knownText.Key] = knownText.Value;
				}
			}
			return catalogData;
		}

		public static void ResetSession()
		{
			lock (_pendingLock)
			{
				_pendingDelta.Clear();
				_nextDebounceTime = -1f;
			}
			_deferredFullSyncTime = -1f;
			_nextPeriodicSyncTime = -1f;
			_hasCompletedInitialSync = false;
			IsApplyingSync = false;
			SuppressUnlockNotifications = false;
		}
	}
	public static class ServerRegistry
	{
		private static readonly object _lock = new object();

		private static string _currentWorldName;

		private static string _filePath;

		private static CatalogData _catalog = new CatalogData();

		private static bool _isDirty;

		private static long _catalogVersion;

		private static long _persistedVersion;

		private static bool _saveScheduled;

		private static readonly object _saveLock = new object();

		private static Task _currentSaveTask;

		public static CatalogData Catalog => GetCatalogSnapshot();

		public static string CurrentWorldName => _currentWorldName;

		public static string FilePath => _filePath;

		public static bool IsDirty => _isDirty;

		public static long CatalogVersion => _catalogVersion;

		public static long PersistedVersion => _persistedVersion;

		public static void Initialize(string worldName, string customDirectory = null)
		{
			lock (_lock)
			{
				worldName = worldName?.Trim();
				if (string.IsNullOrWhiteSpace(worldName))
				{
					worldName = "Default";
				}
				if (!string.Equals(_currentWorldName, worldName, StringComparison.OrdinalIgnoreCase) || _filePath == null)
				{
					_currentWorldName = worldName;
					string text = (string.IsNullOrEmpty(customDirectory) ? Path.Combine((!string.IsNullOrEmpty(Paths.ConfigPath)) ? Paths.ConfigPath : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config"), "GlobalRecipes") : customDirectory);
					if (!Directory.Exists(text))
					{
						Directory.CreateDirectory(text);
					}
					string text2 = SanitizeFileName(worldName);
					_filePath = Path.Combine(text, text2 + "_recipes.json");
					Load();
				}
			}
		}

		public static void Load()
		{
			lock (_lock)
			{
				if (string.IsNullOrEmpty(_filePath) || !File.Exists(_filePath))
				{
					_catalog = new CatalogData();
					_isDirty = false;
					_catalogVersion = 0L;
					_persistedVersion = 0L;
					return;
				}
				try
				{
					_catalog = JsonConvert.DeserializeObject<CatalogData>(File.ReadAllText(_filePath)) ?? new CatalogData();
					_isDirty = false;
					_catalogVersion = 1L;
					_persistedVersion = 1L;
					GlobalRecipesPlugin.Log($"Loaded {_catalog.TotalCount} global recipes for world '{_currentWorldName}'.");
				}
				catch (Exception ex)
				{
					GlobalRecipesPlugin.LogWarning("Failed to read world recipe registry from " + _filePath + ": " + ex.Message + ". Starting fresh.");
					try
					{
						string text = _filePath + $".corrupt.{DateTime.UtcNow:yyyyMMddHHmmss}";
						File.Copy(_filePath, text, overwrite: true);
						GlobalRecipesPlugin.LogWarning("Archived corrupted catalog to " + text);
					}
					catch
					{
					}
					_catalog = new CatalogData();
					_isDirty = false;
					_catalogVersion = 0L;
					_persistedVersion = 0L;
				}
			}
		}

		public static bool Merge(CatalogData incoming, out CatalogData newContributions)
		{
			lock (_lock)
			{
				newContributions = new CatalogData();
				if (incoming == null || incoming.IsEmpty)
				{
					return false;
				}
				newContributions = incoming.ComputeDelta(_catalog);
				if (newContributions.IsEmpty)
				{
					return false;
				}
				_catalog.MergeFrom(newContributions);
				_isDirty = true;
				_catalogVersion++;
				return true;
			}
		}

		public static CatalogData GetCatalogSnapshot()
		{
			lock (_lock)
			{
				CatalogData catalogData = new CatalogData();
				catalogData.MergeFrom(_catalog);
				return catalogData;
			}
		}

		public static void RequestSave()
		{
			lock (_lock)
			{
				if (!_isDirty || string.IsNullOrEmpty(_filePath) || _saveScheduled)
				{
					return;
				}
				_saveScheduled = true;
				CatalogData snapshot = GetCatalogSnapshot();
				string targetPath = _filePath;
				long snapshotVersion = _catalogVersion;
				_currentSaveTask = Task.Run(delegate
				{
					lock (_saveLock)
					{
						lock (_lock)
						{
							if (_filePath == null || _filePath != targetPath || snapshotVersion <= _persistedVersion)
							{
								_saveScheduled = false;
								return;
							}
						}
						try
						{
							string contents = JsonConvert.SerializeObject((object)snapshot, (Formatting)1);
							string text = targetPath + ".tmp";
							File.WriteAllText(text, contents);
							if (File.Exists(targetPath))
							{
								try
								{
									File.Replace(text, targetPath, null);
								}
								catch (Exception)
								{
									File.Copy(text, targetPath, overwrite: true);
									File.Delete(text);
								}
							}
							else
							{
								File.Move(text, targetPath);
							}
							lock (_lock)
							{
								if (_filePath == null || _filePath != targetPath)
								{
									_saveScheduled = false;
								}
								else
								{
									_persistedVersion = Math.Max(_persistedVersion, snapshotVersion);
									_saveScheduled = false;
									if (_catalogVersion == snapshotVersion)
									{
										_isDirty = false;
									}
									else
									{
										RequestSave();
									}
								}
							}
						}
						catch (Exception ex2)
						{
							GlobalRecipesPlugin.LogWarning("Asynchronous save failed: " + ex2.Message);
							lock (_lock)
							{
								_saveScheduled = false;
							}
						}
					}
				});
			}
		}

		public static void Save(bool force = false)
		{
			Task task = null;
			lock (_lock)
			{
				task = _currentSaveTask;
			}
			task?.Wait(3000);
			lock (_saveLock)
			{
				CatalogData catalogSnapshot;
				string filePath;
				long catalogVersion;
				lock (_lock)
				{
					if ((!_isDirty && !force) || string.IsNullOrEmpty(_filePath))
					{
						return;
					}
					catalogSnapshot = GetCatalogSnapshot();
					filePath = _filePath;
					catalogVersion = _catalogVersion;
				}
				try
				{
					string contents = JsonConvert.SerializeObject((object)catalogSnapshot, (Formatting)1);
					string text = filePath + ".tmp";
					File.WriteAllText(text, contents);
					if (File.Exists(filePath))
					{
						try
						{
							File.Replace(text, filePath, null);
						}
						catch (Exception)
						{
							File.Copy(text, filePath, overwrite: true);
							File.Delete(text);
						}
					}
					else
					{
						File.Move(text, filePath);
					}
					lock (_lock)
					{
						if (_filePath == filePath)
						{
							_persistedVersion = Math.Max(_persistedVersion, catalogVersion);
							_saveScheduled = false;
							if (_catalogVersion == catalogVersion)
							{
								_isDirty = false;
							}
						}
					}
					GlobalRecipesPlugin.Log($"Persisted {catalogSnapshot.TotalCount} world recipes to {filePath}");
				}
				catch (Exception ex2)
				{
					GlobalRecipesPlugin.LogWarning("Failed to save world recipes to " + filePath + ": " + ex2.Message);
				}
			}
		}

		public static void ResetSession()
		{
			Save(force: true);
			lock (_lock)
			{
				_currentWorldName = null;
				_filePath = null;
				_catalog = new CatalogData();
				_isDirty = false;
				_saveScheduled = false;
				_catalogVersion = 0L;
				_persistedVersion = 0L;
			}
		}

		public static string SanitizeFileName(string name)
		{
			name = name?.Trim();
			if (string.IsNullOrWhiteSpace(name))
			{
				return "Default";
			}
			char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
			foreach (char oldChar in invalidFileNameChars)
			{
				name = name.Replace(oldChar, '_');
			}
			return name;
		}
	}
	public static class ZPackageSerialization
	{
		public const int MaxCollectionCount = 10000;

		public const int MaxKeyLength = 128;

		public const int MaxTextLength = 4096;

		public const int MaxStationLevel = 50;

		public static void Write(ZPackage pkg, CatalogData catalog)
		{
			if (pkg == null)
			{
				return;
			}
			catalog = catalog ?? new CatalogData();
			pkg.Write(Math.Min(catalog.Materials.Count, 10000));
			int num = 0;
			foreach (string material in catalog.Materials)
			{
				if (num++ >= 10000)
				{
					break;
				}
				pkg.Write(material ?? string.Empty);
			}
			pkg.Write(Math.Min(catalog.Recipes.Count, 10000));
			num = 0;
			foreach (string recipe in catalog.Recipes)
			{
				if (num++ >= 10000)
				{
					break;
				}
				pkg.Write(recipe ?? string.Empty);
			}
			pkg.Write(Math.Min(catalog.Stations.Count, 10000));
			num = 0;
			foreach (KeyValuePair<string, int> station in catalog.Stations)
			{
				if (num++ >= 10000)
				{
					break;
				}
				pkg.Write(station.Key ?? string.Empty);
				pkg.Write(Math.Min(Math.Max(1, station.Value), 50));
			}
			pkg.Write(Math.Min(catalog.Texts.Count, 10000));
			num = 0;
			foreach (KeyValuePair<string, string> text in catalog.Texts)
			{
				if (num++ >= 10000)
				{
					break;
				}
				pkg.Write(text.Key ?? string.Empty);
				pkg.Write(text.Value ?? string.Empty);
			}
		}

		public static CatalogData Read(ZPackage pkg)
		{
			CatalogData catalogData = new CatalogData();
			if (pkg == null)
			{
				return catalogData;
			}
			try
			{
				int num = pkg.Size() - pkg.GetPos();
				int num2 = pkg.ReadInt();
				if (num2 < 0 || num2 > 10000 || num2 > num)
				{
					GlobalRecipesPlugin.LogWarning($"Malformed material count {num2}; aborting read.");
					return catalogData;
				}
				for (int i = 0; i < num2; i++)
				{
					string text = pkg.ReadString();
					if (!string.IsNullOrEmpty(text) && text.Length <= 128)
					{
						catalogData.Materials.Add(text);
					}
				}
				num = pkg.Size() - pkg.GetPos();
				int num3 = pkg.ReadInt();
				if (num3 < 0 || num3 > 10000 || num3 > num)
				{
					GlobalRecipesPlugin.LogWarning($"Malformed recipe count {num3}; aborting read.");
					return catalogData;
				}
				for (int j = 0; j < num3; j++)
				{
					string text2 = pkg.ReadString();
					if (!string.IsNullOrEmpty(text2) && text2.Length <= 128)
					{
						catalogData.Recipes.Add(text2);
					}
				}
				num = pkg.Size() - pkg.GetPos();
				int num4 = pkg.ReadInt();
				if (num4 < 0 || num4 > 10000 || num4 > num)
				{
					GlobalRecipesPlugin.LogWarning($"Malformed station count {num4}; aborting read.");
					return catalogData;
				}
				for (int k = 0; k < num4; k++)
				{
					string text3 = pkg.ReadString();
					int num5 = pkg.ReadInt();
					if (!string.IsNullOrEmpty(text3) && text3.Length <= 128 && num5 >= 1 && num5 <= 50)
					{
						catalogData.Stations[text3] = num5;
					}
				}
				num = pkg.Size() - pkg.GetPos();
				int num6 = pkg.ReadInt();
				if (num6 < 0 || num6 > 10000 || num6 > num)
				{
					GlobalRecipesPlugin.LogWarning($"Malformed text count {num6}; aborting read.");
					return catalogData;
				}
				for (int l = 0; l < num6; l++)
				{
					string text4 = pkg.ReadString();
					string text5 = pkg.ReadString();
					if (!string.IsNullOrEmpty(text4) && text4.Length <= 128 && text5 != null && text5.Length <= 4096)
					{
						catalogData.Texts[text4] = text5;
					}
				}
			}
			catch (Exception ex)
			{
				GlobalRecipesPlugin.LogWarning("Failed to deserialize catalog from ZPackage: " + ex.Message);
			}
			return catalogData;
		}
	}
}