using Cysharp.Threading.Tasks; using MonoMod.Utils; using OWML.Common; using QSB.ConversationSync.Messages; using QSB.LogSync; using QSB.LogSync.Messages; using QSB.Messaging; using QSB.Player.TransformSync; using QSB.TriggerSync.WorldObjects; using QSB.Utility; using QSB.Utility.LinkedWorldObject; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using UnityEngine; namespace QSB.WorldSync; public static class QSBWorldSync { public static WorldObjectManager[] Managers; /// /// Set when all WorldObjectManagers have called Init() on all their objects (AKA all the objects are created) /// public static bool AllObjectsAdded { get; private set; } /// /// Set when all WorldObjects have finished running Init() /// public static bool AllObjectsReady { get; private set; } private static CancellationTokenSource _cts; private static readonly Dictionary _managersBuilding = new(); private static readonly Dictionary _objectsIniting = new(); public static async UniTaskVoid BuildWorldObjects(OWScene scene) { if (_cts != null) { return; } _cts = new CancellationTokenSource(); if (!PlayerTransformSync.LocalInstance) { DebugLog.ToConsole("Warning - Tried to build WorldObjects when LocalPlayer is not ready! Building when ready...", MessageType.Warning); await UniTask.WaitUntil(() => PlayerTransformSync.LocalInstance, cancellationToken: _cts.Token); } GameInit(); foreach (var manager in Managers) { if (ShouldIgnoreManager(manager)) { continue; } var task = UniTask.Create(async () => { await manager.Try("building world objects", async () => { await manager.BuildWorldObjects(scene, _cts.Token); DebugLog.DebugWrite($"Built {manager}", MessageType.Info); }); _managersBuilding.Remove(manager); }); if (!task.Status.IsCompleted()) { _managersBuilding.Add(manager, task); } } await _managersBuilding.Values; if (_cts == null) { return; } AllObjectsAdded = true; DebugLog.DebugWrite("World Objects added.", MessageType.Success); if (!QSBCore.IsHost) { new RequestLinksMessage().Send(); } await _objectsIniting.Values; if (_cts == null) { return; } AllObjectsReady = true; DebugLog.DebugWrite("World Objects ready.", MessageType.Success); DeterministicManager.WorldObjectsReady(); if (!QSBCore.IsHost) { new RequestInitialStatesMessage().Send(); } } public static void RemoveWorldObjects() { if (_cts == null) { return; } if (_managersBuilding.Count > 0) { DebugLog.DebugWrite($"{_managersBuilding.Count} managers still building", MessageType.Warning); } if (_objectsIniting.Count > 0) { DebugLog.DebugWrite($"{_objectsIniting.Count} objects still initing", MessageType.Warning); } _cts.Cancel(); _cts.Dispose(); _cts = null; _managersBuilding.Clear(); _objectsIniting.Clear(); AllObjectsAdded = false; AllObjectsReady = false; GameReset(); foreach (var worldObject in WorldObjects) { worldObject.Try("removing", worldObject.OnRemoval); RequestInitialStatesMessage.SendInitialState -= worldObject.SendInitialState; } WorldObjects.Clear(); UnityObjectsToWorldObjects.Clear(); foreach (var manager in Managers) { if (ShouldIgnoreManager(manager)) { continue; } manager.Try("unbuilding world objects", manager.UnbuildWorldObjects); } } private static bool ShouldIgnoreManager(WorldObjectManager manager) { if (manager.DlcOnly && !QSBCore.DLCInstalled) { return true; } switch (manager.WorldObjectScene) { case WorldObjectScene.SolarSystem when QSBSceneManager.CurrentScene != OWScene.SolarSystem: case WorldObjectScene.Eye when QSBSceneManager.CurrentScene != OWScene.EyeOfTheUniverse: return true; } return false; } // ======================================================================================================= public static readonly List OldDialogueTrees = new(); private static readonly Dictionary DialogueConditions = new(); private static readonly Dictionary PersistentConditions = new(); private static readonly List ShipLogFacts = new(); private static readonly List WorldObjects = new(); private static readonly Dictionary UnityObjectsToWorldObjects = new(); private static readonly Dictionary CachedUnityObjects = new(); static QSBWorldSync() => RequestInitialStatesMessage.SendInitialState += to => { DialogueConditions.ForEach(condition => new DialogueConditionMessage(condition.Key, condition.Value) { To = to }.Send()); ShipLogFacts.ForEach(fact => new RevealFactMessage(fact.Id, fact.SaveGame, false) { To = to }.Send()); }; private static void GameInit() { DebugLog.DebugWrite("GameInit QSBWorldSync", MessageType.Info); OldDialogueTrees.Clear(); OldDialogueTrees.AddRange(GetUnityObjects().SortDeterministic()); if (!QSBCore.IsHost) { return; } DialogueConditions.Clear(); DialogueConditions.AddRange(DialogueConditionManager.SharedInstance._dictConditions); PersistentConditions.Clear(); PersistentConditions.AddRange(PlayerData._currentGameSave.dictConditions); } private static void GameReset() { DebugLog.DebugWrite("GameReset QSBWorldSync", MessageType.Info); OldDialogueTrees.Clear(); DialogueConditions.Clear(); PersistentConditions.Clear(); ShipLogFacts.Clear(); CachedUnityObjects.Clear(); } public static IEnumerable GetWorldObjects() => WorldObjects; public static IEnumerable GetWorldObjects() where TWorldObject : IWorldObject => WorldObjects.OfType(); public static TWorldObject GetWorldObject(this int objectId) where TWorldObject : IWorldObject { if (!WorldObjects.IsInRange(objectId)) { DebugLog.ToConsole($"Warning - Tried to find {typeof(TWorldObject).Name} id {objectId}. Count is {WorldObjects.Count}.", MessageType.Warning); return default; } if (WorldObjects[objectId] is not TWorldObject worldObject) { DebugLog.ToConsole($"Error - {typeof(TWorldObject).Name} id {objectId} is actually {WorldObjects[objectId].GetType().Name}.", MessageType.Error); return default; } return worldObject; } public static TWorldObject GetWorldObject(this MonoBehaviour unityObject) where TWorldObject : IWorldObject { if (!unityObject) { DebugLog.ToConsole($"Error - Trying to run GetWorldFromUnity with a null unity object! TWorldObject:{typeof(TWorldObject).Name}, TUnityObject:NULL, Stacktrace:\r\n{Environment.StackTrace}", MessageType.Error); return default; } if (!UnityObjectsToWorldObjects.TryGetValue(unityObject, out var worldObject)) { DebugLog.ToConsole($"Error - UnityObjectsToWorldObjects does not contain \"{unityObject.name}\"! TWorldObject:{typeof(TWorldObject).Name}, TUnityObject:{unityObject.GetType().Name}, Stacktrace:\r\n{Environment.StackTrace}", MessageType.Error); return default; } return (TWorldObject)worldObject; } /// /// not deterministic across platforms /// public static TUnityObject GetUnityObject() where TUnityObject : MonoBehaviour { if (CachedUnityObjects.ContainsKey(typeof(TUnityObject))) { return CachedUnityObjects[typeof(TUnityObject)] as TUnityObject; } var unityObjects = GetUnityObjects(); if (unityObjects.Count() != 1) { DebugLog.ToConsole($"Warning - Tried to cache a unity object that there are multiple of. ({typeof(TUnityObject).Name})" + $"\r\nCaching the first one - this probably is going to end badly!", MessageType.Warning); } var unityObject = unityObjects.First(); CachedUnityObjects.Add(typeof(TUnityObject), unityObject); return unityObject; } /// /// not deterministic across platforms /// public static IEnumerable GetUnityObjects() where TUnityObject : MonoBehaviour => Resources.FindObjectsOfTypeAll() .Where(x => x.gameObject.scene.name != null); public static void Init() where TWorldObject : WorldObject, new() where TUnityObject : MonoBehaviour { var list = GetUnityObjects().SortDeterministic(); Init(list); } public static void Init(params Type[] typesToExclude) where TWorldObject : WorldObject, new() where TUnityObject : MonoBehaviour { var list = GetUnityObjects() .Where(x => !typesToExclude.Contains(x.GetType())) .SortDeterministic(); Init(list); } /// /// make sure to sort the list! /// public static void Init(IEnumerable listToInitFrom) where TWorldObject : WorldObject, new() where TUnityObject : MonoBehaviour { foreach (var item in listToInitFrom) { var obj = new TWorldObject { AttachedObject = item, ObjectId = WorldObjects.Count }; AddAndInit(obj, item); } } public static void Init(Func triggerSelector) where TWorldObject : QSBTrigger, new() where TUnityObject : MonoBehaviour { var list = GetUnityObjects().SortDeterministic(); foreach (var owner in list) { var item = triggerSelector(owner); if (!item) { continue; } var obj = new TWorldObject { AttachedObject = item, ObjectId = WorldObjects.Count, TriggerOwner = owner }; AddAndInit(obj, item); } } private static void AddAndInit(TWorldObject worldObject, TUnityObject unityObject) where TWorldObject : WorldObject where TUnityObject : MonoBehaviour { if (!UnityObjectsToWorldObjects.TryAdd(unityObject, worldObject)) { DebugLog.ToConsole($"Error - UnityObjectsToWorldObjects already contains \"{unityObject.name}\"! TWorldObject:{typeof(TWorldObject).Name}, TUnityObject:{unityObject.GetType().Name}, Stacktrace:\r\n{Environment.StackTrace}", MessageType.Error); return; } WorldObjects.Add(worldObject); RequestInitialStatesMessage.SendInitialState += worldObject.SendInitialState; var task = UniTask.Create(async () => { await worldObject.Try("initing", () => worldObject.Init(_cts.Token)); _objectsIniting.Remove(worldObject); }); if (!task.Status.IsCompleted()) { _objectsIniting.Add(worldObject, task); } } public static void SetDialogueCondition(string name, bool state) { if (!QSBCore.IsHost) { DebugLog.ToConsole("Warning - Cannot write to dialogue condition dict when not server!", MessageType.Warning); return; } DialogueConditions[name] = state; } public static void SetPersistentCondition(string name, bool state) { if (!QSBCore.IsHost) { DebugLog.ToConsole("Warning - Cannot write to persistent condition dict when not server!", MessageType.Warning); return; } PersistentConditions[name] = state; } public static void AddFactReveal(string id, bool saveGame) { if (!QSBCore.IsHost) { DebugLog.ToConsole("Warning - Cannot write to fact list when not server!", MessageType.Warning); return; } if (ShipLogFacts.Any(x => x.Id == id)) { return; } ShipLogFacts.Add(new FactReveal { Id = id, SaveGame = saveGame }); } }