diff --git a/QSB/Animation/NPC/Patches/SolanumPatches.cs b/QSB/Animation/NPC/Patches/SolanumPatches.cs index 0fbe03b6..69c3e616 100644 --- a/QSB/Animation/NPC/Patches/SolanumPatches.cs +++ b/QSB/Animation/NPC/Patches/SolanumPatches.cs @@ -17,6 +17,11 @@ public class SolanumPatches : QSBPatch [HarmonyPatch(typeof(SolanumAnimController), nameof(SolanumAnimController.LateUpdate))] public static bool SolanumLateUpdateReplacement(SolanumAnimController __instance) { + if (!QSBWorldSync.AllObjectsReady) + { + return true; + } + if (__instance._animatorStateEvents == null) { __instance._animatorStateEvents = __instance._animator.GetBehaviour(); @@ -43,6 +48,10 @@ public class SolanumPatches : QSBPatch [HarmonyPatch(typeof(NomaiConversationManager), nameof(NomaiConversationManager.Update))] public static bool ReplacementUpdate(NomaiConversationManager __instance) { + if (!QSBWorldSync.AllObjectsReady) + { + return true; + } var qsbObj = __instance._solanumAnimController.GetWorldObject(); __instance._playerInWatchVolume = qsbObj.Trigger.Occupants.Any(); @@ -206,4 +215,4 @@ public class SolanumPatches : QSBPatch return false; } -} \ No newline at end of file +} diff --git a/QSB/EchoesOfTheEye/QSBRotatingElements.cs b/QSB/EchoesOfTheEye/QSBRotatingElements.cs index 05b08c69..9be7c3eb 100644 --- a/QSB/EchoesOfTheEye/QSBRotatingElements.cs +++ b/QSB/EchoesOfTheEye/QSBRotatingElements.cs @@ -37,10 +37,13 @@ internal abstract class QSBRotatingElements : LinkedWorldObject { base.OnRemoval(); - foreach (var lightSensor in _qsbLightSensors) + if (_qsbLightSensors != null) { - lightSensor.OnDetectLocalLight -= OnDetectLocalLight; - lightSensor.OnDetectLocalDarkness -= OnDetectLocalDarkness; + foreach (var lightSensor in _qsbLightSensors) + { + lightSensor.OnDetectLocalLight -= OnDetectLocalLight; + lightSensor.OnDetectLocalDarkness -= OnDetectLocalDarkness; + } } } diff --git a/QSB/ItemSync/WorldObjects/Items/QSBItem.cs b/QSB/ItemSync/WorldObjects/Items/QSBItem.cs index bfb67695..c1417b32 100644 --- a/QSB/ItemSync/WorldObjects/Items/QSBItem.cs +++ b/QSB/ItemSync/WorldObjects/Items/QSBItem.cs @@ -1,9 +1,11 @@ using Cysharp.Threading.Tasks; +using OWML.Common; using QSB.ItemSync.Messages; using QSB.ItemSync.WorldObjects.Sockets; using QSB.Messaging; using QSB.Player; using QSB.SectorSync.WorldObjects; +using QSB.Utility; using QSB.WorldSync; using System.Threading; using UnityEngine; diff --git a/QSB/MeteorSync/MeteorManager.cs b/QSB/MeteorSync/MeteorManager.cs index 65226a38..29c841f8 100644 --- a/QSB/MeteorSync/MeteorManager.cs +++ b/QSB/MeteorSync/MeteorManager.cs @@ -17,7 +17,9 @@ public class MeteorManager : WorldObjectManager // wait for all late initializers (which includes meteor launchers) to finish await UniTask.WaitUntil(() => LateInitializerManager.isDoneInitializing, cancellationToken: ct); - WhiteHoleVolume = QSBWorldSync.GetUnityObject(); + // NH can make multiple so ensure its the stock whitehole + var whiteHole = QSBWorldSync.GetUnityObjects().First(x => x.GetAstroObjectName() == AstroObject.Name.WhiteHole); + WhiteHoleVolume = whiteHole?.GetComponentInChildren(); QSBWorldSync.Init(); QSBWorldSync.Init(); QSBWorldSync.Init(); diff --git a/QSB/ModelShip/ModelShipManager.cs b/QSB/ModelShip/ModelShipManager.cs index 23417fb3..4caae0ff 100644 --- a/QSB/ModelShip/ModelShipManager.cs +++ b/QSB/ModelShip/ModelShipManager.cs @@ -37,6 +37,13 @@ internal class ModelShipManager : WorldObjectManager public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) { + // NH can remove this + var modelShip = QSBWorldSync.GetUnityObject()._modelShipBody; + if (!modelShip) + { + return; + } + if (QSBCore.IsHost) { Instantiate(QSBNetworkManager.singleton.ModelShipPrefab).SpawnWithServerAuthority(); diff --git a/QSB/QSBCore.cs b/QSB/QSBCore.cs index 9cf2191f..48e9137c 100644 --- a/QSB/QSBCore.cs +++ b/QSB/QSBCore.cs @@ -69,11 +69,14 @@ public class QSBCore : ModBehaviour public static DebugSettings DebugSettings { get; private set; } = new(); public static Storage Storage { get; private set; } = new(); + public const string NEW_HORIZONS = "xen.NewHorizons"; + public const string NEW_HORIZONS_COMPAT = "xen.NHQSBCompat"; + public static readonly string[] IncompatibleMods = { // incompatible mods "Raicuparta.NomaiVR", - "xen.NewHorizons", + // "xen.NewHorizons", "Vesper.AutoResume", "Vesper.OuterWildsMMO", "_nebula.StopTime", @@ -127,6 +130,8 @@ public class QSBCore : ModBehaviour Helper = ModHelper; DebugLog.ToConsole($"* Start of QSB version {QSBVersion} - authored by {Helper.Manifest.Author}", MessageType.Info); + CheckCompatibilityMods(); + DebugSettings = Helper.Storage.Load("debugsettings.json") ?? new DebugSettings(); Storage = Helper.Storage.Load("storage.json") ?? new Storage(); @@ -270,6 +275,26 @@ public class QSBCore : ModBehaviour DebugLog.ToConsole($"DEBUG MODE = {DebugSettings.DebugMode}"); } } + + private void CheckCompatibilityMods() + { + var mainMod = ""; + var compatMod = ""; + var missingCompat = false; + + if (Helper.Interaction.ModExists(NEW_HORIZONS) && !Helper.Interaction.ModExists(NEW_HORIZONS_COMPAT)) + { + mainMod = NEW_HORIZONS; + compatMod = NEW_HORIZONS_COMPAT; + missingCompat = true; + } + + if (missingCompat) + { + DebugLog.ToConsole($"FATAL - You have mod \"{mainMod}\" installed, which is not compatible with QSB without the compatibility mod \"{compatMod}\". " + + $"Either disable the mod, or install/enable the compatibility mod.", MessageType.Fatal); + } + } } /* diff --git a/QSB/SectorSync/QSBSectorManager.cs b/QSB/SectorSync/QSBSectorManager.cs index 3d345bfd..389060ff 100644 --- a/QSB/SectorSync/QSBSectorManager.cs +++ b/QSB/SectorSync/QSBSectorManager.cs @@ -91,11 +91,15 @@ public class QSBSectorManager : WorldObjectManager // time loop spinning ring { + // NH can remove this var TimeLoopRing_Body = GameObject.Find("TimeLoopRing_Body"); - var Sector_TimeLoopInterior = GameObject.Find("Sector_TimeLoopInterior").GetComponent(); - // use the same trigger as the parent sector - FakeSector.Create(TimeLoopRing_Body, Sector_TimeLoopInterior, - x => x._triggerRoot = Sector_TimeLoopInterior._triggerRoot); + if (TimeLoopRing_Body) + { + var Sector_TimeLoopInterior = GameObject.Find("Sector_TimeLoopInterior").GetComponent(); + // use the same trigger as the parent sector + FakeSector.Create(TimeLoopRing_Body, Sector_TimeLoopInterior, + x => x._triggerRoot = Sector_TimeLoopInterior._triggerRoot); + } } // TH elevators diff --git a/QSB/Syncs/Occasional/OccasionalManager.cs b/QSB/Syncs/Occasional/OccasionalManager.cs index 6f229299..c53a6dfd 100644 --- a/QSB/Syncs/Occasional/OccasionalManager.cs +++ b/QSB/Syncs/Occasional/OccasionalManager.cs @@ -7,6 +7,7 @@ using System.Threading; namespace QSB.Syncs.Occasional; +// BUG: somehow, not including DontDestroyOnLoad things makes this fuck up with NH internal class OccasionalManager : WorldObjectManager { public override WorldObjectScene WorldObjectScene => WorldObjectScene.SolarSystem; @@ -21,6 +22,11 @@ internal class OccasionalManager : WorldObjectManager foreach (var proxy in cannon._realDebrisSectorProxies) { + // NH can remove these + if (!proxy) + { + continue; + } SpawnOccasional(proxy.transform.root.GetAttachedOWRigidbody(), gdBody); } @@ -58,4 +64,4 @@ internal class OccasionalManager : WorldObjectManager Bodies.Clear(); } -} \ No newline at end of file +} diff --git a/QSB/WorldSync/QSBWorldSync.cs b/QSB/WorldSync/QSBWorldSync.cs index c57d6eeb..d6446f37 100644 --- a/QSB/WorldSync/QSBWorldSync.cs +++ b/QSB/WorldSync/QSBWorldSync.cs @@ -11,6 +11,7 @@ using QSB.Utility; using QSB.Utility.LinkedWorldObject; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using UnityEngine; @@ -20,7 +21,8 @@ namespace QSB.WorldSync; public static class QSBWorldSync { public static WorldObjectManager[] Managers; - public static string WorldObjectsHash { get; private set; } + public static Dictionary ManagerHashes { get; private set; } = new(); + public static Dictionary> ManagerToBuiltObjects { get; private set; } = new(); /// /// Set when all WorldObjectManagers have called Init() on all their objects (AKA all the objects are created) @@ -85,12 +87,20 @@ public static class QSBWorldSync DeterministicManager.OnWorldObjectsAdded(); - WorldObjectsHash = WorldObjects.Select(x => x.GetType().Name).GetMD5Hash(); - DebugLog.DebugWrite($"WorldObject hash is {WorldObjectsHash}"); + foreach (var item in ManagerToBuiltObjects) + { + var worldObjects = item.Value; + var hash = worldObjects.Select(x => x.GetType().Name).GetMD5Hash(); + ManagerHashes[item.Key] = hash; + } if (!QSBCore.IsHost) { - new WorldObjectsHashMessage().Send(); + foreach (var item in ManagerHashes) + { + new WorldObjectsHashMessage(item.Key, item.Value).Send(); + } + new RequestLinksMessage().Send(); } @@ -135,6 +145,9 @@ public static class QSBWorldSync AllObjectsAdded = false; AllObjectsReady = false; + ManagerToBuiltObjects = new(); + ManagerHashes = new(); + GameReset(); foreach (var worldObject in WorldObjects) @@ -193,7 +206,8 @@ public static class QSBWorldSync { // So objects have time to be deleted, made, whatever // i.e. wait until Start has been called - Delay.RunNextFrame(() => BuildWorldObjects(loadScene).Forget()); + // TODO: see if this number of frames actually works. TWEAK! + Delay.RunFramesLater(10, () => BuildWorldObjects(loadScene).Forget()); } }; @@ -306,12 +320,55 @@ public static class QSBWorldSync => GetUnityObjects().Single(); /// - /// not deterministic across platforms + /// not deterministic across platforms. + /// excludes prefabs and DontDestroyOnLoad objects. /// public static IEnumerable GetUnityObjects() where TUnityObject : MonoBehaviour => Resources.FindObjectsOfTypeAll() - .Where(x => x.gameObject.scene.name != null); + .Where(x => x.gameObject.scene.name is not (null or "DontDestroyOnLoad")); + + // https://stackoverflow.com/a/48570616 + public static string NameOfCallingClass() + { + string fullName; + Type declaringType; + var skipFrames = 2; + do + { + var method = new StackFrame(skipFrames, false).GetMethod(); + declaringType = method.DeclaringType; + if (declaringType == null) + { + return method.Name; + } + + skipFrames++; + fullName = CleanupFullName(declaringType.FullName); + } + while (declaringType.Module.Name.Equals("mscorlib.dll", StringComparison.OrdinalIgnoreCase) || declaringType == typeof(QSBWorldSync)); + + return fullName; + } + + private static string CleanupFullName(string fullName) + { + var ret = fullName; + + var indexOfPlus = fullName.LastIndexOf('+'); + if (indexOfPlus != -1) + { + ret = fullName.Remove(indexOfPlus); + } + + var indexOfDot = ret.LastIndexOf('.'); + if (indexOfDot != -1) + { + ret = ret.Substring(indexOfDot + 1); + } + + return ret; + } public static void Init() where TWorldObject : WorldObject, new() @@ -382,6 +439,17 @@ public static class QSBWorldSync return; } + var className = NameOfCallingClass(); + + if (!ManagerToBuiltObjects.ContainsKey(className)) + { + ManagerToBuiltObjects.Add(className, new List { worldObject }); + } + else + { + ManagerToBuiltObjects[className].Add(worldObject); + } + WorldObjects.Add(worldObject); RequestInitialStatesMessage.SendInitialState += worldObject.SendInitialState; diff --git a/QSB/WorldSync/WorldObjectsHashMessage.cs b/QSB/WorldSync/WorldObjectsHashMessage.cs index 4d5e6bd2..fc18ec15 100644 --- a/QSB/WorldSync/WorldObjectsHashMessage.cs +++ b/QSB/WorldSync/WorldObjectsHashMessage.cs @@ -8,23 +8,19 @@ namespace QSB.WorldSync; /// /// sends QSBWorldSync.WorldObjectsHash to the server for sanity checking /// -internal class WorldObjectsHashMessage : QSBMessage +internal class WorldObjectsHashMessage : QSBMessage<(string managerName, string hash)> { - public WorldObjectsHashMessage() : base(QSBWorldSync.WorldObjectsHash) => To = 0; + public WorldObjectsHashMessage(string managerName, string hash) : base((managerName, hash)) => To = 0; public override void OnReceiveRemote() { - var serverHash = QSBWorldSync.WorldObjectsHash; + var serverHash = QSBWorldSync.ManagerHashes[Data.managerName]; - if (serverHash != Data) + if (serverHash != Data.hash) { // oh fuck oh no oh god - DebugLog.ToConsole($"Kicking {From} because their WorldObjects hash is wrong. (server:{serverHash}, client:{Data})", MessageType.Error); - new PlayerKickMessage(From, $"WorldObject hash error. (Server:{serverHash}, Client:{Data})").Send(); - } - else - { - DebugLog.DebugWrite($"WorldObject hash from {From} verified!", MessageType.Success); + DebugLog.ToConsole($"Kicking {From} because their WorldObjects hash for {Data.managerName} is wrong. (server:{serverHash}, client:{Data.hash})", MessageType.Error); + new PlayerKickMessage(From, $"WorldObject hash error for {Data.managerName}. (Server:{serverHash}, Client:{Data.hash})").Send(); } } }