Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coordinated Spawns (Purple Edition) #531

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Coordinated spawns per-batch instead of per-entity
  • Loading branch information
Metious committed Jan 19, 2024
commit c372ba3cf17cb77a7b0ff7e17379c4c1029098b2
103 changes: 56 additions & 47 deletions Nautilus/MonoBehaviours/EntitySpawner.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Nautilus.Extensions;
using Nautilus.Handlers;
using Nautilus.Patchers;
Expand All @@ -10,76 +12,83 @@ namespace Nautilus.MonoBehaviours;

internal class EntitySpawner : MonoBehaviour
{
internal SpawnInfo spawnInfo;
internal Int3 batchId;
internal IReadOnlyCollection<SpawnInfo> spawnInfos;
internal bool global;

private void Start()
private IEnumerator Start()
{
StartCoroutine(SpawnAsync());
yield return SpawnAsync();
Destroy(gameObject);
}

private IEnumerator SpawnAsync()
{
string stringToLog = spawnInfo.Type switch
{
SpawnInfo.SpawnType.ClassId => spawnInfo.ClassId,
_ => spawnInfo.TechType.AsString()
};

TaskResult<GameObject> task = new();
yield return GetPrefabAsync(task);
LargeWorldStreamer lws = LargeWorldStreamer.main;
yield return new WaitUntil(() => lws != null && lws.IsReady()); // first we make sure the world streamer is initialized

GameObject prefab = task.Get();
if (prefab == null)
if (!global)
{
InternalLogger.Error($"no prefab found for {stringToLog}; process for Coordinated Spawn canceled.");
Destroy(gameObject);
yield break;
// 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));
}

var batchCenter = lws.GetBatchCenter(batchId);
var bounds = new Bounds(batchCenter, Vector3.zero);

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.

LargeWorldEntity lwe = prefab.GetComponent<LargeWorldEntity>();

if (!lwe)
foreach (var spawnInfo in spawnInfos)
{
InternalLogger.Error($"No LargeWorldEntity component found for prefab '{stringToLog}'; process for Coordinated Spawn canceled.");
Destroy(gameObject);
yield break;
}
string stringToLog = spawnInfo.Type switch
{
SpawnInfo.SpawnType.ClassId => spawnInfo.ClassId,
_ => spawnInfo.TechType.AsString()
};

InternalLogger.Debug($"Spawning {stringToLog}");

TaskResult<GameObject> task = new();
yield return GetPrefabAsync(spawnInfo, task);

LargeWorldStreamer lws = LargeWorldStreamer.main;
yield return new WaitUntil(() => lws != null && lws.IsReady()); // first we make sure the world streamer is initialized
GameObject prefab = task.Get();
if (prefab == null)
{
InternalLogger.Error($"no prefab found for {stringToLog}; process for Coordinated Spawn canceled.");
Destroy(gameObject);
yield break;
}

if (lwe is {cellLevel: not (LargeWorldEntity.CellLevel.Batch or LargeWorldEntity.CellLevel.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)
LargeWorldEntity lwe = prefab.GetComponent<LargeWorldEntity>();

var bounds = new Bounds(spawnInfo.SpawnPosition, Vector3.zero);
if (!lwe)
{
InternalLogger.Error($"No LargeWorldEntity component found for prefab '{stringToLog}'; process for Coordinated Spawn canceled.");
Destroy(gameObject);
yield break;
}

if (!lws.cellManager.AreCellsLoaded(bounds, lwe.cellLevel))
if (lwe.cellLevel != LargeWorldEntity.CellLevel.Global && lwe.cellLevel != LargeWorldEntity.CellLevel.Batch && !lws.cellManager.AreCellsLoaded(bounds, lwe.cellLevel))
{
// Cells aren't ready yet. We have to wait until they are
yield return new WaitUntil(() => lws.cellManager.AreCellsLoaded(bounds, lwe.cellLevel));
}
}

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.

GameObject obj = Instantiate(prefab, spawnInfo.SpawnPosition, spawnInfo.Rotation);
obj.transform.localScale = spawnInfo.ActualScale;

GameObject obj = Instantiate(prefab, spawnInfo.SpawnPosition, spawnInfo.Rotation);
obj.transform.localScale = spawnInfo.ActualScale;

obj.SetActive(true);
obj.SetActive(true);

LargeWorldEntity.Register(obj);
LargeWorldEntity.Register(obj);

LargeWorldStreamerPatcher.SavedSpawnInfos.Add(spawnInfo);
InternalLogger.Debug($"spawned {stringToLog}.");

Destroy(gameObject);
LargeWorldStreamerPatcher.SavedSpawnInfos.Add(spawnInfo);
InternalLogger.Debug($"spawned {stringToLog}.");
}
}

private IEnumerator GetPrefabAsync(IOut<GameObject> gameObject)
private IEnumerator GetPrefabAsync(SpawnInfo spawnInfo, IOut<GameObject> gameObject)
{
GameObject obj;

Expand Down
85 changes: 51 additions & 34 deletions Nautilus/Patchers/LargeWorldStreamerPatcher.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections;

namespace Nautilus.Patchers;

using System;
Expand Down Expand Up @@ -29,9 +31,10 @@ internal static void Patch(Harmony harmony)
HarmonyMethod onBatchFullyLoadedPostfix = new(AccessTools.Method(typeof(LargeWorldStreamerPatcher), nameof(OnBatchFullyLoadedPostfix)));
harmony.Patch(onBatchFullyLoadedOrig, postfix: onBatchFullyLoadedPostfix);
}

internal static readonly HashSet<SpawnInfo> SpawnInfos = new();
internal static readonly HashSet<SpawnInfo> SavedSpawnInfos = new();
internal static readonly Dictionary<Int3, HashSet<SpawnInfo>> BatchToSpawnInfos = new();

private static readonly HashSet<SpawnInfo> _initialSpawnInfos = new();

Expand Down Expand Up @@ -59,7 +62,7 @@ private static void InitializePostfix()
}
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;
}
Expand All @@ -68,6 +71,8 @@ private static void InitializePostfix()
SpawnInfos.RemoveWhere(s => SavedSpawnInfos.Contains(s));
InternalLogger.Debug("Coordinated Spawns have been initialized in the current save.");

var globalSpawns = new HashSet<SpawnInfo>();

// Preload all the prefabs for faster spawning.
new List<SpawnInfo>(SpawnInfos).Do((info) =>
{
Expand All @@ -86,7 +91,7 @@ private static void InitializePostfix()
if (PrefabHandler.Prefabs.TryGetInfoForFileName(prefabName, out var prefabInfo))
{
InternalLogger.Debug($"Preloading {keyToCheck}");
CoroutineHost.StartCoroutine(PreloadModdedPrefab(info ,prefabInfo));
CoroutineHost.StartCoroutine(PreloadModdedPrefab(info, prefabInfo));
}
else
{
Expand Down Expand Up @@ -116,18 +121,42 @@ private static void InitializePostfix()

if (lwe is { cellLevel: LargeWorldEntity.CellLevel.Global })
{
CreateSpawner(info);
InternalLogger.Debug($"Created spawner for {keyToCheck} at the Global level.");
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<object> PreloadModdedPrefab(SpawnInfo info, PrefabInfo prefabInfo)
private static IEnumerator PreloadModdedPrefab(SpawnInfo info, PrefabInfo prefabInfo)
{
var request = new ModPrefabRequest(prefabInfo);
yield return request;
Expand All @@ -146,9 +175,14 @@ private static IEnumerator<object> PreloadModdedPrefab(SpawnInfo info, PrefabInf
}
if (lwe is { cellLevel: LargeWorldEntity.CellLevel.Global })
{
CreateSpawner(info);
InternalLogger.Debug($"Created spawner for {info.ClassId} at the Global level.");
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);
}
}
}

Expand Down Expand Up @@ -189,32 +223,15 @@ private static void InitializeSpawnInfos()
_initialized = true;
}

private static void OnBatchFullyLoadedPostfix(LargeWorldStreamer __instance, Int3 batchId)
{
var spawned = new HashSet<SpawnInfo>();
foreach (SpawnInfo spawnInfo in SpawnInfos)
{
if (__instance.GetContainingBatch(spawnInfo.SpawnPosition) == batchId)
{
CreateSpawner(spawnInfo);
spawned.Add(spawnInfo);
}
}

SpawnInfos.RemoveWhere(spawned.Contains);
}

private static void CreateSpawner(SpawnInfo spawnInfo)
private static void CreateSpawner(Int3 batch, IReadOnlyCollection<SpawnInfo> spawnInfos, bool global)
{
string keyToCheck = spawnInfo.Type switch
{
SpawnInfo.SpawnType.TechType => spawnInfo.TechType.AsString(),
_ => spawnInfo.ClassId
};

InternalLogger.Debug($"Creating Spawner for {keyToCheck}");
GameObject obj = new($"{keyToCheck}Spawner");
obj.transform.SetPositionAndRotation(spawnInfo.SpawnPosition, spawnInfo.Rotation);
obj.EnsureComponent<EntitySpawner>().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<EntitySpawner>();
spawner.batchId = batch;
spawner.spawnInfos = spawnInfos;
spawner.global = global;
}
}