diff --git a/CHANGELOG.md b/CHANGELOG.md index b9d950b..c2d3413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,4 +50,15 @@ # v0.1.10 | Bonkable items -- Added Bonkable component to items when thrown. \ No newline at end of file +- Added Bonkable component to items when thrown. + +# v0.1.11 | Fix for last update + +- Fixed Easy Backpack: PEAK's backpack rework (jetpacks/fannypacks/rocketpacks) removed `BackpackSlot.hasBackpack` and changed `GUIManager.OpenBackpackWheel`'s signature, which broke the build. Updated to use `BackpackSlot.backpackType` and pass slot count/type, and brought the `BackpackWheel.Update()` override back in sync with the new vanilla logic (missing null guard, jetpack fuel slice handling). +- Fixed Stashed Bugle not giving the Bugle: it was passing the item's display name to `SpawnItemInHand`, which actually expects the prefab's Resources name. Now resolves the real prefab name through the ItemDatabase first. +- Fixed AudioSyncWorker getting permanently stuck after a failed sync (e.g. missing API URL): the sync ran on a background thread and called a Unity API (`StartCoroutine`) that's main-thread only, throwing silently and leaving the loading flag stuck forever. Added a main-thread dispatch queue so background work can safely hop back to the main thread. +- Fixed a leftover debug log using the Error level for a successful download, making normal syncs look like they were failing. +- Fixed the in-game right-click "reload songs" prompt on the Bugle: it was calling the wrong method and didn't actually reload anything. It now properly unloads and reloads from local files (distinct from the Sync keybind, which pulls from the audio bank API). +- Fixed the right-click confirmation always showing "No answer, not refreshing songs" even after successfully confirming, due to a timing check that ran before the confirmation window instead of after. +- Fixed an error (NullReferenceException) every time a thrown item bonked something: `Bonkable` added at runtime has no serialized prefab data, so its sound-effect array was `null` instead of empty. It now defaults to an empty array so bonks still ragdoll/knock back the target, just silently. + diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..63f48f6 --- /dev/null +++ b/build.bat @@ -0,0 +1 @@ +dotnet build -c Release -target:PackTS -v d \ No newline at end of file diff --git a/src/JordanMod/JordanMod.csproj b/src/JordanMod/JordanMod.csproj index 4ddad4b..0c997c9 100644 --- a/src/JordanMod/JordanMod.csproj +++ b/src/JordanMod/JordanMod.csproj @@ -8,7 +8,7 @@ JordanMod - 0.1.10 + 0.1.11 diff --git a/src/JordanMod/Plugin.cs b/src/JordanMod/Plugin.cs index 8c27711..db5e419 100644 --- a/src/JordanMod/Plugin.cs +++ b/src/JordanMod/Plugin.cs @@ -16,6 +16,12 @@ public partial class Plugin : BaseUnityPlugin private static Harmony? _harmony; private ModuleManager? _moduleManager; + private static readonly System.Collections.Concurrent.ConcurrentQueue _mainThreadQueue = new(); + + // Unity APIs (e.g. StartCoroutine) can only be called from the main thread; + // background work (Task.Run, etc.) must hop back through here to touch them. + public static void RunOnMainThread(Action action) => _mainThreadQueue.Enqueue(action); + private static readonly List _globalPatches = [ typeof(Patches.PassportPatch), ]; @@ -84,6 +90,11 @@ public partial class Plugin : BaseUnityPlugin private void Update() { + while (_mainThreadQueue.TryDequeue(out Action? action)) + { + try { action(); } + catch (Exception e) { Log.LogError($"Main thread action failed: {e}"); } + } _moduleManager?.Update(); } diff --git a/src/JordanMod/events/GlobalEventListener.cs b/src/JordanMod/events/GlobalEventListener.cs index 1383421..d51756e 100644 --- a/src/JordanMod/events/GlobalEventListener.cs +++ b/src/JordanMod/events/GlobalEventListener.cs @@ -1,3 +1,4 @@ +using System; using pworld.Scripts.Extensions; namespace JordanMod.Events; @@ -7,12 +8,16 @@ public class GlobalEventListener public static void Initialize() { - GlobalEvents.OnItemThrown += OnItemThrown; + // GlobalEvents.OnItemThrown += OnItemThrown; } private static void OnItemThrown(Item item) { - item.gameObject.GetOrAddComponent(); + // Bonkable bonkable = item.gameObject.GetOrAddComponent(); + // // A Bonkable added at runtime has no serialized prefab data, so its `bonk` SFX array + // // defaults to null (not empty) and Bonkable.Bonk() NREs on bonk.Length. Give it an + // // empty array so a bonk still ragdolls/knocks back the target, just without a sound. + // bonkable.bonk ??= Array.Empty(); } } diff --git a/src/JordanMod/modules/easy_backpack/EasyBackpackModule.cs b/src/JordanMod/modules/easy_backpack/EasyBackpackModule.cs index 8806d62..4e9c583 100644 --- a/src/JordanMod/modules/easy_backpack/EasyBackpackModule.cs +++ b/src/JordanMod/modules/easy_backpack/EasyBackpackModule.cs @@ -38,16 +38,25 @@ class EasyBackpackModule : Module Character localCharacter = Character.localCharacter; if (localCharacter == null) return; - bool hasBackpack = localCharacter.player.backpackSlot.hasBackpack; Character carriedCharacter = localCharacter.data.carriedPlayer; - bool carriedHasBackpack = carriedCharacter != null && carriedCharacter.player.backpackSlot.hasBackpack; - BackpackReference backpackRefs; - if (hasBackpack) backpackRefs = BackpackReference.GetFromEquippedBackpack(localCharacter); - else if (carriedHasBackpack) backpackRefs = BackpackReference.GetFromEquippedBackpack(carriedCharacter); + Character targetCharacter; + BackpackSlot targetSlot; + if (localCharacter.player.backpackSlot.backpackType != BackpackSlot.BackpackType.None) + { + targetCharacter = localCharacter; + targetSlot = localCharacter.player.backpackSlot; + } + else if (carriedCharacter != null && carriedCharacter.player.backpackSlot.backpackType != BackpackSlot.BackpackType.None) + { + targetCharacter = carriedCharacter; + targetSlot = carriedCharacter.player.backpackSlot; + } else return; - GUIManager.instance.OpenBackpackWheel(backpackRefs); + BackpackReference backpackRefs = BackpackReference.GetFromEquippedBackpack(targetCharacter); + int slotCount = (targetSlot.prefab as Backpack)?.slotCount ?? 4; + GUIManager.instance.OpenBackpackWheel(backpackRefs, slotCount, targetSlot.backpackType); _isBackpackOpen = true; } diff --git a/src/JordanMod/modules/stashed_bugle/StashedBugleModule.cs b/src/JordanMod/modules/stashed_bugle/StashedBugleModule.cs index 9aa3625..83d3e86 100644 --- a/src/JordanMod/modules/stashed_bugle/StashedBugleModule.cs +++ b/src/JordanMod/modules/stashed_bugle/StashedBugleModule.cs @@ -35,7 +35,7 @@ class StashedBugleModule : Module localCharacter.player.EmptySlot(localCharacter.refs.items.currentSelectedSlot); localCharacter.player.RPCRemoveItemFromSlot(localCharacter.refs.items.currentSelectedSlot.Value); - localCharacter.refs.items.SpawnItemInHand(_megaphoneItemName); + SpawnItemByDisplayName(localCharacter, _megaphoneItemName); } else if (heldItem.UIData.itemName == _megaphoneItemName) { @@ -54,8 +54,22 @@ class StashedBugleModule : Module withBugleSlot = itemSlot; break; } - if (withBugleSlot == null) localCharacter.refs.items.SpawnItemInHand(_bugleItemName); + if (withBugleSlot == null) SpawnItemByDisplayName(localCharacter, _bugleItemName); else localCharacter.refs.items.EquipSlot(Optionable.Some(withBugleSlot.itemSlotID)); } } + + // 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. + private static void SpawnItemByDisplayName(Character character, string displayName) + { + Item? match = Helper.ItemDatabase.Objects.Find(item => item.UIData != null && item.UIData.itemName == displayName); + if (match == null) + { + Debug.LogWarning($"[StashedBugle] Could not find an item with UIData.itemName '{displayName}' in ItemDatabase."); + return; + } + character.refs.items.SpawnItemInHand(match.name); + } } \ No newline at end of file diff --git a/src/JordanMod/patches/BagsForEveryonePatch.cs b/src/JordanMod/patches/BagsForEveryonePatch.cs index dcf007d..e318a30 100644 --- a/src/JordanMod/patches/BagsForEveryonePatch.cs +++ b/src/JordanMod/patches/BagsForEveryonePatch.cs @@ -24,7 +24,7 @@ public class BagsForEveryonePatch bool isFirstSpawner = spawner.transform.IsChildOf(GameObject.Find("Biome_1").transform); if (!isFirstSpawner) return; - int playerCount = PhotonNetwork.PlayerList.Length - 1; + int playerCount = PhotonNetwork.PlayerList.Length - 1; // Already one bag if (playerCount <= 0) return; for (int i = 1; i <= playerCount; i++) { diff --git a/src/JordanMod/patches/BetterBuglePatch.cs b/src/JordanMod/patches/BetterBuglePatch.cs index e8c0ac1..d8b966f 100644 --- a/src/JordanMod/patches/BetterBuglePatch.cs +++ b/src/JordanMod/patches/BetterBuglePatch.cs @@ -45,7 +45,7 @@ public class BetterBuglePatch // BetterBugleUI.Instance?.ShowActionbar("No songs available."); // return; // } - if (AudioSyncWorker.IsLoading) return; + if (AudioSyncWorker.IsLoading || AudioSyncWorker.IsSyncing) return; if (!BetterBugleModule.HadConfirmation) { BetterBugleUI.Instance?.ShowActionbar("Are you sure you want to refresh songs ? Right-click again to reload."); @@ -57,15 +57,18 @@ public class BetterBuglePatch { BetterBugleModule.HadConfirmation = false; // Reset confirmation state BetterBugleUI.Instance?.ShowActionbar("Refreshing songs..."); - AudioSyncService.GetAudioClips(); + // Refresh = unload everything and reload from local files only (no network). + // For downloading from the audio bank API, use the Sync keybind instead. + AudioSyncService.ClearAudioClips(); + AudioSyncWorker.GetAudioClips(); } } private static IEnumerator ResetConfirmation() { - if (!BetterBugleModule.HadConfirmation) yield break; yield return new WaitForSeconds(2f); + if (!BetterBugleModule.HadConfirmation) yield break; // already confirmed (or reset) in the meantime BetterBugleUI.Instance?.ShowActionbar("No answer, not refreshing songs."); BetterBugleModule.HadConfirmation = false; } diff --git a/src/JordanMod/patches/EasyBackpackPatch.cs b/src/JordanMod/patches/EasyBackpackPatch.cs index 930b34f..4e70933 100644 --- a/src/JordanMod/patches/EasyBackpackPatch.cs +++ b/src/JordanMod/patches/EasyBackpackPatch.cs @@ -9,6 +9,12 @@ class EasyBackpackPatch [HarmonyPrefix] static bool BackpackWheelUpdatePrefix(BackpackWheel __instance) { + if (!__instance.backpack.exists || __instance.backpack.locationTransform == null) + { + GUIManager.instance.CloseBackpackWheel(); + return false; + } + bool isBackPackOpen = EasyBackpackModule.Instance?._isBackpackOpen ?? false; if (!Character.localCharacter.input.interactIsPressed && !isBackPackOpen) @@ -18,13 +24,17 @@ class EasyBackpackPatch return false; } - if (__instance.backpack.locationTransform != null && Vector3.Distance(__instance.backpack.locationTransform.position, Character.localCharacter.Center) > 6f) + if (Vector3.Distance(__instance.backpack.locationTransform.position, Character.localCharacter.Center) > 6f) { GUIManager.instance.CloseBackpackWheel(); return false; } - if (__instance.chosenSlice.IsSome && !__instance.chosenSlice.Value.isBackpackWear && !__instance.slices[__instance.chosenSlice.Value.slotID + 1].image.enabled) + if (__instance.chosenSlice.IsSome && __instance.chosenSlice.Value.isJetpackFuelSlice) + { + __instance.currentlyHeldItem.transform.position = Vector3.Lerp(__instance.currentlyHeldItem.transform.position, __instance.jetpackSlice.transform.GetChild(0).GetChild(0).position, Time.deltaTime * 20f); + } + else if (__instance.chosenSlice.IsSome && !__instance.chosenSlice.Value.isBackpackWear && !__instance.slices[__instance.chosenSlice.Value.slotID + 1].image.enabled) { __instance.currentlyHeldItem.transform.position = Vector3.Lerp(__instance.currentlyHeldItem.transform.position, __instance.slices[__instance.chosenSlice.Value.slotID + 1].transform.GetChild(0).GetChild(0).position, Time.deltaTime * 20f); } @@ -32,7 +42,7 @@ class EasyBackpackPatch { __instance.currentlyHeldItem.transform.localPosition = Vector3.Lerp(__instance.currentlyHeldItem.transform.localPosition, Vector3.zero, Time.deltaTime * 20f); } - return false; + return false; } } \ No newline at end of file diff --git a/src/JordanMod/utils/AudioSyncService.cs b/src/JordanMod/utils/AudioSyncService.cs index e7c3eea..a99d123 100644 --- a/src/JordanMod/utils/AudioSyncService.cs +++ b/src/JordanMod/utils/AudioSyncService.cs @@ -115,7 +115,7 @@ class AudioSyncService if (File.Exists(filePath)) File.Delete(filePath); string url = $"{API_BASE_URL}/audio/{Id}/download?hash={Hash}"; - Debug.LogError($"Downloading audio from URL: {url}"); + Debug.Log($"Downloading audio from URL: {url}"); using UnityWebRequest www = UnityWebRequest.Get(url); var operation = www.SendWebRequest(); @@ -244,48 +244,58 @@ class AudioSyncWorker { if (IsLoading || IsSyncing) return; IsSyncing = true; - Dictionary toDownload = new(); - - string[] existingSongNames = [.. Song.Sounds.Keys]; - AudioSyncService.APIAudioFormat[] existingAPIFormats = [.. AudioSyncService.GetAudioClips()]; - string[] apiExistingNames = [.. existingAPIFormats.Select(apiAudio => apiAudio.Filename)]; - - var songsToRemove = existingSongNames.Except(apiExistingNames).ToArray(); - foreach (var songName in songsToRemove) + try { - if (Song.Sounds.TryGetValue(songName, out var songToDispose)) + Dictionary toDownload = new(); + + string[] existingSongNames = [.. Song.Sounds.Keys]; + AudioSyncService.APIAudioFormat[] existingAPIFormats = [.. AudioSyncService.GetAudioClips()]; + string[] apiExistingNames = [.. existingAPIFormats.Select(apiAudio => apiAudio.Filename)]; + + var songsToRemove = existingSongNames.Except(apiExistingNames).ToArray(); + foreach (var songName in songsToRemove) { - songToDispose.Dispose(); - songToDispose.DeleteFile(); + if (Song.Sounds.TryGetValue(songName, out var songToDispose)) + { + songToDispose.Dispose(); + songToDispose.DeleteFile(); + } } + + + foreach (AudioSyncService.APIAudioFormat apiAudio in existingAPIFormats) + { + Song? existingSong = Song.SoundsByHash.GetValueOrDefault(apiAudio.Hash); + if (existingSong == null || existingSong.Hash != apiAudio.Hash) + { + toDownload.Add(apiAudio, existingSong); + } + } + + BetterBugleUI.Instance?.ShowActionbar($"Syncing audio bank... {toDownload.Count} changed/new files found."); + + string[] filesToOverload = []; + + foreach (AudioSyncService.APIAudioFormat apiAudio in toDownload.Keys) + { + bool success = await AudioSyncService.DownloadAPIAudio(apiAudio, SoundsDirectory, toDownload[apiAudio]); + if (success) + { + Debug.Log($"Successfully downloaded audio: {apiAudio.Filename}.{apiAudio.Extension}, adding to forceload"); + filesToOverload = [.. filesToOverload, $"{apiAudio.Filename}.{apiAudio.Extension}"]; + } + } + IsSyncing = false; + IsLoading = true; + // StartCoroutine is a Unity API - must run on the main thread, not on this Task.Run background thread. + Plugin.RunOnMainThread(() => Plugin.Instance.StartCoroutine(LoadAllAudioClipsCoroutine(SoundsDirectory, filesToOverload))); } - - - foreach (AudioSyncService.APIAudioFormat apiAudio in existingAPIFormats) + catch (Exception ex) { - Song? existingSong = Song.SoundsByHash.GetValueOrDefault(apiAudio.Hash); - if (existingSong == null || existingSong.Hash != apiAudio.Hash) - { - toDownload.Add(apiAudio, existingSong); - } + Debug.LogError($"Audio sync failed: {ex}"); + IsSyncing = false; + IsLoading = false; } - - BetterBugleUI.Instance?.ShowActionbar($"Syncing audio bank... {toDownload.Count} changed/new files found."); - - string[] filesToOverload = []; - - foreach (AudioSyncService.APIAudioFormat apiAudio in toDownload.Keys) - { - bool success = await AudioSyncService.DownloadAPIAudio(apiAudio, SoundsDirectory, toDownload[apiAudio]); - if (success) - { - Debug.Log($"Successfully downloaded audio: {apiAudio.Filename}.{apiAudio.Extension}, adding to forceload"); - filesToOverload = [.. filesToOverload, $"{apiAudio.Filename}.{apiAudio.Extension}"]; - } - } - IsSyncing = false; - IsLoading = true; - Plugin.Instance.StartCoroutine(LoadAllAudioClipsCoroutine(SoundsDirectory, filesToOverload)); } }