diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e38bd5..8f6d3ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,3 +66,13 @@ - Open the passport when pressing the "Open Passport" keybind (default: P) - Removed a leftover debug keybind on the BingBong module that was also bound to P and would have fired alongside the new passport keybind. + +# v0.1.13 | Fix for PEAK update + +- Rebuilt against the September PEAK update. Two game methods gained an optional parameter, which compiles fine but leaves an older build calling a signature that no longer exists, so both threw a MissingMethodException at runtime: + - `Player.EmptySlot(slot)` became `EmptySlot(slot, bool andBroadcast = true)`. This killed the whole "Toggle Bugle" keybind (default: V) — both giving the Bugle and swapping it for the Megaphone. + - `PhotonNetwork.InstantiateItemRoom(name, position, rotation)` gained a fourth `bool warnIfNotHost = true`. This one threw inside the Bags for Everyone postfix on `SingleItemSpawner.TrySpawnItems`, and because a throwing postfix propagates out of the method it patched, it took down every `SingleItemSpawner` in the level rather than just the backpack one. Backpacks and BingBong stopped spawning, and lighting a campfire no longer advanced the biome: the spawn loop runs inside `MapHandler`'s transition coroutine, so the throw killed that coroutine and the campfire items, day/night blend, biome title and fog reveal all silently never happened. +- Removed the manual `RPCRemoveItemFromSlot` call the Bugle keybind made right after `EmptySlot`. `EmptySlot` already forwards the removal to the host itself, so the extra call only logged "Only Master Client can remove items!" on every press for anyone who wasn't hosting. +- The "Open Backpack" keybind (default: B) no longer opens the wheel while the backpack is held in your hands. The wheel pulls items out of the backpack's on-back visuals, which the game deactivates while the backpack is in hand, so anything taken out was spawned into a disabled rig and disappeared. Hold-interacting the held backpack still opens it the normal way. +- Hardened the Bags for Everyone patch. Its master-client and spawner-name checks now run in the postfix itself instead of inside the method that spawns the bags, and the spawn is wrapped in a try/catch. Because Mono resolves a method's call targets when it JITs that method, a changed game API used to throw before any of those checks could run, taking down every `SingleItemSpawner` in the level — which is what stopped campfires from advancing the biome in 0.1.12. A failure now costs the extra bags and nothing else. +- The extra backpacks are registered with the spawner's `SpawnedItemTracker`, so a quicksave records them instead of restoring only the vanilla one, and the patch no longer adds a fresh batch on top every time a run is loaded from a save. diff --git a/src/JordanMod/JordanMod.csproj b/src/JordanMod/JordanMod.csproj index e43c6f8..59b3e74 100644 --- a/src/JordanMod/JordanMod.csproj +++ b/src/JordanMod/JordanMod.csproj @@ -8,7 +8,7 @@ JordanMod - 0.1.12 + 0.1.13 diff --git a/src/JordanMod/modules/easy_backpack/EasyBackpackModule.cs b/src/JordanMod/modules/easy_backpack/EasyBackpackModule.cs index 4e9c583..6ab5bb7 100644 --- a/src/JordanMod/modules/easy_backpack/EasyBackpackModule.cs +++ b/src/JordanMod/modules/easy_backpack/EasyBackpackModule.cs @@ -1,5 +1,6 @@ using System; using UnityEngine; +using Zorro.Core; namespace JordanMod.Modules.EasyBackpack; @@ -54,12 +55,26 @@ class EasyBackpackModule : Module } else return; + // The wheel we open is the one for the backpack worn on the target's back, and its + // on-back visuals are what BackpackWheel.Choose() pulls items out of. While the + // backpack is held in hand (its slot, ID 3, is the equipped one) CharacterBackpackHandler + // deactivates those visuals, so anything taken out of the wheel is spawned into a + // disabled rig and vanishes. Refuse to open in that state; the game's own + // hold-interact on the held backpack still works and opens the correct wheel. + if (IsBackpackInHands(targetCharacter)) return; + BackpackReference backpackRefs = BackpackReference.GetFromEquippedBackpack(targetCharacter); int slotCount = (targetSlot.prefab as Backpack)?.slotCount ?? 4; GUIManager.instance.OpenBackpackWheel(backpackRefs, slotCount, targetSlot.backpackType); _isBackpackOpen = true; } + private static bool IsBackpackInHands(Character character) + { + Optionable selectedSlot = character.refs.items.currentSelectedSlot; + return selectedSlot.IsSome && selectedSlot.Value == character.player.backpackSlot.itemSlotID; + } + private void CloseBackpack() { if (!Application.isFocused) return; diff --git a/src/JordanMod/modules/stashed_bugle/StashedBugleModule.cs b/src/JordanMod/modules/stashed_bugle/StashedBugleModule.cs index 83d3e86..4a31cc6 100644 --- a/src/JordanMod/modules/stashed_bugle/StashedBugleModule.cs +++ b/src/JordanMod/modules/stashed_bugle/StashedBugleModule.cs @@ -31,17 +31,12 @@ class StashedBugleModule : Module { if (heldItem.UIData.itemName == _bugleItemName) { - localCharacter.refs.items.DestroyHeldItemRpc(); - localCharacter.player.EmptySlot(localCharacter.refs.items.currentSelectedSlot); - localCharacter.player.RPCRemoveItemFromSlot(localCharacter.refs.items.currentSelectedSlot.Value); - + ClearHeldSlot(localCharacter); SpawnItemByDisplayName(localCharacter, _megaphoneItemName); } else if (heldItem.UIData.itemName == _megaphoneItemName) { - localCharacter.refs.items.DestroyHeldItemRpc(); - localCharacter.player.EmptySlot(localCharacter.refs.items.currentSelectedSlot); - localCharacter.player.RPCRemoveItemFromSlot(localCharacter.refs.items.currentSelectedSlot.Value); + ClearHeldSlot(localCharacter); } } else if (heldItem == null) @@ -59,6 +54,16 @@ class StashedBugleModule : Module } } + // Player.EmptySlot already tells the host to clear the slot (it RPCs + // RPCRemoveItemFromSlot to the master client when we aren't the host), so calling + // RPCRemoveItemFromSlot ourselves on top of it only logged "Only Master Client can + // remove items!" on every non-host press. + private static void ClearHeldSlot(Character character) + { + character.refs.items.DestroyHeldItemRpc(); + character.player.EmptySlot(character.refs.items.currentSelectedSlot); + } + // SpawnItemInHand expects the item's Resources prefab name, which is not // guaranteed to match its UIData.itemName display string, so resolve it // via the ItemDatabase instead of assuming the two are identical. diff --git a/src/JordanMod/patches/BagsForEveryonePatch.cs b/src/JordanMod/patches/BagsForEveryonePatch.cs index e318a30..3795511 100644 --- a/src/JordanMod/patches/BagsForEveryonePatch.cs +++ b/src/JordanMod/patches/BagsForEveryonePatch.cs @@ -1,4 +1,7 @@ +using System; +using System.Collections.Generic; using HarmonyLib; +using Peak; using Photon.Pun; using UnityEngine; @@ -7,34 +10,48 @@ namespace JordanMod.Modules.BagsForEveryone; public class BagsForEveryonePatch { - [HarmonyPatch(typeof(SingleItemSpawner), "TrySpawnItems")] - [HarmonyPostfix] - static void Postfix(SingleItemSpawner __instance) - { - ForEachPlayer(__instance); - } + private const string BackpackSpawnerName = "Backpack_Spawner"; + private const string FirstBiomeName = "Biome_1"; - private static void ForEachPlayer(SingleItemSpawner spawner) + [HarmonyPatch(typeof(SingleItemSpawner), "TrySpawnItems")] + [HarmonyPostfix] + static void Postfix(SingleItemSpawner __instance, List __result) { if (!Helper.IsMasterClient()) return; + if (__instance.transform.name != BackpackSpawnerName) return; - bool isBackpackSPawner = spawner.transform.name == "Backpack_Spawner"; - if (!isBackpackSPawner) return; - - bool isFirstSpawner = spawner.transform.IsChildOf(GameObject.Find("Biome_1").transform); - if (!isFirstSpawner) return; + GameObject firstBiome = GameObject.Find(FirstBiomeName); + if (firstBiome == null || !__instance.transform.IsChildOf(firstBiome.transform)) return; + if (__result != null && __result.Count > 1) return; - int playerCount = PhotonNetwork.PlayerList.Length - 1; // Already one bag - if (playerCount <= 0) return; - for (int i = 1; i <= playerCount; i++) + int extraBags = PhotonNetwork.PlayerList.Length - 1; + if (extraBags <= 0) return; + + try { - Vector3 spawnPosition = spawner.transform.position + Vector3.up * 0.1f + Vector3.right * i; - PhotonView component = PhotonNetwork.InstantiateItemRoom(spawner.prefab.name, spawnPosition, spawner.transform.rotation).GetComponent(); - if (spawner.isKinematic) - { - component.GetComponent().RPC("SetKinematicRPC", RpcTarget.AllBuffered, true, component.transform.position, component.transform.rotation); - } + SpawnExtraBags(__instance, extraBags); + } + catch (Exception e) + { + Plugin.Log.LogError($"Failed to spawn extra backpacks, continuing without them: {e}"); } } -} \ No newline at end of file + private static void SpawnExtraBags(SingleItemSpawner spawner, int count) + { + List spawned = new(count); + for (int i = 1; i <= count; i++) + { + Vector3 spawnPosition = spawner.transform.position + Vector3.up * 0.1f + Vector3.right * i; + PhotonView view = PhotonNetwork.InstantiateItemRoom(spawner.prefab.name, spawnPosition, spawner.transform.rotation).GetComponent(); + if (spawner.isKinematic) + { + view.RPC("SetKinematicRPC", RpcTarget.AllBuffered, true, view.transform.position, view.transform.rotation); + } + spawned.Add(view); + } + + if (spawner.HasSpawnTracking(out SpawnedItemTracker tracker)) tracker.TrackSpawnedItems(spawned); + } + +}