diff --git a/QSB-NH/Patches/GameStateMessagePatches.cs b/QSB-NH/Patches/GameStateMessagePatches.cs new file mode 100644 index 000000000..277f5f724 --- /dev/null +++ b/QSB-NH/Patches/GameStateMessagePatches.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using HarmonyLib; +using Mirror; +using NewHorizons; +using QSB; +using QSB.Patches; +using QSB.Player; +using QSB.SaveSync.Messages; +using QSB.Utility; + +namespace QSBNH.Patches; + + +internal class GameStateMessagePatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + + private static string _initialSystem; + private static int[] _hostAddonHash; + + [HarmonyPostfix] + [HarmonyPatch(typeof(GameStateMessage), nameof(GameStateMessage.Serialize))] + public static void GameStateMessage_Serialize(GameStateMessage __instance, NetworkWriter writer) + { + var currentSystem = QSBNH.Instance.NewHorizonsAPI.GetCurrentStarSystem(); + + writer.Write(currentSystem); + writer.WriteArray(QSBNH.HashAddonsForSystem(currentSystem)); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(GameStateMessage), nameof(GameStateMessage.Deserialize))] + public static void GameStateMessage_Deserialize(GameStateMessage __instance, NetworkReader reader) + { + _initialSystem = reader.Read(); + _hostAddonHash = reader.ReadArray(); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(GameStateMessage), nameof(GameStateMessage.OnReceiveRemote))] + public static void GameStateMessage_OnReceiveRemote() + { + if (QSBCore.IsHost) + { + DebugLog.DebugWrite($"Why is the host being given the initial state info?"); + } + else + { + DebugLog.DebugWrite($"Player#{QSBPlayerManager.LocalPlayerId} is being sent to {_initialSystem}"); + + WarpManager.RemoteChangeStarSystem(_initialSystem, false, false, _hostAddonHash); + } + } +} diff --git a/QSB-NH/QSB-NH.csproj b/QSB-NH/QSB-NH.csproj new file mode 100644 index 000000000..a4427787d --- /dev/null +++ b/QSB-NH/QSB-NH.csproj @@ -0,0 +1,32 @@ + + + + net48 + QSBNH + enable + enable + $(OwmlDir)\Mods\Raicuparta.QuantumSpaceBuddies + + + + + + + + + + + + + ..\Lib\Mirror.dll + + + lib\NewHorizons.dll + false + + + ..\Lib\UniTask.dll + + + + diff --git a/QSB-NH/QSBNH.cs b/QSB-NH/QSBNH.cs new file mode 100644 index 000000000..01c8e2ad9 --- /dev/null +++ b/QSB-NH/QSBNH.cs @@ -0,0 +1,59 @@ +using Mirror; +using NewHorizons; +using OWML.Common; +using OWML.ModHelper; +using QSB; +using QSB.Utility; +using UnityEngine; + +namespace QSBNH +{ + public class QSBNH : MonoBehaviour + { + public static QSBNH Instance; + + public INewHorizons NewHorizonsAPI; + + private void Start() + { + Instance = this; + DebugLog.DebugWrite($"Start of QSB-NH compatibility code.", MessageType.Success); + NewHorizonsAPI = QSBCore.Helper.Interaction.TryGetModApi("xen.NewHorizons"); + } + + public static string HashToMod(int hash) + { + foreach (var mod in NewHorizons.Main.MountedAddons) + { + var name = mod.ModHelper.Manifest.UniqueName; + if (name.GetStableHashCode() == hash) + { + return name; + } + } + + return null; + } + + public static int[] HashAddonsForSystem(string system) + { + if (NewHorizons.Main.BodyDict.TryGetValue(system, out var bodies)) + { + var addonHashes = bodies + .Where(x => x.Mod.ModHelper.Manifest.UniqueName != "xen.NewHorizons") + .Select(x => x.Mod.ModHelper.Manifest.UniqueName.GetStableHashCode()) + .Distinct(); + + var nhPlanetHashes = bodies + .Where(x => x.Mod.ModHelper.Manifest.UniqueName == "xen.NewHorizons") + .Select(x => x.Config.name.GetStableHashCode()); + + return addonHashes.Concat(nhPlanetHashes).ToArray(); + } + else + { + return null; + } + } + } +} diff --git a/QSB-NH/QuantumPlanet/QuantumPlanetManager.cs b/QSB-NH/QuantumPlanet/QuantumPlanetManager.cs new file mode 100644 index 000000000..540221656 --- /dev/null +++ b/QSB-NH/QuantumPlanet/QuantumPlanetManager.cs @@ -0,0 +1,13 @@ +using Cysharp.Threading.Tasks; +using QSB.WorldSync; +using QSBNH.QuantumPlanet.WorldObjects; + +namespace QSBNH.QuantumPlanet; +public class QuantumPlanetManager : WorldObjectManager +{ + public override WorldObjectScene WorldObjectScene => WorldObjectScene.Both; + public override bool DlcOnly => false; + + public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) => + QSBWorldSync.Init(); +} diff --git a/QSB-NH/QuantumPlanet/WorldObjects/QSBQuantumPlanet.cs b/QSB-NH/QuantumPlanet/WorldObjects/QSBQuantumPlanet.cs new file mode 100644 index 000000000..dbafc8cc7 --- /dev/null +++ b/QSB-NH/QuantumPlanet/WorldObjects/QSBQuantumPlanet.cs @@ -0,0 +1,7 @@ +using QSB.QuantumSync.WorldObjects; + +namespace QSBNH.QuantumPlanet.WorldObjects; + +public class QSBQuantumPlanet : QSBQuantumObject +{ +} diff --git a/QSB-NH/WarpManager.cs b/QSB-NH/WarpManager.cs new file mode 100644 index 000000000..62eee1c32 --- /dev/null +++ b/QSB-NH/WarpManager.cs @@ -0,0 +1,168 @@ +using HarmonyLib; +using NewHorizons; +using QSB.Menus; +using QSB.Messaging; +using QSB.Player; +using QSB; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Mirror; +using QSB.Patches; +using QSB.Utility; + +namespace QSBNH; +public static class WarpManager +{ + internal static bool RemoteWarp = false; + + private static void Kick(string reason) + { + DebugLog.DebugWrite(reason); + MenuManager.Instance.OnKicked(reason); + NetworkClient.Disconnect(); + } + + public static void RemoteChangeStarSystem(string system, bool ship, bool vessel, int[] hostAddonHash) + { + // Flag to not send a message + RemoteWarp = true; + + DebugLog.DebugWrite($"Remote request received to go to {system}"); + + if (!NewHorizons.Main.SystemDict.ContainsKey(system)) + { + // If you can't go to that system then you have to be disconnected + Kick($"You don't have the mod installed for {system}"); + } + else + { + var localHash = QSBNH.HashAddonsForSystem(system); + if (localHash != hostAddonHash) + { + var missingAddonHashes = hostAddonHash.Except(localHash); + var extraAddonHashes = localHash.Except(hostAddonHash); + + if (missingAddonHashes.Count() > 0) + { + Kick($"You are missing {missingAddonHashes.Count()} addon(s) that effect {system}"); + return; + } + + if (extraAddonHashes.Count() > 0) + { + var extraMods = extraAddonHashes.Select(x => QSBNH.HashToMod(x)); + + // TODO: Disable these mods for the client and do not kick them + + Kick($"You have {extraAddonHashes.Count()} extra addon(s) that effect {system}. Check the logs."); + DebugLog.DebugWrite($"You have mods affecting {system} that the host does not: {string.Join(", ", extraMods)}"); + return; + } + } + + NewHorizons.Main.Instance.ChangeCurrentStarSystem(system, ship, vessel); + } + } + + public class NHWarpMessage : QSBMessage + { + private string _starSystem; + private bool _shipWarp; + private bool _vesselWarp; + + public NHWarpMessage(string starSystem, bool shipWarp, bool vesselWarp) : base() + { + _starSystem = starSystem; + _shipWarp = shipWarp; + _vesselWarp = vesselWarp; + } + + public override void Serialize(NetworkWriter writer) + { + base.Serialize(writer); + + writer.Write(_starSystem); + writer.Write(_shipWarp); + writer.Write(_vesselWarp); + } + + public override void Deserialize(NetworkReader reader) + { + base.Deserialize(reader); + + _starSystem = reader.Read(); + _shipWarp = reader.Read(); + _vesselWarp = reader.Read(); + } + + public override void OnReceiveRemote() + { + DebugLog.DebugWrite($"Player#{From} is telling Player#{To} to warp to {_starSystem}"); + if (QSBCore.IsHost && !NewHorizons.Main.SystemDict.ContainsKey(_starSystem)) + { + // If the host doesn't have that system then we can't + DebugLog.DebugWrite($"The host doesn't have {_starSystem} installed: aborting"); + } + else + { + if (QSBCore.IsHost) + { + new NHWarpMessage(_starSystem, _shipWarp, _vesselWarp).Send(); + } + + RemoteChangeStarSystem(_starSystem, _shipWarp, _vesselWarp, QSBNH.HashAddonsForSystem(_starSystem)); + } + } + } + + [HarmonyPatch] + public class NHWarpPatch : QSBPatch + { + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + + [HarmonyPrefix] + [HarmonyPatch(typeof(NewHorizons.Main), nameof(NewHorizons.Main.ChangeCurrentStarSystem))] + public static bool NewHorizons_ChangeCurrentStarSystem(string newStarSystem, bool warp, bool vessel) + { + if (RemoteWarp) + { + // We're being told to warp so just do it + RemoteWarp = false; + return true; + } + + DebugLog.DebugWrite($"Local request received to go to {newStarSystem}"); + if (QSBCore.IsHost) + { + // The host will tell all other users to warp + DebugLog.DebugWrite($"Host: Telling others to go to {newStarSystem}"); + new NHWarpMessage(newStarSystem, warp, vessel).Send(); + // The host can now warp + return true; + } + else + { + // We're a client that has to tell the host to start warping people + DebugLog.DebugWrite($"Client: Telling host to send us to {newStarSystem}"); + new NHWarpMessage(newStarSystem, warp, vessel) { To = 0 }.Send(); + + // We have to wait for the host to get back to us + return false; + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(NewHorizons.Main), nameof(NewHorizons.Main.ChangeCurrentStarSystem))] + public static void NewHorizons_ChangeCurrentStarSystem(NewHorizons.Main __instance) + { + if (__instance.IsWarpingFromShip) + { + // If QSB doesn't say we're piloting the ship then dont keep them on as the one warping + __instance.GetType().GetProperty(nameof(NewHorizons.Main.IsWarpingFromShip)).SetValue(__instance, QSBPlayerManager.LocalPlayer.FlyingShip); + } + } + } +} diff --git a/QSB-NH/lib/NewHorizons.dll b/QSB-NH/lib/NewHorizons.dll new file mode 100644 index 000000000..83f0afc8a Binary files /dev/null and b/QSB-NH/lib/NewHorizons.dll differ diff --git a/QSB.sln b/QSB.sln index 8bc9e6c9c..4b1d685bc 100644 --- a/QSB.sln +++ b/QSB.sln @@ -34,6 +34,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "APITestMod", "APITestMod\AP EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QSBPatcher", "QSBPatcher\QSBPatcher.csproj", "{CA4CBA2B-54D5-4C4B-9B51-957BC6D77D6B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QSB-NH", "QSB-NH\QSB-NH.csproj", "{74F84A39-1C9D-4EF7-889A-485D33B7B324}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -64,6 +66,10 @@ Global {CA4CBA2B-54D5-4C4B-9B51-957BC6D77D6B}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA4CBA2B-54D5-4C4B-9B51-957BC6D77D6B}.Release|Any CPU.ActiveCfg = Release|Any CPU {CA4CBA2B-54D5-4C4B-9B51-957BC6D77D6B}.Release|Any CPU.Build.0 = Release|Any CPU + {74F84A39-1C9D-4EF7-889A-485D33B7B324}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74F84A39-1C9D-4EF7-889A-485D33B7B324}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74F84A39-1C9D-4EF7-889A-485D33B7B324}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74F84A39-1C9D-4EF7-889A-485D33B7B324}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/QSB/ConversationSync/ConversationManager.cs b/QSB/ConversationSync/ConversationManager.cs index f394e6f23..219008e32 100644 --- a/QSB/ConversationSync/ConversationManager.cs +++ b/QSB/ConversationSync/ConversationManager.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using QSB.Utility.Deterministic; using UnityEngine; using UnityEngine.UI; @@ -41,8 +42,9 @@ public void Start() public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) { - QSBWorldSync.Init(); - QSBWorldSync.Init(); + // dont create worldobjects + QSBWorldSync.Init(QSBWorldSync.GetUnityObjects().Where(x => x.name != "WarpDriveRemoteTrigger").SortDeterministic()); + QSBWorldSync.Init(QSBWorldSync.GetUnityObjects().Where(x => x.name != "WarpDriveDialogue").SortDeterministic()); } public uint GetPlayerTalkingToTree(CharacterDialogueTree tree) => diff --git a/QSB/MeteorSync/MeteorManager.cs b/QSB/MeteorSync/MeteorManager.cs index b93704aba..29c841f86 100644 --- a/QSB/MeteorSync/MeteorManager.cs +++ b/QSB/MeteorSync/MeteorManager.cs @@ -1,6 +1,7 @@ using Cysharp.Threading.Tasks; using QSB.MeteorSync.WorldObjects; using QSB.WorldSync; +using System.Linq; using System.Threading; namespace QSB.MeteorSync; @@ -16,7 +17,9 @@ public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken // 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/QSBCore.cs b/QSB/QSBCore.cs index e033123f8..f53be3eb4 100644 --- a/QSB/QSBCore.cs +++ b/QSB/QSBCore.cs @@ -84,12 +84,12 @@ public class QSBCore : ModBehaviour private static string randomSkinType; private static string randomJetpackType; + public static Assembly QSBNHAssembly = null; public static readonly string[] IncompatibleMods = { // incompatible mods "Raicuparta.NomaiVR", - "xen.NewHorizons", "Vesper.AutoResume", "Vesper.OuterWildsMMO", "_nebula.StopTime", @@ -227,7 +227,7 @@ public void Start() Helper = ModHelper; DebugLog.ToConsole($"* Start of QSB version {QSBVersion} - authored by {Helper.Manifest.Author}", MessageType.Info); - CheckCompatibilityMods(); + CheckNewHorizons(); DebugSettings = Helper.Storage.Load("debugsettings.json") ?? new DebugSettings(); @@ -463,23 +463,13 @@ private void Update() } } - private void CheckCompatibilityMods() + private void CheckNewHorizons() { - 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) + if (ModHelper.Interaction.ModExists("xen.NewHorizons")) { - 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); + // NH compat has to be in a different DLL since it uses IAddComponentOnStart, and depends on the NH DLL. + QSBNHAssembly = Assembly.LoadFrom(Path.Combine(ModHelper.Manifest.ModFolderPath, "QSB-NH.dll")); + gameObject.AddComponent(QSBNHAssembly.GetType("QSBNH.QSBNH", true)); } } } diff --git a/QSB/Utility/DebugGUI.cs b/QSB/Utility/DebugGUI.cs index 4d2bd3a3a..4fc4b60c0 100644 --- a/QSB/Utility/DebugGUI.cs +++ b/QSB/Utility/DebugGUI.cs @@ -205,6 +205,8 @@ private static void DrawGui() WriteLine(2, $" - Ref. Sector : {(referenceSector == null ? "NULL" : referenceSector.Name)}", referenceSector == null ? Color.red : Color.white); WriteLine(2, $" - Ref. Transform : {(referenceTransform == null ? "NULL" : referenceTransform.name)}", referenceTransform == null ? Color.red : Color.white); + WriteLine(2, $" - Local Position : {player.Body.transform.localPosition}"); + WriteLine(2, $" - Position : {player.Body.transform.position}"); } } diff --git a/QSB/Utility/Extensions.cs b/QSB/Utility/Extensions.cs index 188abab73..e9cc4ba5e 100644 --- a/QSB/Utility/Extensions.cs +++ b/QSB/Utility/Extensions.cs @@ -182,13 +182,21 @@ public static void RaiseEvent(this T instance, string eventName, params objec multiDelegate.SafeInvoke(args); } - public static IEnumerable GetDerivedTypes(this Type type) => - QSBCore.Addons.Values + public static IEnumerable GetDerivedTypes(this Type type) + { + var assemblies = QSBCore.Addons.Values .Select(x => x.GetType().Assembly) - .Append(type.Assembly) - .SelectMany(x => x.GetTypes()) + .Append(type.Assembly); + + if (QSBCore.QSBNHAssembly != null) + { + assemblies = assemblies.Append(QSBCore.QSBNHAssembly); + } + + return assemblies.SelectMany(x => x.GetTypes()) .Where(x => !x.IsInterface && !x.IsAbstract && type.IsAssignableFrom(x)) .OrderBy(x => x.FullName); + } public static Guid ToGuid(this int value) {