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 11 commits
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
19 changes: 18 additions & 1 deletion Nautilus/Assets/ModPrefabCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public void EnterPrefabIntoCache(GameObject prefab)
else
{
prefab.transform.parent = _prefabRoot;
ResetIds(prefab);
prefab.SetActive(true);
}
}
Expand All @@ -121,12 +122,28 @@ 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);
InternalLogger.Debug($"ModPrefabCache: removed prefab {classId}");
Entries.Remove(classId);
}
}

private void ResetIds(GameObject prefab)
{
var uniqueIds = prefab.GetAllComponentsInChildren<UniqueIdentifier>();

foreach (var uniqueId in uniqueIds)
{
if (string.IsNullOrEmpty(uniqueId.id))
{
continue;
}

UniqueIdentifier.identifiers.Remove(uniqueId.id);
uniqueId.id = null;
}
}
}
15 changes: 11 additions & 4 deletions Nautilus/Handlers/CoordinatedSpawnsHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using Nautilus.Assets;
using Nautilus.Patchers;
using Nautilus.Utility;
using Newtonsoft.Json;
using UnityEngine;

Expand All @@ -19,11 +20,17 @@ public static class CoordinatedSpawnsHandler
/// <param name="spawnInfo">the SpawnInfo to spawn.</param>
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);
}
}

/// <summary>
Expand Down
116 changes: 74 additions & 42 deletions Nautilus/MonoBehaviours/EntitySpawner.cs
Original file line number Diff line number Diff line change
@@ -1,74 +1,106 @@
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<SpawnInfo> spawnInfos;
internal bool global;
private readonly List<SpawnInfo> _delayedSpawns = new List<SpawnInfo>();

void Start()
private IEnumerator Start()
{
StartCoroutine(SpawnAsync());
yield return SpawnAsync();
yield return new WaitUntil(() => _delayedSpawns.Count == 0);
Destroy(gameObject);
}

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

TaskResult<GameObject> 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<LargeWorldEntity>();

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<GameObject> task = new();
yield return GetPrefabAsync(spawnInfo, task);

GameObject prefab = task.Get();
if (prefab == null)
{
InternalLogger.Error($"no prefab found for {stringToLog}; process for Coordinated Spawn canceled.");
continue;
}

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

if (!lwe)
{
// @Metious: I don't think this is necessary because LargeWorldEntity.Register(obj) Ensures that the object has a LargeWorldEntity component so we could replace
// lwe.cellLevel < LargeWorldEntity.CellLevel.Batch with (lwe == null || lwe.cellLevel < LargeWorldEntity.CellLevel.Batch) and it should work fine.
InternalLogger.Error($"No LargeWorldEntity component found for prefab '{stringToLog}'; process for Coordinated Spawn canceled.");
continue;
// 😎 Nice.
}

if (lwe.cellLevel < LargeWorldEntity.CellLevel.Batch && !lws.IsRangeActiveAndBuilt(new Bounds(spawnInfo.SpawnPosition, Vector3.one)))
{
// Cells aren't ready yet. We have to wait until they are.
StartCoroutine(WaitForCellLoaded(lws, prefab, spawnInfo, stringToLog));
continue;
}
Metious marked this conversation as resolved.
Show resolved Hide resolved

Spawn(prefab, spawnInfo, stringToLog);
}
}

private IEnumerator WaitForCellLoaded(LargeWorldStreamer lws, GameObject prefab, SpawnInfo spawnInfo, string stringToLog)
{
_delayedSpawns.Add(spawnInfo);
yield return new WaitUntil(() => lws.IsRangeActiveAndBuilt(new Bounds(spawnInfo.SpawnPosition, Vector3.one * 5)));
Spawn(prefab, spawnInfo, stringToLog);
_delayedSpawns.Remove(spawnInfo);
}

private void Spawn(GameObject prefab, SpawnInfo spawnInfo, string stringToLog)
{
GameObject obj = Instantiate(prefab, spawnInfo.SpawnPosition, spawnInfo.Rotation);
obj.transform.localScale = spawnInfo.ActualScale;

obj.SetActive(true);

LargeWorldStreamerPatcher.savedSpawnInfos.Add(spawnInfo);
LargeWorldEntity.Register(obj);

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

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

Expand Down
Loading