diff --git a/NewHorizons/Handlers/HeldItemHandler.cs b/NewHorizons/Handlers/HeldItemHandler.cs new file mode 100644 index 00000000..d6e05c26 --- /dev/null +++ b/NewHorizons/Handlers/HeldItemHandler.cs @@ -0,0 +1,229 @@ +using HarmonyLib; +using NewHorizons.Builder.Props; +using NewHorizons.External.Modules.Props; +using NewHorizons.Utility; +using NewHorizons.Utility.OWML; +using System; +using System.Collections.Generic; +using System.IO; +using UnityEngine; + +namespace NewHorizons.Handlers; + +[HarmonyPatch] +public static class HeldItemHandler +{ + /// + /// Dictionary of system name to item path + /// If we travel to multiple systems within a single loop, this will hold the items we move between systems + /// + private static Dictionary> _pathOfItemTakenFromSystem = new(); + + public static bool WasAWCTakenFromATP => _pathOfItemTakenFromSystem.TryGetValue("SolarSystem", out var list) && list.Contains(ADVANCED_WARP_CORE); + + private static GameObject _currentlyHeldItem; + + /// + /// Track the path of any item we ever pick up, in case we take it out of the system + /// + private static Dictionary _trackedPaths = new(); + + private static bool _isInitialized = false; + private static bool _isSystemReady = false; + + /// + /// We keep our own reference to this because when Unload gets called it might have already been updated + /// + private static string _currentStarSystem; + + private const string ADVANCED_WARP_CORE = "TowerTwin_Body/Sector_TowerTwin/Sector_TimeLoopInterior/Interactables_TimeLoopInterior/WarpCoreSocket/Prefab_NOM_WarpCoreVessel"; + + [HarmonyPrefix, HarmonyPatch(typeof(ItemTool), nameof(ItemTool.Awake))] + private static void Init() + { + _trackedPaths.Clear(); + + _currentStarSystem = Main.Instance.CurrentStarSystem; + + if (!_isInitialized) + { + _isInitialized = true; + Main.Instance.OnChangeStarSystem.AddListener(OnStarSystemChanging); + Main.Instance.OnStarSystemLoaded.AddListener(OnSystemReady); + GlobalMessenger.AddListener("PlayerDeath", OnPlayerDeath); + } + } + + private static void OnPlayerDeath(DeathType _) + { + NHLogger.Log("Player died, resetting held items"); + + // Destroy everything + _pathOfItemTakenFromSystem.Clear(); + GameObject.Destroy(_currentlyHeldItem); + } + + private static GameObject MakePerfectCopy(GameObject go) + { + var owItem = go.GetComponent(); + + var tempParent = new GameObject(); + tempParent.transform.position = new Vector3(100000, 0, 0); + var newObject = DetailBuilder.Make(tempParent, tempParent.AddComponent(), null, go, new DetailInfo() { keepLoaded = true }); + newObject.SetActive(false); + newObject.transform.parent = null; + newObject.name = go.name; + + var newOWItem = newObject.GetComponent(); + + NHLogger.Log($"Cloned {go.name}, original item has component: [{owItem != null}] new item has component: [{newOWItem != null}]"); + + return newObject; + } + + private static void TrackPath(string path) + { + if (!_pathOfItemTakenFromSystem.ContainsKey(Main.Instance.CurrentStarSystem)) + { + _pathOfItemTakenFromSystem[Main.Instance.CurrentStarSystem] = new(); + } + _pathOfItemTakenFromSystem[Main.Instance.CurrentStarSystem].Add(path); + } + + private static void OnStarSystemChanging(string _) + { + if (_currentlyHeldItem != null) + { + // Track it so that when we return to this system we can delete the original + if (_trackedPaths.TryGetValue(_currentlyHeldItem, out var path)) + { + TrackPath(path); + } + + NHLogger.Log($"Scene unloaded, preserved inactive held item {_currentlyHeldItem.name}"); + // For some reason, the original will get destroyed no matter what we do. To avoid, we make a copy + _currentlyHeldItem = MakePerfectCopy(_currentlyHeldItem).DontDestroyOnLoad(); + } + + // If warping with a vessel, make sure to also track the path to the advanced warp core (assuming the player actually removed it) + if (Main.Instance.CurrentStarSystem == "SolarSystem" && Main.Instance.IsWarpingFromVessel) + { + // Making sure its actually gone + var warpCoreSocket = GameObject.FindObjectOfType()._warpCoreSocket; + if (!warpCoreSocket.IsSocketOccupied() || warpCoreSocket.GetWarpCoreType() != WarpCoreType.Vessel) + { + TrackPath(ADVANCED_WARP_CORE); + } + } + + _trackedPaths.Clear(); + _isSystemReady = false; + } + + private static void OnSystemReady(string _) + { + // If something was taken from this system during this life, remove it + if (_pathOfItemTakenFromSystem.TryGetValue(Main.Instance.CurrentStarSystem, out var paths)) + { + foreach (var path in paths) + { + // Have to wait two frames for the sockets to Awake and Start + Delay.FireInNUpdates(() => + { + try + { + NHLogger.Log($"Removing item that was taken from this system at {path}"); + var item = SearchUtilities.Find(path)?.GetComponent(); + // Make sure to update the socket it might be in so that it works + if (item.GetComponentInParent() is OWItemSocket socket) + { + socket.RemoveFromSocket(); + // Time loop core controller doesn't have time to hook up its events yet so we call this manually + if (path == ADVANCED_WARP_CORE) + { + var controller = GameObject.FindObjectOfType(); + controller.OpenCore(); + controller.OnSocketableRemoved(item); + } + NHLogger.Log($"Unsocketed {item.name}"); + } + item.gameObject.SetActive(false); + } + catch (Exception e) + { + NHLogger.LogError($"Failed to remove item at {path}: {e}"); + } + }, 2); + } + } + + // Give whatever item we were previously holding + if (_currentlyHeldItem != null) + { + NHLogger.Log($"Giving player held item {_currentlyHeldItem.name}"); + // Else its spawning the item inside the player and for that one frame it kills you + var newObject = MakePerfectCopy(_currentlyHeldItem); + + // We wait a bit because at some point after not something resets your held item to nothing + Delay.FireInNUpdates(() => + { + try + { + Locator.GetToolModeSwapper().GetItemCarryTool().PickUpItemInstantly(newObject.GetComponent()); + // For some reason picking something up messes up the input mode + if (PlayerState.AtFlightConsole()) + { + Locator.GetToolModeSwapper().UnequipTool(); + Locator.GetToolModeSwapper().OnEnterFlightConsole(Locator.GetShipBody()); + } + newObject.SetActive(true); + } + catch(Exception e) + { + NHLogger.LogError($"Failed to take item {newObject.name} to a new system: {e}"); + GameObject.Destroy(_currentlyHeldItem); + _currentlyHeldItem = null; + } + + }, 5); + } + + _isSystemReady = true; + } + + [HarmonyPrefix, HarmonyPatch(typeof(ItemTool), nameof(ItemTool.MoveItemToCarrySocket))] + private static void HeldItemChanged(ItemTool __instance, OWItem item) + { + if (!_isSystemReady) + { + return; + } + + if (item != null) + { + var path = item.transform.GetPath(); + if (!_trackedPaths.ContainsKey(item.gameObject)) + { + _trackedPaths[item.gameObject] = path; + } + } + + NHLogger.Log($"Player is now holding {item?.name ?? "nothing"}"); + _currentlyHeldItem = item?.gameObject; + } + + [HarmonyPostfix, HarmonyPatch(typeof(ItemTool))] + [HarmonyPatch(nameof(ItemTool.SocketItem))] + [HarmonyPatch(nameof(ItemTool.DropItem))] + [HarmonyPatch(nameof(ItemTool.StartUnsocketItem))] + private static void HeldItemChanged2(ItemTool __instance) + { + if (!_isSystemReady) + { + return; + } + + NHLogger.Log($"Player is now holding nothing"); + _currentlyHeldItem = null; + } +} diff --git a/NewHorizons/Handlers/PlayerSpawnHandler.cs b/NewHorizons/Handlers/PlayerSpawnHandler.cs index 8ee1a007..352b4eff 100644 --- a/NewHorizons/Handlers/PlayerSpawnHandler.cs +++ b/NewHorizons/Handlers/PlayerSpawnHandler.cs @@ -51,11 +51,21 @@ namespace NewHorizons.Handlers Delay.StartCoroutine(SpawnCoroutine(30)); } - var cloak = GetDefaultSpawn()?.GetAttachedOWRigidbody()?.GetComponentInChildren(); - if (cloak != null) + + // It was NREing in here when it was all ?. so explicit null checks + var spawn = GetDefaultSpawn(); + if (spawn != null) { - // Ensures it has invoked everything and actually placed the player in the cloaking field #671 - cloak._firstUpdate = true; + var attachedOWRigidBody = spawn.GetAttachedOWRigidbody(); + if (attachedOWRigidBody != null) + { + var cloak = attachedOWRigidBody.GetComponentInChildren(); + if (cloak != null) + { + // Ensures it has invoked everything and actually placed the player in the cloaking field #671 + cloak._firstUpdate = true; + } + } } // Spawn ship diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index 7b540e69..29d4c0a0 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -58,7 +58,7 @@ namespace NewHorizons public static float SecondsElapsedInLoop = -1; - public static bool IsSystemReady { get; private set; } + public static bool IsSystemReady { get; private set; } public string DefaultStarSystem => SystemDict.ContainsKey(DefaultSystemOverride) ? DefaultSystemOverride : _defaultStarSystem; public string CurrentStarSystem diff --git a/NewHorizons/Patches/TimeLoopPatches.cs b/NewHorizons/Patches/TimeLoopPatches.cs index 5628fa15..d2b266fe 100644 --- a/NewHorizons/Patches/TimeLoopPatches.cs +++ b/NewHorizons/Patches/TimeLoopPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using NewHorizons.Handlers; namespace NewHorizons.Patches { @@ -14,5 +15,16 @@ namespace NewHorizons.Patches [HarmonyPatch(typeof(GlobalMusicController), nameof(GlobalMusicController.UpdateEndTimesMusic))] [HarmonyPatch(typeof(TimeLoop), nameof(TimeLoop.Update))] public static bool DisableWithoutTimeLoop() => Main.Instance.TimeLoopEnabled; + + [HarmonyPostfix] + [HarmonyPatch(typeof(TimeLoop), nameof(TimeLoop.Start))] + public static void TimeLoop_Start(TimeLoop __instance) + { + // If we took the AWC out of the main system make sure to disable time loop + if (Main.Instance.CurrentStarSystem != "SolarSystem" && HeldItemHandler.WasAWCTakenFromATP) + { + TimeLoop.SetTimeLoopEnabled(false); + } + } } }