From 60cff24a5cc0110e1dad9de4b9fedb180d978d42 Mon Sep 17 00:00:00 2001 From: Joshua Gibbs Date: Mon, 22 Jan 2024 12:14:48 -0700 Subject: [PATCH 1/2] fix: Coordinated Spawns duplication fixed!!! (Purple Edition) (#531) * Coordinated Spawns (Purple Edition) * Don't force LWE requirement. * Revert "Don't force LWE requirement." This reverts commit 7cd473a44fd2812f45aaa34f0ef1cb1b960d20eb. * Combined old with new to correctly allow global and batch spawns. and hopefully fix the NRE that i still cannot replicate. * Prefab cache no longer adds objects with IDs set * Fixed infinite spawning for EntitySpawner The issue was because the spawned objects was registered while being inactive in the scene. * Coordinated spawns per-batch instead of per-entity * Make sure it also works when a spawn is registered in-game * Fix 1 fail = all following stopped. * Delayed Spawns don't stop all progress. * Update EntitySpawner.cs * Corrected positioning mistake. * Spawn dammit. * remove _delayedSpawns * Should Not exist in main branch! --------- Co-authored-by: Metious <71298690+Metious@users.noreply.github.com> --- Nautilus/Assets/ModPrefabCache.cs | 19 +- Nautilus/Handlers/CoordinatedSpawnsHandler.cs | 15 +- Nautilus/MonoBehaviours/EntitySpawner.cs | 93 +++++---- .../Patchers/LargeWorldStreamerPatcher.cs | 196 ++++++++++++++---- Nautilus/Patchers/VehicleUpgradesPatcher.cs | 1 - 5 files changed, 235 insertions(+), 89 deletions(-) diff --git a/Nautilus/Assets/ModPrefabCache.cs b/Nautilus/Assets/ModPrefabCache.cs index 73b52670..e3cecb08 100644 --- a/Nautilus/Assets/ModPrefabCache.cs +++ b/Nautilus/Assets/ModPrefabCache.cs @@ -110,6 +110,7 @@ public void EnterPrefabIntoCache(GameObject prefab) else { prefab.transform.parent = _prefabRoot; + ResetIds(prefab); prefab.SetActive(true); } } @@ -121,7 +122,7 @@ public void EnterPrefabIntoCache(GameObject prefab) public void RemoveCachedPrefab(string classId) { - if(Entries.TryGetValue(classId, out var prefab)) + if (Entries.TryGetValue(classId, out var prefab)) { if(!prefab.IsPrefab()) Destroy(prefab); @@ -129,4 +130,20 @@ public void RemoveCachedPrefab(string classId) Entries.Remove(classId); } } + + private void ResetIds(GameObject prefab) + { + var uniqueIds = prefab.GetAllComponentsInChildren(); + + foreach (var uniqueId in uniqueIds) + { + if (string.IsNullOrEmpty(uniqueId.id)) + { + continue; + } + + UniqueIdentifier.identifiers.Remove(uniqueId.id); + uniqueId.id = null; + } + } } \ No newline at end of file diff --git a/Nautilus/Handlers/CoordinatedSpawnsHandler.cs b/Nautilus/Handlers/CoordinatedSpawnsHandler.cs index 1efc1fe2..44141bdd 100644 --- a/Nautilus/Handlers/CoordinatedSpawnsHandler.cs +++ b/Nautilus/Handlers/CoordinatedSpawnsHandler.cs @@ -3,6 +3,7 @@ using System.Linq; using Nautilus.Assets; using Nautilus.Patchers; +using Nautilus.Utility; using Newtonsoft.Json; using UnityEngine; @@ -19,11 +20,17 @@ public static class CoordinatedSpawnsHandler /// the SpawnInfo to spawn. public static void RegisterCoordinatedSpawn(SpawnInfo spawnInfo) { - if (!LargeWorldStreamerPatcher.spawnInfos.Add(spawnInfo)) + if (!LargeWorldStreamerPatcher.SpawnInfos.Add(spawnInfo)) + { + InternalLogger.Error($"SpawnInfo {spawnInfo} already registered."); return; - - if (uGUI.isMainLevel) - LargeWorldStreamerPatcher.CreateSpawner(spawnInfo); + } + + if (LargeWorldStreamer.main) + { + var batch = LargeWorldStreamer.main.GetContainingBatch(spawnInfo.SpawnPosition); + LargeWorldStreamerPatcher.BatchToSpawnInfos.GetOrAddNew(batch).Add(spawnInfo); + } } /// diff --git a/Nautilus/MonoBehaviours/EntitySpawner.cs b/Nautilus/MonoBehaviours/EntitySpawner.cs index 67040419..f4826d74 100644 --- a/Nautilus/MonoBehaviours/EntitySpawner.cs +++ b/Nautilus/MonoBehaviours/EntitySpawner.cs @@ -1,74 +1,81 @@ +namespace Nautilus.MonoBehaviours; + using System.Collections; -using Nautilus.Extensions; +using System.Collections.Generic; using Nautilus.Handlers; using Nautilus.Patchers; using Nautilus.Utility; using UnityEngine; using UWE; -namespace Nautilus.MonoBehaviours; - internal class EntitySpawner : MonoBehaviour { - internal SpawnInfo spawnInfo; + internal Int3 batchId; + internal IReadOnlyCollection spawnInfos; + internal bool global; - void Start() + private IEnumerator Start() { - StartCoroutine(SpawnAsync()); + yield return SpawnAsync(); + Destroy(gameObject); } - IEnumerator SpawnAsync() + private IEnumerator SpawnAsync() { - string stringToLog = spawnInfo.Type switch - { - SpawnInfo.SpawnType.ClassId => spawnInfo.ClassId, - _ => spawnInfo.TechType.AsString() - }; - - TaskResult task = new(); - yield return GetPrefabAsync(task); - - GameObject prefab = task.Get(); - if (prefab == null) - { - InternalLogger.Error($"no prefab found for {stringToLog}; process for Coordinated Spawn canceled."); - Destroy(gameObject); - yield break; - } - - if (!prefab.IsPrefab()) - { - prefab.SetActive(false); - } - - GameObject obj = UWE.Utils.InstantiateDeactivated(prefab, spawnInfo.SpawnPosition, spawnInfo.Rotation, spawnInfo.ActualScale); - - LargeWorldEntity lwe = obj.GetComponent(); - LargeWorldStreamer lws = LargeWorldStreamer.main; yield return new WaitUntil(() => lws != null && lws.IsReady()); // first we make sure the world streamer is initialized - // non-global objects cannot be spawned in unloaded terrain so we need to wait - if (lwe is {cellLevel: not (LargeWorldEntity.CellLevel.Batch or LargeWorldEntity.CellLevel.Global)}) + if (!global) { - Int3 batch = lws.GetContainingBatch(spawnInfo.SpawnPosition); - yield return new WaitUntil(() => lws.IsBatchReadyToCompile(batch)); // then we wait until the terrain is fully loaded (must be checked on each frame for faster spawns) + // then we wait until the terrain is fully loaded (must be checked on each frame for faster spawns) + yield return new WaitUntil(() => lws.IsBatchReadyToCompile(batchId)); } LargeWorld lw = LargeWorld.main; - + yield return new WaitUntil(() => lw != null && lw.streamer.globalRoot != null); // need to make sure global root is ready too for global spawns. + + foreach (var spawnInfo in spawnInfos) + { + string stringToLog = spawnInfo.Type switch + { + SpawnInfo.SpawnType.ClassId => spawnInfo.ClassId, + _ => spawnInfo.TechType.AsString() + }; + + InternalLogger.Debug($"Spawning {stringToLog}"); - lw.streamer.cellManager.RegisterEntity(obj); + TaskResult task = new(); + yield return GetPrefabAsync(spawnInfo, task); - obj.SetActive(true); + GameObject prefab = task.Get(); + if (prefab == null) + { + InternalLogger.Error($"no prefab found for {stringToLog}; process for Coordinated Spawn canceled."); + continue; + } - LargeWorldStreamerPatcher.savedSpawnInfos.Add(spawnInfo); + LargeWorldEntity lwe = prefab.GetComponent(); - Destroy(gameObject); + if (!lwe) + { + InternalLogger.Error($"No LargeWorldEntity component found for prefab '{stringToLog}'; process for Coordinated Spawn canceled."); + continue; + } + + GameObject obj = Instantiate(prefab, spawnInfo.SpawnPosition, spawnInfo.Rotation); + obj.transform.localScale = spawnInfo.ActualScale; + + obj.SetActive(true); + + LargeWorldEntity.Register(obj); + + LargeWorldStreamerPatcher.SavedSpawnInfos.Add(spawnInfo); + InternalLogger.Debug($"spawned {stringToLog}."); + } } - IEnumerator GetPrefabAsync(IOut gameObject) + private IEnumerator GetPrefabAsync(SpawnInfo spawnInfo, IOut gameObject) { GameObject obj; diff --git a/Nautilus/Patchers/LargeWorldStreamerPatcher.cs b/Nautilus/Patchers/LargeWorldStreamerPatcher.cs index 6d333c7c..ed6024d5 100644 --- a/Nautilus/Patchers/LargeWorldStreamerPatcher.cs +++ b/Nautilus/Patchers/LargeWorldStreamerPatcher.cs @@ -1,32 +1,44 @@ +using System.Collections; + +namespace Nautilus.Patchers; + using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using HarmonyLib; +using Nautilus.Assets; using Nautilus.Handlers; using Nautilus.Json.Converters; using Nautilus.MonoBehaviours; using Nautilus.Utility; using Newtonsoft.Json; using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.ResourceManagement.AsyncOperations; +using UWE; -namespace Nautilus.Patchers; - -internal class LargeWorldStreamerPatcher +internal static class LargeWorldStreamerPatcher { internal static void Patch(Harmony harmony) { - System.Reflection.MethodInfo initializeOrig = AccessTools.Method(typeof(LargeWorldStreamer), nameof(LargeWorldStreamer.Initialize)); + MethodInfo initializeOrig = AccessTools.Method(typeof(LargeWorldStreamer), nameof(LargeWorldStreamer.Initialize)); HarmonyMethod initPostfix = new(AccessTools.Method(typeof(LargeWorldStreamerPatcher), nameof(InitializePostfix))); harmony.Patch(initializeOrig, postfix: initPostfix); + + MethodInfo onBatchFullyLoadedOrig = AccessTools.Method(typeof(LargeWorldStreamer), nameof(LargeWorldStreamer.OnBatchFullyLoaded)); + HarmonyMethod onBatchFullyLoadedPostfix = new(AccessTools.Method(typeof(LargeWorldStreamerPatcher), nameof(OnBatchFullyLoadedPostfix))); + harmony.Patch(onBatchFullyLoadedOrig, postfix: onBatchFullyLoadedPostfix); } + + internal static readonly HashSet SpawnInfos = new(); + internal static readonly HashSet SavedSpawnInfos = new(); + internal static readonly Dictionary> BatchToSpawnInfos = new(); - internal static readonly HashSet spawnInfos = new(); - internal static readonly HashSet savedSpawnInfos = new(); - - private static readonly HashSet initialSpawnInfos = new(); + private static readonly HashSet _initialSpawnInfos = new(); - private static bool initialized; + private static bool _initialized; private static void InitializePostfix() { @@ -43,23 +55,135 @@ private static void InitializePostfix() List deserializedList = JsonConvert.DeserializeObject>(reader.ReadToEnd(), new Vector3Converter(), new QuaternionConverter()); if (deserializedList is not null) { - savedSpawnInfos.AddRange(deserializedList); + SavedSpawnInfos.AddRange(deserializedList); } reader.Close(); } catch (Exception ex) { - InternalLogger.Error($"Failed to load Saved spawn data from {file}\nSkipping static spawning until fixed!\n{ex}"); + InternalLogger.Error($"Failed to load Saved spawn data from {file}\nSkipping Coordinated spawning until fixed!\n{ex}"); reader.Close(); return; } } - spawnInfos.RemoveWhere(s => savedSpawnInfos.Contains(s)); - - InitializeSpawners(); + SpawnInfos.RemoveWhere(s => SavedSpawnInfos.Contains(s)); InternalLogger.Debug("Coordinated Spawns have been initialized in the current save."); + + var globalSpawns = new HashSet(); + + // Preload all the prefabs for faster spawning. + new List(SpawnInfos).Do((info) => + { + string keyToCheck = info.Type switch + { + SpawnInfo.SpawnType.TechType => CraftData.GetClassIdForTechType(info.TechType), + _ => info.ClassId + }; + if (!PrefabDatabase.TryGetPrefabFilename(keyToCheck, out string prefabName)) + { + InternalLogger.Error($"Failed to get prefab name for {keyToCheck}; process for Coordinated Spawn canceled."); + SpawnInfos.Remove(info); + return; + } + + if (PrefabHandler.Prefabs.TryGetInfoForFileName(prefabName, out var prefabInfo)) + { + InternalLogger.Debug($"Preloading {keyToCheck}"); + CoroutineHost.StartCoroutine(PreloadModdedPrefab(info, prefabInfo)); + } + else + { + var task = new AssetReferenceGameObject(prefabName).LoadAssetAsync(); + task.Completed += (t) => + { + if (t.Status != AsyncOperationStatus.Succeeded) + { + InternalLogger.Error($"Failed to preload {keyToCheck} with error: {t.OperationException}"); + return; + } + + var prefab = t.Result; + if (prefab == null) + { + InternalLogger.Error($"no prefab found for {keyToCheck}; process for Coordinated Spawn canceled."); + SpawnInfos.Remove(info); + return; + } + + LargeWorldEntity lwe = prefab.GetComponent(); + if (lwe is null) + { + InternalLogger.Error($"No LargeWorldEntity found on {keyToCheck}; Please ensure the prefab has a LargeWorldEntity component when using Coordinated Spawns."); + lwe = prefab.AddComponent(); + } + + if (lwe is { cellLevel: LargeWorldEntity.CellLevel.Global }) + { + globalSpawns.Add(info); + InternalLogger.Debug($"Init: Created spawner for {keyToCheck} at the Global level."); + SpawnInfos.Remove(info); + } + + InternalLogger.Debug($"Preloaded {keyToCheck}"); + }; + } + }); + + CreateSpawner(LargeWorldStreamer.main.GetContainingBatch(Vector3.zero), globalSpawns, true); + + foreach (var spawnInfo in SpawnInfos) + { + var batch = LargeWorldStreamer.main.GetContainingBatch(spawnInfo.SpawnPosition); + + if (globalSpawns.Contains(spawnInfo)) + { + continue; + } + + BatchToSpawnInfos.GetOrAddNew(batch).Add(spawnInfo); + } + } + + private static void OnBatchFullyLoadedPostfix(Int3 batchId) + { + if (BatchToSpawnInfos.TryGetValue(batchId, out var spawnInfos)) + { + CreateSpawner(batchId, spawnInfos, false); + SpawnInfos.RemoveRange(spawnInfos); + BatchToSpawnInfos.Remove(batchId); + } + } + + private static IEnumerator PreloadModdedPrefab(SpawnInfo info, PrefabInfo prefabInfo) + { + var request = new ModPrefabRequest(prefabInfo); + yield return request; + + if (!request.TryGetPrefab(out var prefab)) + { + InternalLogger.Error($"no prefab found for {prefabInfo.ClassID}; process for Coordinated Spawn canceled."); + SpawnInfos.Remove(info); + yield break; + } + LargeWorldEntity lwe = prefab.GetComponent(); + if (lwe is null) + { + InternalLogger.Error($"No LargeWorldEntity found on {prefabInfo.ClassID}; Please ensure the prefab has a LargeWorldEntity component when using Coordinated Spawns."); + lwe = prefab.AddComponent(); + } + if (lwe is { cellLevel: LargeWorldEntity.CellLevel.Global }) + { + var batch = LargeWorldStreamer.main.GetContainingBatch(info.SpawnPosition); + CreateSpawner(batch, new []{info}, true); + InternalLogger.Debug($"Preload: Created spawner for {info.ClassId} at the Global level."); + SpawnInfos.Remove(info); + if (BatchToSpawnInfos.TryGetValue(batch, out var spawnInfos)) + { + spawnInfos.Remove(info); + } + } } private static void SaveData() @@ -68,7 +192,7 @@ private static void SaveData() using StreamWriter writer = new(file); try { - string data = JsonConvert.SerializeObject(savedSpawnInfos, Formatting.Indented, new Vector3Converter(), new QuaternionConverter()); + string data = JsonConvert.SerializeObject(SavedSpawnInfos, Formatting.Indented, new Vector3Converter(), new QuaternionConverter()); writer.Write(data); writer.Flush(); writer.Close(); @@ -79,43 +203,35 @@ private static void SaveData() writer.Close(); } } - + // We keep an initial copy of the spawn infos so Coordinated Spawns also works if you quit to main menu. private static void InitializeSpawnInfos() { - if (initialized) + if (_initialized) { // we already have an initialSpawnInfos initialized, refresh our spawnInfos List. - savedSpawnInfos.Clear(); - foreach (SpawnInfo spawnInfo in initialSpawnInfos.Where(spawnInfo => !spawnInfos.Contains(spawnInfo))) + SavedSpawnInfos.Clear(); + foreach (SpawnInfo spawnInfo in _initialSpawnInfos.Where(spawnInfo => !SpawnInfos.Contains(spawnInfo))) { - spawnInfos.Add(spawnInfo); + SpawnInfos.Add(spawnInfo); } return; } - - initialSpawnInfos.AddRange(spawnInfos); - SaveUtils.RegisterOnSaveEvent(SaveData); - initialized = true; - } - private static void InitializeSpawners() - { - foreach (SpawnInfo spawnInfo in spawnInfos) - { - CreateSpawner(spawnInfo); - } + _initialSpawnInfos.AddRange(SpawnInfos); + SaveUtils.RegisterOnSaveEvent(SaveData); + _initialized = true; } - internal static void CreateSpawner(SpawnInfo spawnInfo) + private static void CreateSpawner(Int3 batch, IReadOnlyCollection spawnInfos, bool global) { - string keyToCheck = spawnInfo.Type switch - { - SpawnInfo.SpawnType.TechType => spawnInfo.TechType.AsString(), - _ => spawnInfo.ClassId - }; - - GameObject obj = new($"{keyToCheck}Spawner"); - obj.EnsureComponent().spawnInfo = spawnInfo; + var centerPosition = LargeWorldStreamer.main.GetBatchCenter(batch); + InternalLogger.Debug($"Creating Spawner for batch: {batch} at {centerPosition}"); + GameObject obj = new($"{batch} Spawner"); + obj.transform.position = centerPosition; + var spawner = obj.EnsureComponent(); + spawner.batchId = batch; + spawner.spawnInfos = spawnInfos; + spawner.global = global; } } \ No newline at end of file diff --git a/Nautilus/Patchers/VehicleUpgradesPatcher.cs b/Nautilus/Patchers/VehicleUpgradesPatcher.cs index 27ff03af..4bdb577b 100644 --- a/Nautilus/Patchers/VehicleUpgradesPatcher.cs +++ b/Nautilus/Patchers/VehicleUpgradesPatcher.cs @@ -24,7 +24,6 @@ internal class VehicleUpgradesPatcher internal static void Patch(Harmony harmony) { InternalLogger.Debug("VehicleUpgradePatcher: attempting patch..."); - HarmonyFileLog.Enabled = true; try { harmony.PatchAll(typeof(VehicleUpgradesPatcher)); From fb4b64a6a7c8f5561a3c17aff120e45f9adbea0b Mon Sep 17 00:00:00 2001 From: Joshua Gibbs Date: Mon, 22 Jan 2024 12:29:42 -0700 Subject: [PATCH 2/2] fix: Existing Node checks. (#532) --- Nautilus/Patchers/CraftTreePatcher.cs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Nautilus/Patchers/CraftTreePatcher.cs b/Nautilus/Patchers/CraftTreePatcher.cs index 41b7761b..aadf9144 100644 --- a/Nautilus/Patchers/CraftTreePatcher.cs +++ b/Nautilus/Patchers/CraftTreePatcher.cs @@ -120,7 +120,7 @@ private static void GetTreePreFix(CraftTree.Type treeType, ref CraftTree __resul private static void PatchCraftTree(ref CraftTree __result, CraftTree.Type type) { - List removals = NodesToRemove.TryGetValue(type, out removals)? removals: new List(); + List removals = NodesToRemove.TryGetValue(type, out removals) ? removals : new List(); RemoveNodes(ref __result, ref removals); AddCustomTabs(ref __result, type); @@ -135,7 +135,7 @@ private static void AddCustomTabs(ref CraftTree tree, CraftTree.Type type) List customTabs = TabNodes.TryGetValue(type, out customTabs) ? customTabs : new List(); foreach (TabNode customNode in customTabs) { - if(!TraverseTree(tree.nodes, customNode.Path, out var currentNode)) + if (!TraverseTree(tree.nodes, customNode.Path, out var currentNode)) { InternalLogger.Error($"Cannot add tab: {customNode.Name} to {customNode.Scheme} at {string.Join("/", customNode.Path)} as the parent node could not be found."); continue; @@ -147,11 +147,14 @@ private static void AddCustomTabs(ref CraftTree tree, CraftTree.Type type) continue; } - // Add the new tab node. - currentNode.AddNode(new TreeNode[] + if (TraverseTree(currentNode, new[] { customNode.Name }, out _)) { - new CraftNode(customNode.Name, TreeAction.Expand, TechType.None) - }); + // This node already exists, skip it. + continue; + } + + // Add the new tab node. + currentNode.AddNode(new CraftNode(customNode.Name, TreeAction.Expand, TechType.None)); InternalLogger.Debug($"Added tab: {customNode.Name} to {customNode.Scheme} at {string.Join("/", customNode.Path)}"); } } @@ -184,11 +187,14 @@ private static void PatchNodes(ref CraftTree tree, CraftTree.Type type) } } - // Add the node. - currentNode.AddNode(new TreeNode[] + if (TraverseTree(currentNode, new[] { customNode.TechType.AsString(false) }, out _)) { - new CraftNode(customNode.TechType.AsString(false), TreeAction.Craft, customNode.TechType) - }); + // This node already exists, skip it. + continue; + } + + // Add the node. + currentNode.AddNode(new CraftNode(customNode.TechType.AsString(false), TreeAction.Craft, customNode.TechType)); InternalLogger.Debug($"Added Crafting node: {customNode.TechType.AsString()} to {customNode.Scheme} at {string.Join("/", customNode.Path)}"); } }