diff --git a/Nitrox.Test/Patcher/Patches/Dynamic/SpawnConsoleCommand_OnConsoleCommand_PatchTest.cs b/Nitrox.Test/Patcher/Patches/Dynamic/SpawnConsoleCommand_OnConsoleCommand_PatchTest.cs new file mode 100644 index 0000000000..014b1b9750 --- /dev/null +++ b/Nitrox.Test/Patcher/Patches/Dynamic/SpawnConsoleCommand_OnConsoleCommand_PatchTest.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using HarmonyLib; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NitroxTest.Patcher; + +namespace NitroxPatcher.Patches.Dynamic; + +[TestClass] +public class SpawnConsoleCommand_OnConsoleCommand_PatchTest +{ + [TestMethod] + public void Sanity() + { + IEnumerable originalIl = PatchTestHelper.GetInstructionsFromMethod(SpawnConsoleCommand_OnConsoleCommand_Patch.TARGET_METHOD); + IEnumerable transformedIl = SpawnConsoleCommand_OnConsoleCommand_Patch.Transpiler(originalIl); + transformedIl.Count().Should().Be(originalIl.Count() + 2); + } +} diff --git a/Nitrox.Test/Patcher/Patches/Dynamic/SubConsoleCommand_OnConsoleCommand_sub_PatchTest.cs b/Nitrox.Test/Patcher/Patches/Dynamic/SubConsoleCommand_OnConsoleCommand_sub_PatchTest.cs new file mode 100644 index 0000000000..c2b8ddb679 --- /dev/null +++ b/Nitrox.Test/Patcher/Patches/Dynamic/SubConsoleCommand_OnConsoleCommand_sub_PatchTest.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using HarmonyLib; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NitroxTest.Patcher; + +namespace NitroxPatcher.Patches.Dynamic; + +[TestClass] +public class SubConsoleCommand_OnConsoleCommand_sub_PatchTest +{ + [TestMethod] + public void Sanity() + { + IEnumerable originalIl = PatchTestHelper.GetInstructionsFromMethod(SubConsoleCommand_OnConsoleCommand_sub_Patch.TARGET_METHOD); + IEnumerable transformedIl = SubConsoleCommand_OnConsoleCommand_sub_Patch.Transpiler(originalIl); + transformedIl.Count().Should().Be(originalIl.Count()); + } +} diff --git a/NitroxClient/GameLogic/MobileVehicleBay.cs b/NitroxClient/GameLogic/MobileVehicleBay.cs index 16c6456ce7..b48f5a61a1 100644 --- a/NitroxClient/GameLogic/MobileVehicleBay.cs +++ b/NitroxClient/GameLogic/MobileVehicleBay.cs @@ -1,10 +1,8 @@ using NitroxClient.Communication.Abstract; -using NitroxClient.GameLogic.Helper; using NitroxClient.MonoBehaviours; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.Packets; -using NitroxModel_Subnautica.DataStructures; using UnityEngine; namespace NitroxClient.GameLogic; @@ -42,8 +40,7 @@ public void BeginCrafting(ConstructorInput constructor, GameObject constructedOb NitroxId constructedObjectId = NitroxEntity.GenerateNewId(constructedObject); - VehicleWorldEntity vehicleEntity = new(constructorId, DayNightCycle.main.timePassedAsFloat, constructedObject.transform.ToLocalDto(), string.Empty, false, constructedObjectId, techType.ToDto(), null); - VehicleChildEntityHelper.PopulateChildren(constructedObjectId, constructedObject.GetFullHierarchyPath(), vehicleEntity.ChildEntities, constructedObject); + VehicleWorldEntity vehicleEntity = Vehicles.BuildVehicleWorldEntity(constructedObject, constructedObjectId, techType, constructorId); packetSender.Send(new EntitySpawnedByClient(vehicleEntity)); diff --git a/NitroxClient/GameLogic/NitroxConsole.cs b/NitroxClient/GameLogic/NitroxConsole.cs index 903a14938a..ed313db3c9 100644 --- a/NitroxClient/GameLogic/NitroxConsole.cs +++ b/NitroxClient/GameLogic/NitroxConsole.cs @@ -1,11 +1,9 @@ using System; using NitroxClient.Communication.Abstract; -using NitroxClient.GameLogic.Helper; using NitroxClient.MonoBehaviours; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.Packets; -using NitroxModel_Subnautica.DataStructures; using NitroxModel_Subnautica.Helper; using UnityEngine; @@ -16,29 +14,28 @@ public class NitroxConsole public static bool DisableConsole { get; set; } = true; private readonly IPacketSender packetSender; - private readonly Items item; + private readonly Items items; - public NitroxConsole(IPacketSender packetSender, Items item) + public NitroxConsole(IPacketSender packetSender, Items items) { this.packetSender = packetSender; - this.item = item; + this.items = items; } //List of things that can be spawned : https://subnauticacommands.com/items public void Spawn(GameObject gameObject) { - TechType techType = CraftData.GetTechType(gameObject); + TechType techType = GetObjectTechType(gameObject); try { if (VehicleHelper.IsVehicle(techType)) { - SpawnVehicle(gameObject); + SpawnVehicle(gameObject, techType); } else { - SpawnItem(gameObject); - //TODO: Add support for no AI creature that need to be spawned as well + DefaultSpawn(gameObject); } } catch (Exception ex) @@ -48,32 +45,39 @@ public void Spawn(GameObject gameObject) } /// - /// Spawns a Seamoth or an Exosuit + /// Spawns Seamoth, Exosuit or Cyclops /// - private void SpawnVehicle(GameObject gameObject) + private void SpawnVehicle(GameObject gameObject, TechType techType) { - TechType techType = CraftData.GetTechType(gameObject); - NitroxId id = NitroxEntity.GetIdOrGenerateNew(gameObject); - VehicleWorldEntity vehicleEntity = new VehicleWorldEntity(null, DayNightCycle.main.timePassedAsFloat, gameObject.transform.ToLocalDto(), "", false, id, techType.ToDto(), null); - VehicleChildEntityHelper.PopulateChildren(id, gameObject.GetFullHierarchyPath(), vehicleEntity.ChildEntities, gameObject); - + VehicleWorldEntity vehicleEntity = Vehicles.BuildVehicleWorldEntity(gameObject, id, techType); + packetSender.Send(new EntitySpawnedByClient(vehicleEntity)); - Log.Debug($"Spawning vehicle {techType} with id {techType} at {gameObject.transform.position}"); + Log.Debug($"Spawning vehicle {techType} with id {id} at {gameObject.transform.position}"); } - /// - /// Spawns a Pickupable item - /// - private void SpawnItem(GameObject gameObject) + private void DefaultSpawn(GameObject gameObject) + { + items.Dropped(gameObject); + } + + private static TechType GetObjectTechType(GameObject gameObject) { - if (gameObject.TryGetComponent(out Pickupable pickupable)) + TechType techType = CraftData.GetTechType(gameObject); + if (techType != TechType.None) { - Log.Debug($"Spawning item {pickupable.GetTechName()} at {gameObject.transform.position}"); - item.Dropped(gameObject, pickupable.GetTechType()); + return techType; } + + // Cyclops' GameObject doesn't have a way to give its a TechType so we detect it differently + if (gameObject.TryGetComponent(out SubRoot subRoot) && subRoot.isCyclops) + { + return TechType.Cyclops; + } + + return TechType.None; } } } diff --git a/NitroxClient/GameLogic/Vehicles.cs b/NitroxClient/GameLogic/Vehicles.cs index ea0fd0d530..3132b8b7b3 100644 --- a/NitroxClient/GameLogic/Vehicles.cs +++ b/NitroxClient/GameLogic/Vehicles.cs @@ -1,14 +1,16 @@ using System.Collections; using NitroxClient.Communication; using NitroxClient.Communication.Abstract; +using NitroxClient.GameLogic.Helper; using NitroxClient.MonoBehaviours; using NitroxClient.Unity.Helper; -using NitroxModel_Subnautica.DataStructures; -using NitroxModel_Subnautica.DataStructures.GameLogic; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; +using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.DataStructures.Util; using NitroxModel.Packets; +using NitroxModel_Subnautica.DataStructures; +using NitroxModel_Subnautica.DataStructures.GameLogic; using UnityEngine; namespace NitroxClient.GameLogic; @@ -232,4 +234,11 @@ public static void RemoveNitroxEntitiesTagging(GameObject constructedObject) UnityEngine.Object.DestroyImmediate(nitroxEntity); } } + + public static VehicleWorldEntity BuildVehicleWorldEntity(GameObject constructedObject, NitroxId constructedObjectId, TechType techType, NitroxId constructorId = null) + { + VehicleWorldEntity vehicleEntity = new(constructorId, DayNightCycle.main.timePassedAsFloat, constructedObject.transform.ToLocalDto(), string.Empty, false, constructedObjectId, techType.ToDto(), null); + VehicleChildEntityHelper.PopulateChildren(constructedObjectId, constructedObject.GetFullHierarchyPath(), vehicleEntity.ChildEntities, constructedObject); + return vehicleEntity; + } } diff --git a/NitroxPatcher/Patches/Dynamic/SpawnConsoleCommand_OnConsoleCommand_Patch.cs b/NitroxPatcher/Patches/Dynamic/SpawnConsoleCommand_OnConsoleCommand_Patch.cs index 4bfffc018e..4381696cc4 100644 --- a/NitroxPatcher/Patches/Dynamic/SpawnConsoleCommand_OnConsoleCommand_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/SpawnConsoleCommand_OnConsoleCommand_Patch.cs @@ -1,47 +1,24 @@ -using System; -using System.Collections.Generic; using System.Reflection; -using System.Reflection.Emit; -using HarmonyLib; using NitroxClient.GameLogic; +using NitroxModel.DataStructures.GameLogic; using NitroxModel.Helper; -using UnityEngine; namespace NitroxPatcher.Patches.Dynamic; +/// +/// Prevents local player from using "spawn" command without at least the permissions. +/// public sealed partial class SpawnConsoleCommand_OnConsoleCommand_Patch : NitroxPatch, IDynamicPatch { - private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SpawnConsoleCommand t) => t.OnConsoleCommand_spawn(default(NotificationCenter.Notification))); + internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((SpawnConsoleCommand t) => t.OnConsoleCommand_spawn(default)); - private static readonly OpCode INJECTION_CODE = OpCodes.Call; - private static readonly object INJECTION_OPERAND = Reflect.Method(() => Utils.CreatePrefab(default(GameObject), default(float), default(bool))); - - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) + public static bool Prefix(NotificationCenter.Notification n) { - Validate.NotNull(INJECTION_OPERAND); - - foreach (CodeInstruction instruction in instructions) + if (Resolve().Permissions < Perms.MODERATOR) { - yield return instruction; - - /* - * GameObject gameObject = global::Utils.CreatePrefab(prefabForTechType, maxDist, i > 0); - * -> SpawnConsoleCommand_OnConsoleCommand_Patch.Callback(gameObject); - * LargeWorldEntity.Register(gameObject); - * CrafterLogic.NotifyCraftEnd(gameObject, techType); - * gameObject.SendMessage("StartConstruction", SendMessageOptions.DontRequireReceiver); - */ - if (instruction.opcode == INJECTION_CODE && instruction.operand.Equals(INJECTION_OPERAND)) - { - yield return new CodeInstruction(OpCodes.Dup); - yield return new CodeInstruction(OpCodes.Call, ((Action)Callback).Method); - } - + Log.InGame(Language.main.Get("Nitrox_MissingPermission").Replace("{PERMISSION}", Perms.MODERATOR.ToString())); + return false; } - } - - public static void Callback(GameObject gameObject) - { - Resolve().Spawn(gameObject); + return true; } } diff --git a/NitroxPatcher/Patches/Dynamic/SpawnConsoleCommand_SpawnAsync_Patch.cs b/NitroxPatcher/Patches/Dynamic/SpawnConsoleCommand_SpawnAsync_Patch.cs new file mode 100644 index 0000000000..750de7a5be --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/SpawnConsoleCommand_SpawnAsync_Patch.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using NitroxClient.GameLogic; +using NitroxModel.Helper; +using UnityEngine; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Syncs "spawn" command. +/// +public sealed partial class SpawnConsoleCommand_SpawnAsync_Patch : NitroxPatch, IDynamicPatch +{ + internal static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((SpawnConsoleCommand t) => t.SpawnAsync(default))); + + /* + * MODIFIED: + * GameObject gameObject = global::Utils.CreatePrefab(prefabForTechType, maxDist, i > 0); + * SpawnConsoleCommand_OnConsoleCommand_Patch.WrappedCallback(gameObject); <---- INSERTED LINE + * LargeWorldEntity.Register(gameObject); + * CrafterLogic.NotifyCraftEnd(gameObject, techType); + * gameObject.SendMessage("StartConstruction", SendMessageOptions.DontRequireReceiver); + */ + public static IEnumerable Transpiler(IEnumerable instructions) + { + return new CodeMatcher(instructions).MatchStartForward([ + new CodeMatch(OpCodes.Call, Reflect.Method(() => Utils.CreatePrefab(default, default, default))) + ]) + .Advance(1) + .Insert([ + new CodeInstruction(OpCodes.Dup), + new CodeInstruction(OpCodes.Call, Reflect.Method(() => Callback(default))) + ]) + .InstructionEnumeration(); + } + + public static void Callback(GameObject gameObject) + { + Resolve().Spawn(gameObject); + } +} diff --git a/NitroxPatcher/Patches/Dynamic/SubConsoleCommand_OnConsoleCommand_sub_Patch.cs b/NitroxPatcher/Patches/Dynamic/SubConsoleCommand_OnConsoleCommand_sub_Patch.cs index d0d6c13dbf..8c7714329b 100644 --- a/NitroxPatcher/Patches/Dynamic/SubConsoleCommand_OnConsoleCommand_sub_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/SubConsoleCommand_OnConsoleCommand_sub_Patch.cs @@ -1,19 +1,63 @@ +using System.Collections.Generic; using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using NitroxClient.GameLogic; +using NitroxModel.DataStructures.GameLogic; using NitroxModel.Helper; +using UnityEngine; namespace NitroxPatcher.Patches.Dynamic; /// -/// Called whenever a Cyclops or Seamoth is spawned. Nitrox already has its own command to spawn vehicles. -/// This patch is only meant to block the method from executing, causing two vehicles to be spawned instead of one +/// Prevents local player from using "sub" command without at least the permissions. +/// Once they have the permissions, sync this command. /// public sealed partial class SubConsoleCommand_OnConsoleCommand_sub_Patch : NitroxPatch, IDynamicPatch { - private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SubConsoleCommand t) => t.OnConsoleCommand_sub(default)); + internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((SubConsoleCommand t) => t.OnConsoleCommand_sub(default)); - public static bool Prefix() + public static bool Prefix(NotificationCenter.Notification n) { - Log.InGame(Language.main.Get("Nitrox_CommandNotAvailable")); - return false; + if (Resolve().Permissions < Perms.MODERATOR) + { + Log.InGame(Language.main.Get("Nitrox_MissingPermission").Replace("{PERMISSION}", Perms.MODERATOR.ToString())); + return false; + } + + string text = (string)n.data[0]; + if (!string.IsNullOrEmpty(text) && !text.ToLowerInvariant().Equals("cyclops")) + { + Log.InGame(Language.main.Get("Nitrox_CommandNotAvailable")); + return false; + } + return true; + } + + /* + * REPLACE: + * LightmappedPrefabs.main.RequestScenePrefab(text, new LightmappedPrefabs.OnPrefabLoaded(this.OnSubPrefabLoaded)); + * BY: + * LightmappedPrefabs.main.RequestScenePrefab(text, new LightmappedPrefabs.OnPrefabLoaded(SubConsoleCommand_OnConsoleCommand_sub_Patch.WrappedCallback)); + */ + public static IEnumerable Transpiler(IEnumerable instructions) + { + return new CodeMatcher(instructions).MatchEndForward([ + new CodeMatch(OpCodes.Stfld), + new CodeMatch(OpCodes.Ldsfld), + new CodeMatch(OpCodes.Ldloc_0), + new CodeMatch(OpCodes.Ldarg_0) + ]) + .SetOpcodeAndAdvance(OpCodes.Ldnull) + .SetOperandAndAdvance(Reflect.Method(() => WrappedCallback(default))) + .InstructionEnumeration(); + } + + public static void WrappedCallback(GameObject prefab) + { + SubConsoleCommand instance = SubConsoleCommand.main; + // Call the original callback and then get the object it created to broadcast its creation + instance.OnSubPrefabLoaded(prefab); + Resolve().Spawn(instance.lastCreatedSub); } }