using Cysharp.Threading.Tasks; using MonoMod.Utils; using OWML.Common; using QSB.ConversationSync.Patches; using QSB.LogSync; 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 (manager.DlcOnly && !QSBCore.DLCInstalled) { continue; } switch (manager.WorldObjectScene) { case WorldObjectScene.SolarSystem when QSBSceneManager.CurrentScene != OWScene.SolarSystem: case WorldObjectScene.Eye when QSBSceneManager.CurrentScene != OWScene.EyeOfTheUniverse: 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 item in WorldObjects) { item.Try("removing", item.OnRemoval); } WorldObjects.Clear(); UnityObjectsToWorldObjects.Clear(); foreach (var manager in Managers) { manager.Try("unbuilding world objects", manager.UnbuildWorldObjects); } } // ======================================================================================================= public static readonly List OldDialogueTrees = new(); public static readonly Dictionary DialogueConditions = new(); private static readonly Dictionary PersistentConditions = new(); public static readonly List ShipLogFacts = new(); private static readonly List WorldObjects = new(); private static readonly Dictionary UnityObjectsToWorldObjects = new(); private static void GameInit() { DebugLog.DebugWrite($"GameInit QSBWorldSync", MessageType.Info); OldDialogueTrees.Clear(); OldDialogueTrees.AddRange(GetUnityObjects().SortDeterministic()); if (!QSBCore.IsHost) { return; } DebugLog.DebugWrite($"DIALOGUE CONDITIONS :"); DialogueConditions.Clear(); DialogueConditions.AddRange(DialogueConditionManager.SharedInstance._dictConditions); foreach (var item in DialogueConditions) { DebugLog.DebugWrite($"- {item.Key}, {item.Value}"); } DebugLog.DebugWrite($"PERSISTENT CONDITIONS :"); var dictConditions = PlayerData._currentGameSave.dictConditions; var syncedConditions = dictConditions.Where(x => ConversationPatches.PersistentConditionsToSync.Contains(x.Key)); PersistentConditions.Clear(); PersistentConditions.AddRange(syncedConditions.ToDictionary(x => x.Key, x => x.Value)); foreach (var item in PersistentConditions) { DebugLog.DebugWrite($"- {item.Key}, {item.Value}"); } } private static void GameReset() { DebugLog.DebugWrite($"GameReset QSBWorldSync", MessageType.Info); OldDialogueTrees.Clear(); DialogueConditions.Clear(); PersistentConditions.Clear(); ShipLogFacts.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 - WorldObjectsToUnityObjects 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 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 { WorldObjects.Add(worldObject); UnityObjectsToWorldObjects.Add(unityObject, worldObject); 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 }); } }