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
Next Next commit
Coordinated Spawns (Purple Edition)
  • Loading branch information
MrPurple6411 committed Jan 14, 2024
commit e7e7866edfa930262a956ca1f0f29e35b954a825
10 changes: 5 additions & 5 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,10 @@ public static class CoordinatedSpawnsHandler
/// <param name="spawnInfo">the SpawnInfo to spawn.</param>
public static void RegisterCoordinatedSpawn(SpawnInfo spawnInfo)
{
if (!LargeWorldStreamerPatcher.spawnInfos.Add(spawnInfo))
return;

if (uGUI.isMainLevel)
LargeWorldStreamerPatcher.CreateSpawner(spawnInfo);
if (!LargeWorldStreamerPatcher.SpawnInfos.Add(spawnInfo))
{
InternalLogger.Error($"SpawnInfo {spawnInfo} already registered.");
}
}

/// <summary>
Expand Down
34 changes: 10 additions & 24 deletions Nautilus/MonoBehaviours/EntitySpawner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ IEnumerator SpawnAsync()
};

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

GameObject prefab = task.Get();
if (prefab == null)
Expand All @@ -35,40 +35,26 @@ IEnumerator SpawnAsync()
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
LargeWorldEntity lwe = prefab.GetComponent<LargeWorldEntity>();

// 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 (lwe == null)
{
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)
InternalLogger.Error($"no LargeWorldEntity found for {stringToLog}; process for Coordinated Spawn canceled.");
Destroy(gameObject);
yield break;
}

LargeWorld lw = LargeWorld.main;
GameObject obj = UWE.Utils.InstantiateDeactivated(prefab, spawnInfo.SpawnPosition, spawnInfo.Rotation, spawnInfo.ActualScale);
lwe = obj.GetComponent<LargeWorldEntity>();

yield return new WaitUntil(() => lw != null && lw.streamer.globalRoot != null); // need to make sure global root is ready too for global spawns.

lw.streamer.cellManager.RegisterEntity(obj);
yield return new WaitUntil(()=> LargeWorld.main?.streamer?.cellManager?.RegisterEntity(lwe)?? false); // then we register the entity to the cell manager

obj.SetActive(true);

LargeWorldStreamerPatcher.savedSpawnInfos.Add(spawnInfo);

Destroy(gameObject);
}

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

Expand Down
92 changes: 66 additions & 26 deletions Nautilus/Patchers/LargeWorldStreamerPatcher.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
namespace Nautilus.Patchers;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using Nautilus.Handlers;
using Nautilus.Json.Converters;
using Nautilus.MonoBehaviours;
using Nautilus.Utility;
using Newtonsoft.Json;
using UnityEngine;
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<SpawnInfo> spawnInfos = new();
internal static readonly HashSet<SpawnInfo> savedSpawnInfos = new();
private static readonly HashSet<SpawnInfo> initialSpawnInfos = new();
internal static readonly HashSet<SpawnInfo> SpawnInfos = new();
internal static readonly HashSet<SpawnInfo> SavedSpawnInfos = new();

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

private static bool initialized;
private static bool _initialized;

private static void InitializePostfix()
{
Expand All @@ -43,7 +49,7 @@ private static void InitializePostfix()
List<SpawnInfo> deserializedList = JsonConvert.DeserializeObject<List<SpawnInfo>>(reader.ReadToEnd(), new Vector3Converter(), new QuaternionConverter());
if (deserializedList is not null)
{
savedSpawnInfos.AddRange(deserializedList);
SavedSpawnInfos.AddRange(deserializedList);
}

reader.Close();
Expand All @@ -56,10 +62,15 @@ private static void InitializePostfix()
}
}

spawnInfos.RemoveWhere(s => savedSpawnInfos.Contains(s));

InitializeSpawners();
SpawnInfos.RemoveWhere(s => SavedSpawnInfos.Contains(s));
InternalLogger.Debug("Coordinated Spawns have been initialized in the current save.");

// Preload all the prefabs for faster spawning.
SpawnInfos.Do((info) =>
{
// Ensures the prefab is loaded in the cache for faster spawning.
CoroutineHost.StartCoroutine(EntitySpawner.GetPrefabAsync(new TaskResult<GameObject>(), info));
});
}

private static void SaveData()
Expand All @@ -68,7 +79,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();
Expand All @@ -79,43 +90,72 @@ 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);

_initialSpawnInfos.AddRange(SpawnInfos);
SaveUtils.RegisterOnSaveEvent(SaveData);
initialized = true;
_initialized = true;
}

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

spawned.Do((info) =>
{
SavedSpawnInfos.Add(info);
SpawnInfos.Remove(info);
});
}

internal static void CreateSpawner(SpawnInfo spawnInfo)
private static bool CreateSpawner(SpawnInfo spawnInfo, LargeWorldStreamer streamer)
{
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.SetActive(false);

obj.transform.SetPositionAndRotation(spawnInfo.SpawnPosition, spawnInfo.Rotation);
LargeWorldEntity lwe = obj.EnsureComponent<LargeWorldEntity>();
lwe.cellLevel = LargeWorldEntity.CellLevel.Batch;
obj.EnsureComponent<EntitySpawner>().spawnInfo = spawnInfo;

if (!streamer.cellManager.RegisterEntity(lwe))
{
GameObject.Destroy(obj);
return false;
}

obj.SetActive(true);
return true;
}
}