Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of GlobalRecipes v1.0.0
GlobalRecipes.dll
Decompiled 2 days agousing 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; } } }