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);
+ }
+
+}