From 1e8b7ae14931336895513e04b74f3f7a9c028691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=20E=20L=20=CE=94?= <36549174+VELD-Dev@users.noreply.github.com> Date: Mon, 10 Apr 2023 23:16:59 +0200 Subject: [PATCH 1/5] SpawnOnKill and BreakableResource replication added (e.g. Coral Disks or Limestones) (#2016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added SpawnOnKill behaviour Added SpawnOnKill_Patch.cs to handle SpawnOnKill packets, still in work --- It syncs the destroy but not the spawning yet / I plan to copy the code from the original game, but if someone has the bravour to make a transpiler... then go on. * Updated SpawnOnKill and renamed the file to match the naming convetion. It is done ! I finished my first task ! I'm really proud 😊 * Updated SpawnOnKill_OnKill_Patch.cs to match requested changes * Patched the game object not updating when entity is "killed". It was dropping the original entity, so even if physic was now simulated, the other player(s) still had to knife it or kill it on their side. Now it's working perfectly. * Create BreakableResource_BreakIntoResources_Patch.cs Created a patch to delete the rock when a breakableresource is broken. However, I still need to make a transpiler for function SpawnResourceFromPrefab() which is a coroutine. Wish me good luck ;-; * Updated SpawnOnKill from a Prefix to a Transpiler. It's optimized 👌 * Removed the prefix code * renamed Callback argument "__this" to "spawnOnKill". Updated refractors. * Started working on SpawnResourceFromPrefab patch. * Slight refactor of EntityDestroyedProcessor.Process to not always call expensive methods * Added docs to Items.cs APIs Changed Dropped() to use UWE API to get TechType from passed in GameObject if not provided. * Cleaned up SpawnOnKill_OnKill_Patch.cs Added to pattern match on Object.Instantiate from UnityEngine to better detect code updates that break this patch (trying to prevent silent errors). Removed some comments that added too much information and makes code harder to understand at face-value. * Added unit test for SpawnOnKill_OnKill_Patch.cs * Updated SpawnResourceFromPrefab method [NOT DONE!] * Merged BreakableResource to SpawnOnKill branches and finished BreakableResource_SpawnResourceFromPrefab_Patch, testing needed * patched syntax error * added class comments * optimized SpawnResourceFromPrefab to match latest changes on .Dropped() function * Fixed SpawnResourceFromPrefab. TEST NEEDED. After this commit, please review the PR and tell me if there's anything else needed for the 1.8 * Added weak matching on call-like instructions when using Reflect API in pattern matching * Removed dead code in EntityDestroyedProcessor * Added logging when EntityDestroyedProcessor failed to find the gameobject * corrected partially requested changes. Added a test class, used the new reflect.method for operand checks, deleted useless usings... * corrected test class (offset was 3 instead of 2) * Added a security to BreakIntoResources * I failed my save, now it's secured* * removed comment because I understood * Disabled weak match on call opcode in IL pattern when using InstructionPattern.Call directly * Reverted change in Items.Dropped() API to not stop on TechType.None * edited spawnOnKill patch and added a security like in BreakIntoResources patch. Ready for merge btw * corrected what @sunrunner told me to change * corrected syntax error, undefined reference and else clause. * Corrected naming and formatting. Ready for merging ! --------- Co-authored-by: Measurity --- ...ource_SpawnResourceFromPrefab_PatchTest.cs | 22 +++++++ .../Dynamic/SpawnOnKill_OnKill_PatchTest.cs | 22 +++++++ .../Processors/EntityDestroyedProcessor.cs | 44 ++++++------- NitroxClient/GameLogic/Items.cs | 34 +++++----- NitroxLauncher/NitroxLauncher.csproj | 3 + ...akableResource_BreakIntoResources_Patch.cs | 36 ++++++++++ ...eResource_SpawnResourceFromPrefab_Patch.cs | 52 +++++++++++++++ .../Dynamic/SpawnOnKill_OnKill_Patch.cs | 65 +++++++++++++++++++ .../PatternMatching/InstructionPattern.cs | 16 ++++- .../PatternMatching/OpCodePattern.cs | 36 +++++----- 10 files changed, 268 insertions(+), 62 deletions(-) create mode 100644 Nitrox.Test/Patcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_PatchTest.cs create mode 100644 Nitrox.Test/Patcher/Patches/Dynamic/SpawnOnKill_OnKill_PatchTest.cs create mode 100644 NitroxPatcher/Patches/Dynamic/BreakableResource_BreakIntoResources_Patch.cs create mode 100644 NitroxPatcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_Patch.cs create mode 100644 NitroxPatcher/Patches/Dynamic/SpawnOnKill_OnKill_Patch.cs diff --git a/Nitrox.Test/Patcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_PatchTest.cs b/Nitrox.Test/Patcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_PatchTest.cs new file mode 100644 index 0000000000..ecf20435a5 --- /dev/null +++ b/Nitrox.Test/Patcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_PatchTest.cs @@ -0,0 +1,22 @@ +using FluentAssertions; +using HarmonyLib; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NitroxTest.Patcher; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using static NitroxPatcher.Patches.Dynamic.BreakableResource_SpawnResourceFromPrefab_Patch; + +namespace NitroxPatcher.Patches.Dynamic; + +[TestClass] +public class BreakableResource_SpawnResourceFromPrefab_PatchTest +{ + [TestMethod] + public void Sanity() + { + ReadOnlyCollection originalIL = PatchTestHelper.GetInstructionsFromMethod(TARGET_METHOD); + IEnumerable transformedIL = Transpiler(TARGET_METHOD, originalIL); + originalIL.Count.Should().Be(transformedIL.Count() - 2); + } +} diff --git a/Nitrox.Test/Patcher/Patches/Dynamic/SpawnOnKill_OnKill_PatchTest.cs b/Nitrox.Test/Patcher/Patches/Dynamic/SpawnOnKill_OnKill_PatchTest.cs new file mode 100644 index 0000000000..c672c2aaee --- /dev/null +++ b/Nitrox.Test/Patcher/Patches/Dynamic/SpawnOnKill_OnKill_PatchTest.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using FluentAssertions; +using HarmonyLib; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NitroxTest.Patcher; +using static NitroxPatcher.Patches.Dynamic.SpawnOnKill_OnKill_Patch; + +namespace NitroxPatcher.Patches.Dynamic; + +[TestClass] +public class SpawnOnKill_OnKill_PatchTest +{ + [TestMethod] + public void Sanity() + { + ReadOnlyCollection originalIl = PatchTestHelper.GetInstructionsFromMethod(TARGET_METHOD); + IEnumerable transformedIl = Transpiler(TARGET_METHOD, originalIl); + originalIl.Count.Should().Be(transformedIl.Count() - 3); + } +} diff --git a/NitroxClient/Communication/Packets/Processors/EntityDestroyedProcessor.cs b/NitroxClient/Communication/Packets/Processors/EntityDestroyedProcessor.cs index 244aefae18..a31bbbe932 100644 --- a/NitroxClient/Communication/Packets/Processors/EntityDestroyedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/EntityDestroyedProcessor.cs @@ -1,4 +1,3 @@ -using NitroxClient.Communication.Abstract; using NitroxClient.Communication.Packets.Processors.Abstract; using NitroxClient.GameLogic; using NitroxClient.GameLogic.PlayerLogic; @@ -13,38 +12,35 @@ public class EntityDestroyedProcessor : ClientPacketProcessor public const DamageType DAMAGE_TYPE_RUN_ORIGINAL = (DamageType)100; private readonly Entities entities; - private readonly IPacketSender packetSender; - public EntityDestroyedProcessor(Entities entities, IPacketSender packetSender) + public EntityDestroyedProcessor(Entities entities) { this.entities = entities; - this.packetSender = packetSender; } public override void Process(EntityDestroyed packet) { entities.RemoveEntity(packet.Id); + if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject gameObject)) + { + Log.Warn($"[{nameof(EntityDestroyedProcessor)}] Could not find entity with id: {packet.Id} to destroy."); + return; + } using (PacketSuppressor.Suppress()) { - if (NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject gameObject)) + // This type of check could get out of control if there are many types with custom destroy logic. If we get a few more, move to separate processors. + if (gameObject.TryGetComponent(out Vehicle vehicle)) { - // This type of check could get out of control if there are many types with custom destroy logic. If we get a few more, move to separate processors. - Vehicle vehicle = gameObject.GetComponent(); - SubRoot subRoot = gameObject.GetComponent(); - - if (vehicle) - { - DestroyVehicle(vehicle); - } - else if(subRoot) - { - DestroySubroot(subRoot); - } - else - { - DefaultDestroyAction(gameObject); - } + DestroyVehicle(vehicle); + } + else if (gameObject.TryGetComponent(out SubRoot subRoot)) + { + DestroySubroot(subRoot); + } + else + { + DefaultDestroyAction(gameObject); } } } @@ -85,17 +81,17 @@ private void DestroyVehicle(Vehicle vehicle) { if (vehicle.destructionEffect) { - GameObject gameObject = UnityEngine.Object.Instantiate(vehicle.destructionEffect); + GameObject gameObject = Object.Instantiate(vehicle.destructionEffect); gameObject.transform.position = vehicle.transform.position; gameObject.transform.rotation = vehicle.transform.rotation; } - UnityEngine.Object.Destroy(vehicle.gameObject); + Object.Destroy(vehicle.gameObject); } } private void DefaultDestroyAction(GameObject gameObject) { - UnityEngine.Object.Destroy(gameObject); + Object.Destroy(gameObject); } } diff --git a/NitroxClient/GameLogic/Items.cs b/NitroxClient/GameLogic/Items.cs index f03a000588..b61079e17a 100644 --- a/NitroxClient/GameLogic/Items.cs +++ b/NitroxClient/GameLogic/Items.cs @@ -59,25 +59,28 @@ public void PickedUp(GameObject gameObject, TechType techType) packetSender.Send(pickupItem); } - public void Dropped(GameObject gameObject, TechType techType) + /// + /// Tracks the object (as dropped) and notifies the server to spawn the item for other players. + /// + public void Dropped(GameObject gameObject, TechType? techType = null) { + techType ??= CraftData.GetTechType(gameObject); + // there is a theoretical possibility of a stray remote tracking packet that re-adds the monobehavior, this is purely a safety call. RemoveAnyRemoteControl(gameObject); Optional waterparkId = GetCurrentWaterParkId(); NitroxId id = NitroxEntity.GetId(gameObject); Optional metadata = EntityMetadataExtractor.Extract(gameObject); - - bool inGlobalRoot = map.GlobalRootTechTypes.Contains(techType.ToDto()); + bool inGlobalRoot = map.GlobalRootTechTypes.Contains(techType.Value.ToDto()); string classId = gameObject.GetComponent().ClassId; - - WorldEntity droppedItem = new WorldEntity(gameObject.transform.ToWorldDto(), 0, classId, inGlobalRoot, waterparkId.OrNull(), false, id, techType.ToDto(), metadata.OrNull(), null, new List()); - droppedItem.ChildEntities = GetPrefabChildren(gameObject, id).ToList(); + WorldEntity droppedItem = new(gameObject.transform.ToWorldDto(), 0, classId, inGlobalRoot, waterparkId.OrNull(), false, id, techType.Value.ToDto(), metadata.OrNull(), null, new List()) + { + ChildEntities = GetPrefabChildren(gameObject, id).ToList() + }; Log.Debug($"Dropping item: {droppedItem}"); - - EntitySpawnedByClient spawnedPacket = new EntitySpawnedByClient(droppedItem); - packetSender.Send(spawnedPacket); + packetSender.Send(new EntitySpawnedByClient(droppedItem)); } public void Created(GameObject gameObject) @@ -91,9 +94,9 @@ public void Created(GameObject gameObject) } } - // This function will record any notable children of the dropped item as a PrefabChildEntity. In this case, a 'notable' + // This function will record any notable children of the dropped item as a PrefabChildEntity. In this case, a 'notable' // child is one that UWE has tagged with a PrefabIdentifier (class id) and has entity metadata that can be extracted. An - // example would be recording a Battery PrefabChild inside of a Flashlight WorldEntity. + // example would be recording a Battery PrefabChild inside of a Flashlight WorldEntity. public static IEnumerable GetPrefabChildren(GameObject gameObject, NitroxId parentId) { foreach (IGrouping prefabGroup in gameObject.GetAllComponentsInChildren() @@ -137,12 +140,13 @@ private InventoryItemEntity ConvertToInventoryItemEntity(GameObject gameObject) return inventoryItemEntity; } + /// + /// Some items might be remotely simulated if they were dropped by other players. We'll want to remove + /// any remote tracking when we actively handle the item. + /// private void RemoveAnyRemoteControl(GameObject gameObject) { - // Some items might be remotely simulated if they were dropped by other players. We'll want to remove - // any remote tracking when we actively handle the item. - RemotelyControlled remotelyControlled = gameObject.GetComponent(); - Object.Destroy(remotelyControlled); + Object.Destroy(gameObject.GetComponent()); } private Optional GetCurrentWaterParkId() diff --git a/NitroxLauncher/NitroxLauncher.csproj b/NitroxLauncher/NitroxLauncher.csproj index 7e20193ec4..c9358bbb6c 100644 --- a/NitroxLauncher/NitroxLauncher.csproj +++ b/NitroxLauncher/NitroxLauncher.csproj @@ -9,6 +9,9 @@ true icon.ico True + Nitrox + Nitrox + https://github.com/SubnauticaNitrox/Nitrox diff --git a/NitroxPatcher/Patches/Dynamic/BreakableResource_BreakIntoResources_Patch.cs b/NitroxPatcher/Patches/Dynamic/BreakableResource_BreakIntoResources_Patch.cs new file mode 100644 index 0000000000..9a05e6ec1c --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/BreakableResource_BreakIntoResources_Patch.cs @@ -0,0 +1,36 @@ +using HarmonyLib; +using NitroxClient.Communication.Abstract; +using NitroxClient.MonoBehaviours; +using NitroxModel.DataStructures; +using NitroxModel.Helper; +using NitroxModel.Packets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.NetworkInformation; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace NitroxPatcher.Patches.Dynamic; + +public class BreakableResource_BreakIntoResources_Patch : NitroxPatch, IDynamicPatch +{ + private static MethodInfo TARGET_METHOD = Reflect.Method((BreakableResource t) => t.BreakIntoResources()); + + public static void Prefix(BreakableResource __instance) + { + if (!NitroxEntity.TryGetEntityFrom(__instance.gameObject, out NitroxEntity destroyedEntity)) + { + Log.Warn($"[{nameof(BreakableResource_BreakIntoResources_Patch)}] Could not find {nameof(NitroxEntity)} for breakable entity {__instance.gameObject.GetFullHierarchyPath()}."); + return; + } + // Send packet to destroy the entity + Resolve().Send(new EntityDestroyed(destroyedEntity.Id)); + } + + public override void Patch(Harmony harmony) + { + PatchPrefix(harmony, TARGET_METHOD); + } +} diff --git a/NitroxPatcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_Patch.cs b/NitroxPatcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_Patch.cs new file mode 100644 index 0000000000..9287614adb --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_Patch.cs @@ -0,0 +1,52 @@ +using HarmonyLib; +using NitroxClient.GameLogic; +using NitroxClient.MonoBehaviours; +using NitroxModel.Helper; +using NitroxPatcher.PatternMatching; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using static System.Reflection.Emit.OpCodes; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Synchronizes entities that can be broken and that will drop material, such as limestones... +/// +public class BreakableResource_SpawnResourceFromPrefab_Patch : NitroxPatch, IDynamicPatch +{ + public static readonly MethodInfo TARGET_METHOD_ORIGINAL = Reflect.Method(() => BreakableResource.SpawnResourceFromPrefab(default, default, default)); + public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(TARGET_METHOD_ORIGINAL); + + private static readonly InstructionsPattern SpawnResFromPrefPattern = new() + { + { Reflect.Method((Rigidbody b) => b.AddForce(default(Vector3))), "DropItemInstance" }, + Ldc_I4_0 + }; + + public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) + { + static IEnumerable InsertCallback(string label, CodeInstruction _) + { + switch(label) + { + case "DropItemInstance": + yield return new(Ldloc_1); + yield return new(Call, Reflect.Method(() => Callback(default))); + break; + } + } + return instructions.Transform(SpawnResFromPrefPattern, InsertCallback); + } + + private static void Callback(GameObject __instance) + { + NitroxEntity.SetNewId(__instance, new()); + Resolve().Dropped(__instance); + } + + public override void Patch(Harmony harmony) + { + PatchTranspiler(harmony, TARGET_METHOD); + } +} diff --git a/NitroxPatcher/Patches/Dynamic/SpawnOnKill_OnKill_Patch.cs b/NitroxPatcher/Patches/Dynamic/SpawnOnKill_OnKill_Patch.cs new file mode 100644 index 0000000000..ac2c391912 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/SpawnOnKill_OnKill_Patch.cs @@ -0,0 +1,65 @@ +using HarmonyLib; +using NitroxClient.Communication.Abstract; +using NitroxClient.GameLogic; +using NitroxClient.MonoBehaviours; +using NitroxModel.DataStructures; +using NitroxModel.Helper; +using NitroxModel.Packets; +using NitroxPatcher.PatternMatching; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using static System.Reflection.Emit.OpCodes; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Synchronizes entities that Spawn something when they are killed, e.g. Coral Disks. +/// +public class SpawnOnKill_OnKill_Patch : NitroxPatch, IDynamicPatch +{ + public static readonly MethodInfo TARGET_METHOD = Reflect.Method((SpawnOnKill t) => t.OnKill()); + + private static readonly InstructionsPattern spawnInstanceOnKillPattern = new() + { + Reflect.Method(() => Object.Instantiate(default(GameObject), default(Vector3), default(Quaternion))), + { Stloc_0, "DropOnKillInstance" }, + Ldarg_0, + }; + + public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) + { + static IEnumerable InsertCallbackCall(string label, CodeInstruction _) + { + switch (label) + { + case "DropOnKillInstance": + yield return new(Ldarg_0); + yield return new(Ldloc_0); + yield return new(Call, Reflect.Method(() => Callback(default, default))); + break; + } + } + + return instructions.Transform(spawnInstanceOnKillPattern, InsertCallbackCall); + } + + private static void Callback(SpawnOnKill spawnOnKill, GameObject spawningItem) + { + if (!NitroxEntity.TryGetEntityFrom(spawnOnKill.gameObject, out NitroxEntity destroyedEntity)) + { + Log.Warn($"[{nameof(SpawnOnKill_OnKill_Patch)}] Could not find {nameof(NitroxEntity)} for breakable entity {spawnOnKill.gameObject.GetFullHierarchyPath()}."); + } + else + { + Resolve().Send(new EntityDestroyed(destroyedEntity.Id)); + } + NitroxEntity.SetNewId(spawningItem, new()); + Resolve().Dropped(spawningItem); + } + + public override void Patch(Harmony harmony) + { + PatchTranspiler(harmony, TARGET_METHOD); + } +} diff --git a/NitroxPatcher/PatternMatching/InstructionPattern.cs b/NitroxPatcher/PatternMatching/InstructionPattern.cs index f98ed71a81..c74645bc21 100644 --- a/NitroxPatcher/PatternMatching/InstructionPattern.cs +++ b/NitroxPatcher/PatternMatching/InstructionPattern.cs @@ -30,16 +30,26 @@ public override int GetHashCode() public static implicit operator InstructionPattern(OpCode opCode) => new() { OpCode = opCode }; public static implicit operator InstructionPattern(OperandPattern operand) => new() { Operand = operand }; - public static implicit operator InstructionPattern(MethodInfo method) => Call(method); + public static implicit operator InstructionPattern(MethodInfo method) => Call(method, true); public static InstructionPattern Call(string className, string methodName) => new() { OpCode = OpCodes.Call, Operand = new(className, methodName) }; - public static InstructionPattern Call(MethodInfo method) + public static InstructionPattern Call(MethodInfo method) => Call(method, false); + + private static InstructionPattern Call(MethodInfo method, bool matchAnyCallOpcode) { Type methodDeclaringType = method.DeclaringType; Validate.NotNull(methodDeclaringType); - return new() { OpCode = OpCodes.Call, Operand = new(methodDeclaringType.FullName, method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()) }; + return new() + { + OpCode = new OpCodePattern + { + OpCode = OpCodes.Call, + WeakMatch = matchAnyCallOpcode + }, + Operand = new(methodDeclaringType.FullName, method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()) + }; } public static bool operator ==(InstructionPattern pattern, CodeInstruction instruction) diff --git a/NitroxPatcher/PatternMatching/OpCodePattern.cs b/NitroxPatcher/PatternMatching/OpCodePattern.cs index 08f3353db7..769ed8b772 100644 --- a/NitroxPatcher/PatternMatching/OpCodePattern.cs +++ b/NitroxPatcher/PatternMatching/OpCodePattern.cs @@ -13,26 +13,22 @@ public readonly struct OpCodePattern public OpCode? OpCode { get; init; } + /// + /// If true, similar opcodes will be matched as being the same. + /// + /// + /// Example for similar opcodes (call): call, callvirt and calli. + /// + public bool WeakMatch { get; init; } + + public bool IsAnyCall => WeakMatch && (OpCode == OpCodes.Call || OpCode == OpCodes.Callvirt || OpCode == OpCodes.Calli); + public static implicit operator OpCodePattern(OpCode opCode) => new() { OpCode = opCode }; - public static bool operator ==(OpCodePattern pattern, OpCode opCode) - { - return pattern.OpCode == opCode; - } - - public static bool operator ==(OpCode opCode, OpCodePattern pattern) - { - return pattern.OpCode == opCode; - } - - public static bool operator !=(OpCode opCode, OpCodePattern pattern) - { - return !(opCode == pattern); - } - - public static bool operator !=(OpCodePattern pattern, OpCode opCode) - { - return !(pattern == opCode); - } -} + public static bool operator ==(OpCodePattern pattern, OpCode opCode) => pattern.OpCode == opCode || + (pattern.IsAnyCall && (opCode == OpCodes.Call || opCode == OpCodes.Callvirt || opCode == OpCodes.Calli)); + public static bool operator ==(OpCode opCode, OpCodePattern pattern) => pattern == opCode; + public static bool operator !=(OpCodePattern pattern, OpCode opCode) => !(pattern == opCode); + public static bool operator !=(OpCode opCode, OpCodePattern pattern) => !(opCode == pattern); +} From f9ac48b7845f778d1214b6cafe68fc3f43501b88 Mon Sep 17 00:00:00 2001 From: Jannify <23176718+Jannify@users.noreply.github.com> Date: Sat, 22 Apr 2023 21:22:09 +0200 Subject: [PATCH 2/5] Creating custom AutoBogus for WorldPersistenceTest and PacketsSerializableTest (#2018) * Fix potential NRE in AssertHelper * Remove old Bogus implementation * Added custom AutoFaker implementation * Use custom faker for WorldPersistenceTest * Updating WorldPersistenceTest with data structure changes * Fixing unnoticed bugs; not anymore with the awesome savedata faker :D * Implement the new faker in the PacketsSerializableTest * Downgrade AutoBogus nuget to Bogus * Address requested changes --- Nitrox.Test/Helper/AssertHelper.cs | 7 + .../Helper/Faker/NitroxAbstractFaker.cs | 70 +++++ Nitrox.Test/Helper/Faker/NitroxActionFaker.cs | 19 ++ Nitrox.Test/Helper/Faker/NitroxAutoFaker.cs | 218 +++++++++++++++ .../Helper/Faker/NitroxCollectionFaker.cs | 163 +++++++++++ Nitrox.Test/Helper/Faker/NitroxFaker.cs | 119 ++++++++ .../Helper/Faker/NitroxNullableFaker.cs | 27 ++ .../Helper/Faker/NitroxOptionalFaker.cs | 28 ++ .../Serialization/NitroxAutoBinderBase.cs | 84 ------ .../Helper/Serialization/NitroxAutoFaker.cs | 42 --- .../NitroxAutoFakerNonGeneric.cs | 42 --- .../Helper/Serialization/PacketAutoBinder.cs | 17 -- .../Model/Packets/PacketsSerializableTest.cs | 65 ++--- Nitrox.Test/Nitrox.Test.csproj | 3 +- .../Serialization/WorldPersistenceTest.cs | 256 ++++++------------ .../Processors/ExosuitArmActionProcessor.cs | 8 +- NitroxClient/GameLogic/Cyclops.cs | 3 +- NitroxClient/GameLogic/ExosuitModuleEvent.cs | 3 +- .../Actions/SerializableCreatureAction.cs | 10 +- .../GameLogic/CyclopsDamageInfoData.cs | 5 +- .../Packets/ExosuitArmActionPacket.cs | 7 +- .../Entities/Metadata/EntityMetadata.cs | 1 + .../Entities/Metadata/PlayerMetadata.cs | 8 +- .../DataStructures/GameLogic/Entity.cs | 1 + NitroxModel/DataStructures/NitroxVersion.cs | 1 + NitroxModel/Packets/VehicleMovement.cs | 6 + .../Serialization/World/WorldPersistence.cs | 54 ++-- 27 files changed, 841 insertions(+), 426 deletions(-) create mode 100644 Nitrox.Test/Helper/Faker/NitroxAbstractFaker.cs create mode 100644 Nitrox.Test/Helper/Faker/NitroxActionFaker.cs create mode 100644 Nitrox.Test/Helper/Faker/NitroxAutoFaker.cs create mode 100644 Nitrox.Test/Helper/Faker/NitroxCollectionFaker.cs create mode 100644 Nitrox.Test/Helper/Faker/NitroxFaker.cs create mode 100644 Nitrox.Test/Helper/Faker/NitroxNullableFaker.cs create mode 100644 Nitrox.Test/Helper/Faker/NitroxOptionalFaker.cs delete mode 100644 Nitrox.Test/Helper/Serialization/NitroxAutoBinderBase.cs delete mode 100644 Nitrox.Test/Helper/Serialization/NitroxAutoFaker.cs delete mode 100644 Nitrox.Test/Helper/Serialization/NitroxAutoFakerNonGeneric.cs delete mode 100644 Nitrox.Test/Helper/Serialization/PacketAutoBinder.cs diff --git a/Nitrox.Test/Helper/AssertHelper.cs b/Nitrox.Test/Helper/AssertHelper.cs index 7b93cb3bfe..230d087923 100644 --- a/Nitrox.Test/Helper/AssertHelper.cs +++ b/Nitrox.Test/Helper/AssertHelper.cs @@ -9,6 +9,9 @@ public static class AssertHelper { public static void IsListEqual(IOrderedEnumerable first, IOrderedEnumerable second, Action assertComparer) { + Assert.IsNotNull(first); + Assert.IsNotNull(second); + List firstList = first.ToList(); List secondList = second.ToList(); @@ -22,6 +25,8 @@ public static void IsListEqual(IOrderedEnumerable first, IOrde public static void IsDictionaryEqual(IDictionary first, IDictionary second) { + Assert.IsNotNull(first); + Assert.IsNotNull(second); Assert.AreEqual(first.Count, second.Count); for (int index = 0; index < first.Count; index++) @@ -34,6 +39,8 @@ public static void IsDictionaryEqual(IDictionary fir public static void IsDictionaryEqual(IDictionary first, IDictionary second, Action, KeyValuePair> assertComparer) { + Assert.IsNotNull(first); + Assert.IsNotNull(second); Assert.AreEqual(first.Count, second.Count); for (int index = 0; index < first.Count; index++) diff --git a/Nitrox.Test/Helper/Faker/NitroxAbstractFaker.cs b/Nitrox.Test/Helper/Faker/NitroxAbstractFaker.cs new file mode 100644 index 0000000000..04afa84663 --- /dev/null +++ b/Nitrox.Test/Helper/Faker/NitroxAbstractFaker.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using NitroxModel_Subnautica.Logger; +using NitroxModel.Packets; +using NitroxModel.Packets.Processors.Abstract; +using NitroxServer; +using NitroxServer_Subnautica; +using NitroxServer.ConsoleCommands.Abstract; + +namespace Nitrox.Test.Helper.Faker; + +public class NitroxAbstractFaker : NitroxFaker, INitroxFaker +{ + private static readonly Dictionary subtypesByBaseType; + + static NitroxAbstractFaker() + { + Assembly[] assemblies = { typeof(Packet).Assembly, typeof(SubnauticaInGameLogger).Assembly, typeof(ServerAutoFacRegistrar).Assembly, typeof(SubnauticaServerAutoFacRegistrar).Assembly }; + HashSet blacklistedTypes = new() { typeof(Packet), typeof(CorrelatedPacket), typeof(Command), typeof(PacketProcessor) }; + + List types = new(); + foreach (Assembly assembly in assemblies) + { + types.AddRange(assembly.GetTypes()); + } + + subtypesByBaseType = types.Where(type => type.IsAbstract && !type.IsSealed && !blacklistedTypes.Contains(type)) + .ToDictionary(type => type, type => types.Where(t => type.IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface).ToArray()) + .Where(dict => dict.Value.Length > 0) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + + public readonly int AssignableTypesCount; + private readonly Queue assignableFakers = new(); + + public NitroxAbstractFaker(Type type) + { + if (!type.IsAbstract) + { + throw new ArgumentException("Argument is not abstract", nameof(type)); + } + + if (!subtypesByBaseType.TryGetValue(type, out Type[] subTypes)) + { + throw new ArgumentException($"Argument is not contained in {nameof(subtypesByBaseType)}", nameof(type)); + } + + OutputType = type; + AssignableTypesCount = subTypes.Length; + FakerByType.Add(type, this); + foreach (Type subType in subTypes) + { + assignableFakers.Enqueue(GetOrCreateFaker(subType)); + } + } + + public INitroxFaker[] GetSubFakers() => assignableFakers.ToArray(); + + /// + /// Selects an implementing type in a round-robin fashion of the abstract type of this faker. Then creates an instance of it. + /// + public object GenerateUnsafe(HashSet typeTree) + { + INitroxFaker assignableFaker = assignableFakers.Dequeue(); + assignableFakers.Enqueue(assignableFaker); + return assignableFaker.GenerateUnsafe(typeTree); + } +} diff --git a/Nitrox.Test/Helper/Faker/NitroxActionFaker.cs b/Nitrox.Test/Helper/Faker/NitroxActionFaker.cs new file mode 100644 index 0000000000..8e821d5a9d --- /dev/null +++ b/Nitrox.Test/Helper/Faker/NitroxActionFaker.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace Nitrox.Test.Helper.Faker; + +public class NitroxActionFaker : NitroxFaker, INitroxFaker +{ + private readonly Func generateAction; + + public NitroxActionFaker(Type type, Func action) + { + OutputType = type; + generateAction = action; + } + + public INitroxFaker[] GetSubFakers() => Array.Empty(); + + public object GenerateUnsafe(HashSet _) => generateAction.Invoke(Faker); +} diff --git a/Nitrox.Test/Helper/Faker/NitroxAutoFaker.cs b/Nitrox.Test/Helper/Faker/NitroxAutoFaker.cs new file mode 100644 index 0000000000..510df23750 --- /dev/null +++ b/Nitrox.Test/Helper/Faker/NitroxAutoFaker.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using BinaryPack.Attributes; + +namespace Nitrox.Test.Helper.Faker; + +public class NitroxAutoFaker : NitroxFaker, INitroxFaker +{ + private readonly ConstructorInfo constructor; + private readonly MemberInfo[] memberInfos; + private readonly INitroxFaker[] parameterFakers; + + public NitroxAutoFaker() + { + Type type = typeof(T); + if (!IsValidType(type)) + { + throw new InvalidOperationException($"{type.Name} is not a valid type for {nameof(NitroxAutoFaker)}"); + } + + OutputType = type; + FakerByType.Add(type, this); + + if (type.GetCustomAttributes(typeof(DataContractAttribute), false).Length > 0) + { + memberInfos = type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(member => member.GetCustomAttributes().Any()).ToArray(); + } + else + { + memberInfos = type.GetMembers(BindingFlags.Public | BindingFlags.Instance) + .Where(member => member.MemberType is MemberTypes.Field or MemberTypes.Property && + !member.GetCustomAttributes().Any()) + .ToArray(); + } + + if (!TryGetConstructorForType(type, memberInfos, out constructor) && + !TryGetConstructorForType(type, Array.Empty(), out constructor)) + { + throw new NullReferenceException($"Could not find a constructor with no parameters for {type}"); + } + + parameterFakers = new INitroxFaker[memberInfos.Length]; + + Type[] constructorArgumentTypes = constructor.GetParameters().Select(p => p.ParameterType).ToArray(); + for (int i = 0; i < memberInfos.Length; i++) + { + Type dataMemberType = constructorArgumentTypes.Length == memberInfos.Length ? constructorArgumentTypes[i] : memberInfos[i].GetMemberType(); + + if (FakerByType.TryGetValue(dataMemberType, out INitroxFaker memberFaker)) + { + parameterFakers[i] = memberFaker; + } + else + { + parameterFakers[i] = CreateFaker(dataMemberType); + } + } + } + + private void ValidateFakerTree() + { + List fakerTree = new(); + + void ValidateFaker(INitroxFaker nitroxFaker) + { + if (fakerTree.Contains(nitroxFaker)) + { + return; + } + + fakerTree.Add(nitroxFaker); + + if (nitroxFaker is NitroxAbstractFaker abstractFaker) + { + NitroxCollectionFaker collectionFaker = (NitroxCollectionFaker)fakerTree.LastOrDefault(f => f.GetType() == typeof(NitroxCollectionFaker)); + if (collectionFaker != null) + { + collectionFaker.GenerateSize = Math.Max(collectionFaker.GenerateSize, abstractFaker.AssignableTypesCount); + } + } + + foreach (INitroxFaker subFaker in nitroxFaker.GetSubFakers()) + { + ValidateFaker(subFaker); + } + + fakerTree.Remove(nitroxFaker); + } + + foreach (INitroxFaker parameterFaker in parameterFakers) + { + ValidateFaker(parameterFaker); + } + } + + public INitroxFaker[] GetSubFakers() => parameterFakers; + + public T Generate() + { + ValidateFakerTree(); + return (T)GenerateUnsafe(new HashSet()); + } + + public object GenerateUnsafe(HashSet typeTree) + { + object[] parameterValues = new object[parameterFakers.Length]; + + for (int i = 0; i < parameterValues.Length; i++) + { + INitroxFaker parameterFaker = parameterFakers[i]; + + if (typeTree.Contains(parameterFaker.OutputType)) + { + if (parameterFaker is NitroxCollectionFaker collectionFaker) + { + parameterValues[i] = Activator.CreateInstance(collectionFaker.OutputCollectionType); + } + else + { + parameterValues[i] = null; + } + } + else + { + typeTree.Add(parameterFaker.OutputType); + parameterValues[i] = parameterFakers[i].GenerateUnsafe(typeTree); + typeTree.Remove(parameterFaker.OutputType); + } + } + + if (constructor.GetParameters().Length == parameterValues.Length) + { + return (T)constructor.Invoke(parameterValues); + } + + T obj = (T)constructor.Invoke(Array.Empty()); + for (int index = 0; index < memberInfos.Length; index++) + { + MemberInfo memberInfo = memberInfos[index]; + switch (memberInfo.MemberType) + { + case MemberTypes.Field: + ((FieldInfo)memberInfo).SetValue(obj, parameterValues[index]); + break; + case MemberTypes.Property: + PropertyInfo propertyInfo = (PropertyInfo)memberInfo; + + if (!propertyInfo.CanWrite && + NitroxCollectionFaker.IsCollection(parameterValues[index].GetType(), out NitroxCollectionFaker.CollectionType collectionType)) + { + dynamic origColl = propertyInfo.GetValue(obj); + + switch (collectionType) + { + case NitroxCollectionFaker.CollectionType.ARRAY: + for (int i = 0; i < ((Array)parameterValues[index]).Length; i++) + { + origColl[i] = ((Array)parameterValues[index]).GetValue(i); + } + + break; + case NitroxCollectionFaker.CollectionType.LIST: + case NitroxCollectionFaker.CollectionType.DICTIONARY: + case NitroxCollectionFaker.CollectionType.SET: + foreach (dynamic createdValue in ((IEnumerable)parameterValues[index])) + { + origColl.Add(createdValue); + } + + break; + case NitroxCollectionFaker.CollectionType.NONE: + default: + throw new ArgumentOutOfRangeException(); + } + } + else + { + propertyInfo.SetValue(obj, parameterValues[index]); + } + + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + return obj; + } + + private static bool TryGetConstructorForType(Type type, MemberInfo[] dataMembers, out ConstructorInfo constructorInfo) + { + foreach (ConstructorInfo constructor in type.GetConstructors()) + { + if (constructor.GetParameters().Length != dataMembers.Length) + { + continue; + } + + bool parameterValid = constructor.GetParameters() + .All(parameter => dataMembers.Any(d => d.GetMemberType() == parameter.ParameterType && + d.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase))); + + if (parameterValid) + { + constructorInfo = constructor; + return true; + } + } + + constructorInfo = null; + return false; + } +} diff --git a/Nitrox.Test/Helper/Faker/NitroxCollectionFaker.cs b/Nitrox.Test/Helper/Faker/NitroxCollectionFaker.cs new file mode 100644 index 0000000000..c3400d721e --- /dev/null +++ b/Nitrox.Test/Helper/Faker/NitroxCollectionFaker.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Nitrox.Test.Helper.Faker; + +public class NitroxCollectionFaker : NitroxFaker, INitroxFaker +{ + public enum CollectionType + { + NONE, + ARRAY, + LIST, + DICTIONARY, + SET + } + + private const int DEFAULT_SIZE = 2; + + public static bool IsCollection(Type t, out CollectionType collectionType) + { + if (t.IsArray && t.GetArrayRank() == 1) + { + collectionType = CollectionType.ARRAY; + return true; + } + + if (t.IsGenericType) + { + Type[] genericInterfacesDefinition = t.GetInterfaces() + .Where(i => i.IsGenericType) + .Select(i => i.GetGenericTypeDefinition()) + .ToArray(); + + if (genericInterfacesDefinition.Any(i => i == typeof(IList<>))) + { + collectionType = CollectionType.LIST; + return true; + } + + if (genericInterfacesDefinition.Any(i => i == typeof(IDictionary<,>))) + { + collectionType = CollectionType.DICTIONARY; + return true; + } + + if (genericInterfacesDefinition.Any(i => i == typeof(ISet<>))) + { + collectionType = CollectionType.SET; + return true; + } + } + + collectionType = CollectionType.NONE; + return false; + } + + public static bool TryGetCollectionTypes(Type type, out Type[] types) + { + if (!IsCollection(type, out CollectionType collectionType)) + { + types = Array.Empty(); + return false; + } + + if (collectionType == CollectionType.ARRAY) + { + types = new[] {type.GetElementType() }; + return true; + } + + types = type.GenericTypeArguments; + return true; + } + + public int GenerateSize = DEFAULT_SIZE; + public Type OutputCollectionType; + + private readonly INitroxFaker[] subFakers; + private readonly Func, object> generateAction; + + public NitroxCollectionFaker(Type type, CollectionType collectionType) + { + OutputCollectionType = type; + INitroxFaker elementFaker; + + switch (collectionType) + { + case CollectionType.ARRAY: + Type arrayType = OutputType = type.GetElementType(); + elementFaker = GetOrCreateFaker(arrayType); + subFakers = new[] { elementFaker }; + + generateAction = typeTree => + { + typeTree.Add(arrayType); + Array array = Array.CreateInstance(arrayType, GenerateSize); + + for (int i = 0; i < GenerateSize; i++) + { + array.SetValue(elementFaker.GenerateUnsafe(typeTree), i); + } + + typeTree.Remove(arrayType); + return array; + }; + break; + case CollectionType.LIST: + case CollectionType.SET: + Type listType = OutputType = type.GenericTypeArguments[0]; + elementFaker = GetOrCreateFaker(listType); + subFakers = new[] { elementFaker }; + + generateAction = typeTree => + { + typeTree.Add(listType); + dynamic list = Activator.CreateInstance(type); + + for (int i = 0; i < GenerateSize; i++) + { + MethodInfo castMethod = CastMethodBase.MakeGenericMethod(OutputType); + dynamic castedObject = castMethod.Invoke(null, new[] { elementFaker.GenerateUnsafe(typeTree)}); + list.Add(castedObject); + } + + typeTree.Remove(listType); + return list; + }; + break; + case CollectionType.DICTIONARY: + Type[] dicType = type.GenericTypeArguments; + OutputType = dicType[1]; // A little hacky but should work as we don't use circular dependencies as keys + INitroxFaker keyFaker = GetOrCreateFaker(dicType[0]); + INitroxFaker valueFaker = GetOrCreateFaker(dicType[1]); + subFakers = new[] { keyFaker, valueFaker }; + + generateAction = typeTree => + { + typeTree.Add(dicType[0]); + typeTree.Add(dicType[1]); + IDictionary dict = (IDictionary) Activator.CreateInstance(type); + for (int i = 0; i < GenerateSize; i++) + { + dict.Add(keyFaker.GenerateUnsafe(typeTree), valueFaker.GenerateUnsafe(typeTree)); + } + + typeTree.Remove(dicType[0]); + typeTree.Remove(dicType[1]); + return dict; + }; + break; + case CollectionType.NONE: + default: + throw new ArgumentOutOfRangeException(nameof(collectionType), collectionType, null); + } + } + + public INitroxFaker[] GetSubFakers() => subFakers; + + public object GenerateUnsafe(HashSet typeTree) => generateAction.Invoke(typeTree); +} diff --git a/Nitrox.Test/Helper/Faker/NitroxFaker.cs b/Nitrox.Test/Helper/Faker/NitroxFaker.cs new file mode 100644 index 0000000000..b24f7d9b93 --- /dev/null +++ b/Nitrox.Test/Helper/Faker/NitroxFaker.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using NitroxModel.DataStructures; +using NitroxModel.DataStructures.GameLogic; +using NitroxModel.DataStructures.Util; + +namespace Nitrox.Test.Helper.Faker; + +public interface INitroxFaker +{ + public Type OutputType { get; } + public INitroxFaker[] GetSubFakers(); + public object GenerateUnsafe(HashSet typeTree); +} + +public abstract class NitroxFaker +{ + public Type OutputType { get; protected init; } + protected static readonly Bogus.Faker Faker; + + static NitroxFaker() + { + Faker = new Bogus.Faker(); + } + + protected static readonly Dictionary FakerByType = new() + { + // Basic types + { typeof(bool), new NitroxActionFaker(typeof(bool), f => f.Random.Bool()) }, + { typeof(byte), new NitroxActionFaker(typeof(byte), f => f.Random.Byte()) }, + { typeof(sbyte), new NitroxActionFaker(typeof(sbyte), f => f.Random.SByte()) }, + { typeof(short), new NitroxActionFaker(typeof(short), f => f.Random.Short()) }, + { typeof(ushort), new NitroxActionFaker(typeof(ushort), f => f.Random.UShort()) }, + { typeof(int), new NitroxActionFaker(typeof(int), f => f.Random.Int()) }, + { typeof(uint), new NitroxActionFaker(typeof(uint), f => f.Random.UInt()) }, + { typeof(long), new NitroxActionFaker(typeof(long), f => f.Random.Long()) }, + { typeof(ulong), new NitroxActionFaker(typeof(ulong), f => f.Random.ULong()) }, + { typeof(decimal), new NitroxActionFaker(typeof(decimal), f => f.Random.Decimal()) }, + { typeof(float), new NitroxActionFaker(typeof(float), f => f.Random.Float()) }, + { typeof(double), new NitroxActionFaker(typeof(double), f => f.Random.Double()) }, + { typeof(char), new NitroxActionFaker(typeof(char), f => f.Random.Char()) }, + { typeof(string), new NitroxActionFaker(typeof(string), f => f.Random.Word()) }, + + // Nitrox types + { typeof(NitroxTechType), new NitroxActionFaker(typeof(NitroxTechType), f => new NitroxTechType(f.PickRandom().ToString())) }, + { typeof(NitroxId), new NitroxActionFaker(typeof(NitroxId), f => new NitroxId(f.Random.Guid())) }, + }; + + public static INitroxFaker GetOrCreateFaker(Type t) + { + return FakerByType.TryGetValue(t, out INitroxFaker nitroxFaker) ? nitroxFaker : CreateFaker(t); + } + + protected static INitroxFaker CreateFaker(Type type) + { + if (type.IsAbstract) + { + return new NitroxAbstractFaker(type); + } + + if (type.IsEnum) + { + return new NitroxActionFaker(type, f => + { + string[] selection = Enum.GetNames(type); + if (selection.Length == 0) + { + throw new ArgumentException("There are no enum values after exclusion to choose from."); + } + + string val = f.Random.ArrayElement(selection); + return Enum.Parse(type, val); + }); + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) + { + return new NitroxOptionalFaker(type); + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return new NitroxNullableFaker(type); + } + + if (NitroxCollectionFaker.IsCollection(type, out NitroxCollectionFaker.CollectionType collectionType)) + { + return new NitroxCollectionFaker(type, collectionType); + } + + ConstructorInfo constructor = typeof(NitroxAutoFaker<>).MakeGenericType(type).GetConstructor(Array.Empty()); + + if (constructor == null) + { + throw new NullReferenceException($"Could not get generic constructor for {type}"); + } + + return (INitroxFaker)constructor.Invoke(Array.Empty()); + } + + protected static bool IsValidType(Type type) + { + return FakerByType.ContainsKey(type) || + type.GetCustomAttributes(typeof(DataContractAttribute), false).Length >= 1 || + type.GetCustomAttributes(typeof(SerializableAttribute), false).Length >= 1 || + (NitroxCollectionFaker.TryGetCollectionTypes(type, out Type[] collectionTypes) && collectionTypes.All(IsValidType)) || + type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + protected static readonly MethodInfo CastMethodBase = typeof(NitroxFaker).GetMethod(nameof(Cast), BindingFlags.NonPublic | BindingFlags.Static); + + protected static T Cast(object o) + { + return (T)o; + } +} diff --git a/Nitrox.Test/Helper/Faker/NitroxNullableFaker.cs b/Nitrox.Test/Helper/Faker/NitroxNullableFaker.cs new file mode 100644 index 0000000000..470d151b39 --- /dev/null +++ b/Nitrox.Test/Helper/Faker/NitroxNullableFaker.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Nitrox.Test.Helper.Faker; + +public class NitroxNullableFaker : NitroxFaker, INitroxFaker +{ + private readonly Func, object> generateAction; + + public NitroxNullableFaker(Type type) + { + OutputType = type.GenericTypeArguments[0]; + generateAction = (typeTree) => + { + MethodInfo castMethod = CastMethodBase.MakeGenericMethod(OutputType); + object castedObject = castMethod.Invoke(null, new[] { GetOrCreateFaker(OutputType).GenerateUnsafe(typeTree) }); + + Type nullableType = typeof(Nullable<>).MakeGenericType(OutputType); + return Activator.CreateInstance(nullableType, castedObject); + }; + } + + public INitroxFaker[] GetSubFakers() => new[] { GetOrCreateFaker(OutputType) }; + + public object GenerateUnsafe(HashSet typeTree) => generateAction.Invoke(typeTree); +} diff --git a/Nitrox.Test/Helper/Faker/NitroxOptionalFaker.cs b/Nitrox.Test/Helper/Faker/NitroxOptionalFaker.cs new file mode 100644 index 0000000000..29ec70cc5e --- /dev/null +++ b/Nitrox.Test/Helper/Faker/NitroxOptionalFaker.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using NitroxModel.DataStructures.Util; + +namespace Nitrox.Test.Helper.Faker; + +public class NitroxOptionalFaker : NitroxFaker, INitroxFaker +{ + private readonly Func, object> generateAction; + + public NitroxOptionalFaker(Type type) + { + OutputType = type.GenericTypeArguments[0]; + generateAction = (typeTree) => + { + MethodInfo castMethod = CastMethodBase.MakeGenericMethod(OutputType); + object castedObject = castMethod.Invoke(null, new[] { GetOrCreateFaker(OutputType).GenerateUnsafe(typeTree) }); + + Type optionalType = typeof(Optional<>).MakeGenericType(OutputType); + return Activator.CreateInstance(optionalType, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance, null, new[] { castedObject }, null); + }; + } + + public INitroxFaker[] GetSubFakers() => new[] { GetOrCreateFaker(OutputType) }; + + public object GenerateUnsafe(HashSet typeTree) => generateAction.Invoke(typeTree); +} diff --git a/Nitrox.Test/Helper/Serialization/NitroxAutoBinderBase.cs b/Nitrox.Test/Helper/Serialization/NitroxAutoBinderBase.cs deleted file mode 100644 index 8fda088515..0000000000 --- a/Nitrox.Test/Helper/Serialization/NitroxAutoBinderBase.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using AutoBogus; - -namespace Nitrox.Test.Helper.Serialization; - -public abstract class NitroxAutoBinderBase : AutoBinder -{ - private readonly Dictionary> subtypesByBaseType; - - public NitroxAutoBinderBase(Dictionary subtypesByBaseType) - { - this.subtypesByBaseType = subtypesByBaseType.ToDictionary(pair => pair.Key, pair => new Queue(pair.Value)); - } - - /// - /// Populates abstract members with a manually generated object (see ) which is implementing the abstract class. - /// - /// - public override TType CreateInstance(AutoGenerateContext context) - { - Type type = typeof(TType); - - if (type.IsAbstract && type != typeof(Type)) // System.Type is abstract but that is not important to us - { - - if (!subtypesByBaseType.ContainsKey(type)) - { - throw new Exception($"{type} has no registered implementing classes."); - } - - if (subtypesByBaseType[type].Count == 0) - { - return default; - } - - Type subtype = subtypesByBaseType[type].Dequeue(); - subtypesByBaseType[type].Enqueue(subtype); - - // AutoBogus does not support a non-generic CreateInstance method - TType instance = (TType)typeof(AutoBinder).GetMethod(nameof(base.CreateInstance)) - .MakeGenericMethod(subtype).Invoke(this, new object[] { context }); - - return instance; - } - - if (context.GenerateType == context.ParentType) - { - return default; - } - - return base.CreateInstance(context); - } - - public override void PopulateInstance(object instance, AutoGenerateContext context, IEnumerable members = null) - { - Type type = typeof(TType); - - if (type.IsAbstract) - { - return; - } - - if (instance == null) - { - return; - } - - // Avoids type mismatch that would only populate inherited members - typeof(NitroxAutoBinderBase).GetMethod(nameof(PopulateInstanceBase), BindingFlags.NonPublic | BindingFlags.Instance) - .MakeGenericMethod(instance.GetType()).Invoke(this, new object[] { instance, context, members }); - } - - // Weird reflection bypass - private void PopulateInstanceBase(object instance, AutoGenerateContext context, IEnumerable members) - { - base.PopulateInstance(instance, context, members); - } - - /// - public abstract override Dictionary GetMembers(Type t); -} diff --git a/Nitrox.Test/Helper/Serialization/NitroxAutoFaker.cs b/Nitrox.Test/Helper/Serialization/NitroxAutoFaker.cs deleted file mode 100644 index 1c4488cf9d..0000000000 --- a/Nitrox.Test/Helper/Serialization/NitroxAutoFaker.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AutoBogus; -using NitroxModel.DataStructures.GameLogic; - -namespace Nitrox.Test.Helper.Serialization; - -public class NitroxAutoFaker : AutoFaker - where TType : class - where TBinder : NitroxAutoBinderBase -{ - public NitroxAutoFaker(TBinder binder) : this(new Dictionary(), binder) { } - - public NitroxAutoFaker(Dictionary subtypesByBaseType, TBinder binder) - { - Configure(newBinder => - { - newBinder.WithBinder(binder) - .WithOverride(new NitroxTechTypeOverride()); - - if (subtypesByBaseType.Values.Count != 0) - { - int highestAbstractObjectCount = subtypesByBaseType.Values.Max(objects => objects.Length); - newBinder.WithRepeatCount(Math.Max(2, highestAbstractObjectCount)); - } - }); - } - - private class NitroxTechTypeOverride : AutoGeneratorOverride - { - public override bool CanOverride(AutoGenerateContext context) - { - return context.GenerateType == typeof(NitroxTechType); - } - - public override void Generate(AutoGenerateOverrideContext context) - { - context.Instance = new NitroxTechType(context.Faker.PickRandom().ToString()); - } - } -} diff --git a/Nitrox.Test/Helper/Serialization/NitroxAutoFakerNonGeneric.cs b/Nitrox.Test/Helper/Serialization/NitroxAutoFakerNonGeneric.cs deleted file mode 100644 index 2139a95211..0000000000 --- a/Nitrox.Test/Helper/Serialization/NitroxAutoFakerNonGeneric.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using AutoBogus; -using Bogus; - -namespace Nitrox.Test.Helper.Serialization; - -/// -/// wrapper with non-generic type support -/// -/// -public class NitroxAutoFakerNonGeneric -{ - private readonly object faker; - - public NitroxAutoFakerNonGeneric(Type type, Dictionary subtypesByBaseType, IBinder binder) - { - ConstructorInfo constructor = typeof(NitroxAutoFaker<,>).MakeGenericType(type, typeof(PacketAutoBinder)) - .GetConstructor(new[] { typeof(Dictionary), typeof(PacketAutoBinder) }); - - if (constructor == null) - { - throw new NullReferenceException($"Could not get generic constructor for {type}"); - } - - faker = constructor.Invoke(new object[] { subtypesByBaseType, binder }); - } - - public T Generate(Type type) - { - MethodInfo method = typeof(AutoFaker<>).MakeGenericType(type) - .GetMethod(nameof(AutoFaker.Generate), new[] { typeof(string) }); - - if (method == null) - { - throw new NullReferenceException($"Could not get generic method for {type}"); - } - - return (T)method.Invoke(faker, new object[] { null }); - } -} diff --git a/Nitrox.Test/Helper/Serialization/PacketAutoBinder.cs b/Nitrox.Test/Helper/Serialization/PacketAutoBinder.cs deleted file mode 100644 index 0e76f647e5..0000000000 --- a/Nitrox.Test/Helper/Serialization/PacketAutoBinder.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Nitrox.Test.Helper.Serialization; -internal class PacketAutoBinder : NitroxAutoBinderBase -{ - public PacketAutoBinder(Dictionary subtypesByBaseType) : base(subtypesByBaseType) { } - - public override Dictionary GetMembers(Type t) - { - return t.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .Where(member => member.MemberType == MemberTypes.Field) - .GroupBy(member => member.Name).ToDictionary(k => k.Key, g => g.First()); - } -} diff --git a/Nitrox.Test/Model/Packets/PacketsSerializableTest.cs b/Nitrox.Test/Model/Packets/PacketsSerializableTest.cs index 38e7827fc3..dc000788b8 100644 --- a/Nitrox.Test/Model/Packets/PacketsSerializableTest.cs +++ b/Nitrox.Test/Model/Packets/PacketsSerializableTest.cs @@ -6,7 +6,8 @@ using KellermanSoftware.CompareNetObjects; using KellermanSoftware.CompareNetObjects.TypeComparers; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Nitrox.Test.Helper.Serialization; +using Nitrox.Test.Helper.Faker; +using NitroxModel_Subnautica.Logger; using NitroxModel.DataStructures; namespace NitroxModel.Packets; @@ -14,14 +15,6 @@ namespace NitroxModel.Packets; [TestClass] public class PacketsSerializableTest { - private static Assembly subnauticaModelAssembly; - - [TestInitialize] - public void Initialize() - { - subnauticaModelAssembly = AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName("NitroxModel-Subnautica.dll")); - } - [TestMethod] public void InitSerializerTest() { @@ -31,53 +24,47 @@ public void InitSerializerTest() [TestMethod] public void PacketSerializationTest() { - IEnumerable types = typeof(Packet).Assembly.GetTypes().Concat(subnauticaModelAssembly.GetTypes()); - Dictionary subtypesByBaseType = types - .Where(type => type.IsAbstract && !type.IsSealed && !type.ContainsGenericParameters && type != typeof(Packet)) - .ToDictionary(type => type, type => types.Where(t => type.IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface).ToArray()); - IEnumerable packetTypes = types.Where(p => typeof(Packet).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract); + ComparisonConfig config = new(); + config.SkipInvalidIndexers = true; + config.AttributesToIgnore.Add(typeof(IgnoredMemberAttribute)); + config.CustomComparers.Add(new CustomComparer((id1, id2) => id1.Equals(id2))); + CompareLogic comparer = new(config); + IEnumerable types = typeof(Packet).Assembly.GetTypes().Concat(typeof(SubnauticaInGameLogger).Assembly.GetTypes()); + Type[] packetTypes = types.Where(p => typeof(Packet).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract).ToArray(); // We want to ignore packets with no members when using ShouldNotCompare - PacketAutoBinder binder = new(subtypesByBaseType); - Type[] emptyPackets = packetTypes.Where(x => binder.GetMembers(x).Count == 0 || - binder.GetMembers(x).All(m => m.Value.GetMemberType().IsEnum)) + Type[] emptyPackets = packetTypes.Where(t => !t.GetMembers(BindingFlags.Public | BindingFlags.Instance) + .Any(member => member.MemberType is MemberTypes.Field or MemberTypes.Property && + !member.GetCustomAttributes().Any())) .ToArray(); // We generate two different versions of each packet to verify comparison is actually working - List> generatedPackets = new(); + List<(Packet, Packet)> generatedPackets = new(); foreach (Type type in packetTypes) { - NitroxAutoFakerNonGeneric faker = new(type, subtypesByBaseType, binder); + dynamic faker = NitroxFaker.GetOrCreateFaker(type); + + Packet packet = faker.Generate(); + Packet packet2 = null; - if (subtypesByBaseType.ContainsKey(type)) + if (!emptyPackets.Contains(type)) { - for (int i = 0; i < subtypesByBaseType[type].Length; i++) + ComparisonResult result; + do { - Packet packet = faker.Generate(subtypesByBaseType[type][i]); - Packet packet2 = faker.Generate(subtypesByBaseType[type][i]); - generatedPackets.Add(new Tuple(packet, packet2)); - } - } - else - { - Packet packet = faker.Generate(type); - Packet packet2 = faker.Generate(type); - generatedPackets.Add(new Tuple(packet, packet2)); + packet2 = faker.Generate(); + result = comparer.Compare(packet, packet2); + } while (result == null || result.AreEqual); } + + generatedPackets.Add(new ValueTuple(packet, packet2)); } Packet.InitSerializer(); - - - ComparisonConfig config = new(); - config.SkipInvalidIndexers = true; - config.AttributesToIgnore.Add(typeof(IgnoredMemberAttribute)); - config.CustomComparers.Add(new CustomComparer((id1, id2) => id1.Equals(id2))); - - foreach (Tuple packet in generatedPackets) + foreach (ValueTuple packet in generatedPackets) { Packet deserialized = Packet.Deserialize(packet.Item1.Serialize()); diff --git a/Nitrox.Test/Nitrox.Test.csproj b/Nitrox.Test/Nitrox.Test.csproj index e1d7df0ede..7e032ebeec 100644 --- a/Nitrox.Test/Nitrox.Test.csproj +++ b/Nitrox.Test/Nitrox.Test.csproj @@ -7,8 +7,7 @@ - - + diff --git a/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs b/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs index 80aeced60f..09e2945a85 100644 --- a/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs +++ b/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs @@ -6,20 +6,13 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Nitrox.Test; using Nitrox.Test.Helper; +using Nitrox.Test.Helper.Faker; using NitroxModel_Subnautica.DataStructures.GameLogic.Buildings.Rotation.Metadata; using NitroxModel.Core; -using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.GameLogic.Buildings.Metadata; -using NitroxModel.DataStructures.GameLogic.Buildings.Rotation; -using NitroxModel.DataStructures.Unity; -using NitroxModel.DataStructures.Util; -using NitroxModel.Server; using NitroxServer_Subnautica; using NitroxServer.GameLogic; -using NitroxServer.GameLogic.Bases; -using NitroxServer.GameLogic.Entities; -using NitroxServer.GameLogic.Players; using NitroxServer.GameLogic.Unlockables; using NitroxServer.Serialization.World; using NitroxModel.DataStructures.GameLogic.Entities; @@ -30,7 +23,7 @@ namespace NitroxServer.Serialization; [TestClass] public class WorldPersistenceTest { - private static string tempSaveFilePath; + private static readonly string tempSaveFilePath = Path.Combine(Path.GetTempPath(), "NitroxTestTempDir"); private static PersistedWorldData worldData; public static PersistedWorldData[] WorldsDataAfter { get; private set; } public static IServerSerializer[] ServerSerializers { get; private set; } @@ -44,24 +37,18 @@ public static void ClassInitialize(TestContext context) WorldPersistence worldPersistence = NitroxServiceLocator.LocateService(); ServerSerializers = NitroxServiceLocator.LocateService(); WorldsDataAfter = new PersistedWorldData[ServerSerializers.Length]; - tempSaveFilePath = Path.Combine(Path.GetTempPath(), "NitroxTestTempDir"); - worldData = GeneratePersistedWorldData(); - World.World world = worldPersistence.CreateWorld(worldData, ServerGameMode.CREATIVE); - world.TimeKeeper.ResetCount(); + worldData = new NitroxAutoFaker().Generate(); for (int index = 0; index < ServerSerializers.Length; index++) { //Checking saving worldPersistence.UpdateSerializer(ServerSerializers[index]); - Assert.IsTrue(worldPersistence.Save(world, tempSaveFilePath), $"Saving normal world failed while using {ServerSerializers[index]}."); - Assert.IsFalse(worldPersistence.Save(null, tempSaveFilePath), $"Saving null world worked while using {ServerSerializers[index]}."); + Assert.IsTrue(worldPersistence.Save(worldData, tempSaveFilePath), $"Saving normal world failed while using {ServerSerializers[index]}."); //Checking loading - Optional worldAfter = worldPersistence.LoadFromFile(tempSaveFilePath); - Assert.IsTrue(worldAfter.HasValue, $"Loading saved world failed while using {ServerSerializers[index]}."); - worldAfter.Value.TimeKeeper.ResetCount(); - WorldsDataAfter[index] = PersistedWorldData.From(worldAfter.Value); + WorldsDataAfter[index] = worldPersistence.LoadDataFromPath(tempSaveFilePath); + Assert.IsNotNull(WorldsDataAfter[index], $"Loading saved world failed while using {ServerSerializers[index]}."); } } @@ -70,6 +57,10 @@ public void WorldDataTest(PersistedWorldData worldDataAfter, string serializerNa { Assert.IsTrue(worldData.WorldData.ParsedBatchCells.SequenceEqual(worldDataAfter.WorldData.ParsedBatchCells)); Assert.AreEqual(worldData.WorldData.Seed, worldDataAfter.WorldData.Seed); + + PDAStateTest(worldData.WorldData.GameData.PDAState, worldDataAfter.WorldData.GameData.PDAState); + StoryGoalTest(worldData.WorldData.GameData.StoryGoals, worldDataAfter.WorldData.GameData.StoryGoals); + StoryTimingTest(worldData.WorldData.GameData.StoryTiming, worldDataAfter.WorldData.GameData.StoryTiming); } private static void ItemDataTest(ItemData itemData, ItemData itemDataAfter) @@ -95,14 +86,6 @@ private static void ItemDataTest(ItemData itemData, ItemData itemDataAfter) } } - [DataTestMethod, DynamicWorldDataAfter] - public void GameDataTest(PersistedWorldData worldDataAfter, string serializerName) - { - PDAStateTest(worldData.WorldData.GameData.PDAState, worldDataAfter.WorldData.GameData.PDAState); - StoryGoalTest(worldData.WorldData.GameData.StoryGoals, worldDataAfter.WorldData.GameData.StoryGoals); - StoryTimingTest(worldData.WorldData.GameData.StoryTiming, worldDataAfter.WorldData.GameData.StoryTiming); - } - private static void PDAStateTest(PDAStateData pdaState, PDAStateData pdaStateAfter) { Assert.IsTrue(pdaState.KnownTechTypes.SequenceEqual(pdaStateAfter.KnownTechTypes)); @@ -128,7 +111,12 @@ private static void StoryGoalTest(StoryGoalData storyGoal, StoryGoalData storyGo Assert.IsTrue(storyGoal.CompletedGoals.SequenceEqual(storyGoalAfter.CompletedGoals)); Assert.IsTrue(storyGoal.RadioQueue.SequenceEqual(storyGoalAfter.RadioQueue)); Assert.IsTrue(storyGoal.GoalUnlocks.SequenceEqual(storyGoalAfter.GoalUnlocks)); - Assert.IsTrue(storyGoal.ScheduledGoals.SequenceEqual(storyGoalAfter.ScheduledGoals)); + AssertHelper.IsListEqual(storyGoal.ScheduledGoals.OrderBy(x => x.GoalKey), storyGoalAfter.ScheduledGoals.OrderBy(x => x.GoalKey), (scheduledGoal, scheduledGoalAfter) => + { + Assert.AreEqual(scheduledGoal.TimeExecute, scheduledGoalAfter.TimeExecute); + Assert.AreEqual(scheduledGoal.GoalKey, scheduledGoalAfter.GoalKey); + Assert.AreEqual(scheduledGoal.GoalType, scheduledGoalAfter.GoalType); + }); } private static void StoryTimingTest(StoryTimingData storyTiming, StoryTimingData storyTimingAfter) @@ -271,6 +259,9 @@ private static void EntityTest(Entity entity, Entity entityAfter) case PrecursorDoorwayMetadata metadata when entityAfter.Metadata is PrecursorDoorwayMetadata metadataAfter: Assert.AreEqual(metadata.IsOpen, metadataAfter.IsOpen); break; + case PrecursorTeleporterMetadata metadata when entityAfter.Metadata is PrecursorTeleporterMetadata metadataAfter: + Assert.AreEqual(metadata.IsOpen, metadataAfter.IsOpen); + break; case PrecursorKeyTerminalMetadata metadata when entityAfter.Metadata is PrecursorKeyTerminalMetadata metadataAfter: Assert.AreEqual(metadata.Slotted, metadataAfter.Slotted); break; @@ -307,32 +298,55 @@ private static void EntityTest(Entity entity, Entity entityAfter) case RepairedComponentMetadata metadata when entityAfter.Metadata is RepairedComponentMetadata metadataAfter: Assert.AreEqual(metadata.TechType, metadataAfter.TechType); break; - case PlantableMetadata metadata when entityAfter.Metadata is PlantableMetadata metadataAfter: - Assert.AreEqual(metadata.Progress, metadataAfter.Progress); - break; case CrafterMetadata metadata when entityAfter.Metadata is CrafterMetadata metadataAfter: - Assert.AreEqual(metadata.Duration, metadataAfter.Duration); Assert.AreEqual(metadata.TechType, metadataAfter.TechType); Assert.AreEqual(metadata.StartTime, metadataAfter.StartTime); + Assert.AreEqual(metadata.Duration, metadataAfter.Duration); + break; + case PlantableMetadata metadata when entityAfter.Metadata is PlantableMetadata metadataAfter: + Assert.AreEqual(metadata.Progress, metadataAfter.Progress); break; case CyclopsMetadata metadata when entityAfter.Metadata is CyclopsMetadata metadataAfter: Assert.AreEqual(metadata.SilentRunningOn, metadataAfter.SilentRunningOn); + Assert.AreEqual(metadata.ShieldOn, metadataAfter.ShieldOn); + Assert.AreEqual(metadata.SonarOn, metadataAfter.SonarOn); Assert.AreEqual(metadata.EngineOn, metadataAfter.EngineOn); Assert.AreEqual(metadata.EngineMode, metadataAfter.EngineMode); Assert.AreEqual(metadata.Health, metadataAfter.Health); break; + case SeamothMetadata metadata when entityAfter.Metadata is SeamothMetadata metadataAfter: + Assert.AreEqual(metadata.LightsOn, metadataAfter.LightsOn); + Assert.AreEqual(metadata.Health, metadataAfter.Health); + break; + case SubNameInputMetadata metadata when entityAfter.Metadata is SubNameInputMetadata metadataAfter: + Assert.AreEqual(metadata.Name, metadataAfter.Name); + Assert.IsTrue(metadata.Colors.SequenceEqual(metadataAfter.Colors)); + break; + case RocketMetadata metadata when entityAfter.Metadata is RocketMetadata metadataAfter: + Assert.AreEqual(metadata.CurrentStage, metadataAfter.CurrentStage); + Assert.AreEqual(metadata.LastStageTransitionTime, metadataAfter.LastStageTransitionTime); + Assert.AreEqual(metadata.ElevatorState, metadataAfter.ElevatorState); + Assert.AreEqual(metadata.ElevatorPosition, metadataAfter.ElevatorPosition); + Assert.IsTrue(metadata.PreflightChecks.SequenceEqual(metadataAfter.PreflightChecks)); + break; case CyclopsLightingMetadata metadata when entityAfter.Metadata is CyclopsLightingMetadata metadataAfter: - Assert.AreEqual(metadata.InternalLightsOn, metadataAfter.InternalLightsOn); Assert.AreEqual(metadata.FloodLightsOn, metadataAfter.FloodLightsOn); + Assert.AreEqual(metadata.InternalLightsOn, metadataAfter.InternalLightsOn); break; case FireExtinguisherHolderMetadata metadata when entityAfter.Metadata is FireExtinguisherHolderMetadata metadataAfter: - Assert.AreEqual(metadata.Fuel, metadataAfter.Fuel); Assert.AreEqual(metadata.HasExtinguisher, metadataAfter.HasExtinguisher); + Assert.AreEqual(metadata.Fuel, metadataAfter.Fuel); break; - case null when entityAfter.Metadata is null: + case PlayerMetadata metadata when entityAfter.Metadata is PlayerMetadata metadataAfter: + AssertHelper.IsListEqual(metadata.EquippedItems.OrderBy(x => x.Id), metadataAfter.EquippedItems.OrderBy(x => x.Id), (equippedItem, equippedItemAfter) => + { + Assert.AreEqual(equippedItem.Id, equippedItemAfter.Id); + Assert.AreEqual(equippedItem.Slot, equippedItemAfter.Slot); + Assert.AreEqual(equippedItem.TechType, equippedItemAfter.TechType); + }); break; default: - Assert.Fail($"Runtime type of {nameof(Entity)}.{nameof(Entity.Metadata)} is not equal"); + Assert.Fail($"Runtime type of {nameof(Entity)}.{nameof(Entity.Metadata)} is not equal: {entity.Metadata?.GetType().Name} - {entityAfter.Metadata?.GetType().Name}"); break; } @@ -348,15 +362,32 @@ private static void EntityTest(Entity entity, Entity entityAfter) Assert.AreEqual(worldEntity.WaterParkId, worldEntityAfter.WaterParkId); Assert.AreEqual(worldEntity.ExistsInGlobalRoot, worldEntityAfter.ExistsInGlobalRoot); - switch (worldEntity) + if (worldEntity.GetType() != worldEntityAfter.GetType()) + { + Assert.Fail($"Runtime type of {nameof(WorldEntity)} is not equal: {worldEntity.GetType().Name} - {worldEntityAfter.GetType().Name}"); + } + else if (worldEntity.GetType() != typeof(WorldEntity)) { - case EscapePodWorldEntity escapePodWorldEntity when worldEntityAfter is EscapePodWorldEntity escapePodWorldEntityAfter: - Assert.AreEqual(escapePodWorldEntity.Damaged, escapePodWorldEntityAfter.Damaged); - Assert.IsTrue(escapePodWorldEntity.Players.SequenceEqual(escapePodWorldEntityAfter.Players)); - break; - default: - Assert.AreEqual(worldEntity.GetType(), worldEntityAfter.GetType()); - break; + switch (worldEntity) + { + case PlaceholderGroupWorldEntity _ when worldEntityAfter is PlaceholderGroupWorldEntity _: + break; + case EscapePodWorldEntity escapePodWorldEntity when worldEntityAfter is EscapePodWorldEntity escapePodWorldEntityAfter: + Assert.AreEqual(escapePodWorldEntity.Damaged, escapePodWorldEntityAfter.Damaged); + Assert.IsTrue(escapePodWorldEntity.Players.SequenceEqual(escapePodWorldEntityAfter.Players)); + break; + case PlayerWorldEntity _ when worldEntityAfter is PlayerWorldEntity _: + break; + case VehicleWorldEntity vehicleWorldEntity when worldEntityAfter is VehicleWorldEntity vehicleWorldEntityAfter: + Assert.AreEqual(vehicleWorldEntity.SpawnerId, vehicleWorldEntityAfter.SpawnerId); + Assert.AreEqual(vehicleWorldEntity.ConstructionTime, vehicleWorldEntityAfter.ConstructionTime); + break; + case CellRootEntity _ when worldEntityAfter is CellRootEntity _: + break; + default: + Assert.Fail($"Runtime type of {nameof(WorldEntity)} is not equal even after the check: {worldEntity.GetType().Name} - {worldEntityAfter.GetType().Name}"); + break; + } } break; @@ -373,15 +404,17 @@ private static void EntityTest(Entity entity, Entity entityAfter) case InventoryItemEntity inventoryItemEntity when entityAfter is InventoryItemEntity inventoryItemEntityAfter: Assert.AreEqual(inventoryItemEntity.ClassId, inventoryItemEntityAfter.ClassId); break; - case VehicleWorldEntity vehicleWorldEntity when entityAfter is VehicleWorldEntity vehicleWorldEntityAfter: - Assert.AreEqual(vehicleWorldEntity.SpawnerId, vehicleWorldEntityAfter.SpawnerId); - Assert.AreEqual(vehicleWorldEntity.ConstructionTime, vehicleWorldEntityAfter.ConstructionTime); + case PathBasedChildEntity pathBasedChildEntity when entityAfter is PathBasedChildEntity pathBasedChildEntityAfter: + Assert.AreEqual(pathBasedChildEntity.Path, pathBasedChildEntityAfter.Path); + break; + case InstalledBatteryEntity _ when entityAfter is InstalledBatteryEntity _: break; - case PathBasedChildEntity PathBasedChildEntity when entityAfter is PathBasedChildEntity pathBasedChildEntityAfter: - Assert.AreEqual(PathBasedChildEntity.Path, pathBasedChildEntityAfter.Path); + case InstalledModuleEntity installedModuleEntity when entityAfter is InstalledModuleEntity installedModuleEntityAfter: + Assert.AreEqual(installedModuleEntity.Slot, installedModuleEntityAfter.Slot); + Assert.AreEqual(installedModuleEntity.ClassId, installedModuleEntityAfter.ClassId); break; default: - Assert.Fail($"Runtime type of {nameof(Entity)} is not equal"); + Assert.Fail($"Runtime type of {nameof(Entity)} is not equal: {entity.GetType().Name} - {entityAfter.GetType().Name}"); break; } @@ -397,130 +430,9 @@ public static void ClassCleanup() Directory.Delete(tempSaveFilePath, true); } } - - private static PersistedWorldData GeneratePersistedWorldData() - { - return new PersistedWorldData() - { - BaseData = - new BaseData() - { - CompletedBasePieceHistory = - new List() - { - new BasePiece(new NitroxId(), NitroxVector3.Zero, NitroxQuaternion.Identity, NitroxVector3.Zero, NitroxQuaternion.Identity, new NitroxTechType("BasePiece1"), Optional.Of(new NitroxId()), false, - Optional.Empty, Optional.Of(new SignMetadata("ExampleText", 1, 2, new[] { true, false }, true))) - }, - PartiallyConstructedPieces = new List() - { - new BasePiece(new NitroxId(), NitroxVector3.One, NitroxQuaternion.Identity, NitroxVector3.One, NitroxQuaternion.Identity, new NitroxTechType("BasePiece2"), Optional.Empty, false, - Optional.Of(new AnchoredFaceBuilderMetadata(new NitroxInt3(1, 2, 3), 1, 2, new NitroxInt3(0, 1, 2)))), - new BasePiece(new NitroxId(), NitroxVector3.One, NitroxQuaternion.Identity, NitroxVector3.One, NitroxQuaternion.Identity, new NitroxTechType("BasePiece2"), Optional.Empty, false, - Optional.Of(new BaseModuleBuilderMetadata(new NitroxInt3(1, 2, 3), 1))), - new BasePiece(new NitroxId(), NitroxVector3.One, NitroxQuaternion.Identity, NitroxVector3.One, NitroxQuaternion.Identity, new NitroxTechType("BasePiece2"), Optional.Empty, false, - Optional.Of(new CorridorBuilderMetadata(new NitroxVector3(1, 2, 3), 2, false, new NitroxInt3(4, 5, 6)))), - new BasePiece(new NitroxId(), NitroxVector3.One, NitroxQuaternion.Identity, NitroxVector3.One, NitroxQuaternion.Identity, new NitroxTechType("BasePiece2"), Optional.Empty, false, - Optional.Of(new MapRoomBuilderMetadata(0x20, 2))) - } - }, - EntityData = - new EntityData() - { - Entities = new List() - { - new PrefabChildEntity(new NitroxId(), "pretty class id", new NitroxTechType("Fabricator"), 1, new CrafterMetadata(new NitroxTechType("FilteredWater"), 100, 10), new NitroxId()), - new PrefabPlaceholderEntity(new NitroxId(), new NitroxTechType("Bulkhead"), new NitroxId()), - new WorldEntity(NitroxVector3.Zero, NitroxQuaternion.Identity, NitroxVector3.One, new NitroxTechType("Peeper"), 1, "PeeperClass", false, new NitroxId(), null, false, new NitroxId()), - new PlaceholderGroupWorldEntity(new WorldEntity(NitroxVector3.Zero, NitroxQuaternion.Identity, NitroxVector3.One, NitroxTechType.None, 1, "Wreck1", false, new NitroxId(), null, false, new NitroxId()), new List() - { - new PrefabPlaceholderEntity(new NitroxId(), new NitroxTechType("Door"), new NitroxId()) - }), - new EscapePodWorldEntity(NitroxVector3.One, new NitroxId(), new RepairedComponentMetadata(new NitroxTechType("Radio"))), - new InventoryEntity(1, new NitroxId(), new NitroxTechType("planterbox"), null, new NitroxId(), new List() - { - new InventoryItemEntity(new NitroxId(), "classId", new NitroxTechType("bluepalmseed"), new PlantableMetadata(0.5f), new NitroxId(), new List()) - }), - new VehicleWorldEntity(new NitroxId(), 0, null, "classId", true, new NitroxId(), new NitroxTechType("seamoth"), new CyclopsMetadata(true, true, true, true, 0, 100f)), - new PathBasedChildEntity("cyclops_lighting", new NitroxId(), NitroxTechType.None, new CyclopsLightingMetadata(true, true), new NitroxId(), new List()), - new PathBasedChildEntity("fire_extinguisher_holder", new NitroxId(), NitroxTechType.None, new FireExtinguisherHolderMetadata(true, 1), new NitroxId(), new List()) - } - }, - PlayerData = new PlayerData() - { - Players = new List() - { - new PersistedPlayerData() - { - NitroxId = new NitroxId(), - Id = 1, - Name = "Test1", - IsPermaDeath = false, - Permissions = Perms.ADMIN, - SpawnPosition = NitroxVector3.Zero, - SubRootId = null, - CurrentStats = new PlayerStatsData(45, 45, 40, 39, 28, 1), - UsedItems = new List(0), - QuickSlotsBindingIds = new NitroxId[] { new NitroxId() }, - EquippedItems = new List(0), - Modules = new List(0), - PlayerPreferences = new(new(), new()) - }, - new PersistedPlayerData() - { - NitroxId = new NitroxId(), - Id = 2, - Name = "Test2", - IsPermaDeath = true, - Permissions = Perms.PLAYER, - SpawnPosition = NitroxVector3.One, - SubRootId = new NitroxId(), - CurrentStats = new PlayerStatsData(40, 40, 30, 29, 28, 0), - UsedItems = new List { new NitroxTechType("Knife"), new NitroxTechType("Flashlight") }, - QuickSlotsBindingIds = new NitroxId[] { new NitroxId(), new NitroxId() }, - EquippedItems = new List - { - new EquippedItemData(new NitroxId(), new NitroxId(), new byte[] { 0x30, 0x40 }, "Slot3", new NitroxTechType("Flashlight")), - new EquippedItemData(new NitroxId(), new NitroxId(), new byte[] { 0x50, 0x9D }, "Slot4", new NitroxTechType("Knife")) - }, - Modules = new List() { new EquippedItemData(new NitroxId(), new NitroxId(), new byte[] { 0x35, 0xD0 }, "Module1", new NitroxTechType("Compass")) }, - PlayerPreferences = new(new() { { "eda14b58-cfe0-4a56-aa4a-47942567d897", new(0, false) }, { "Signal_Lifepod12", new(4, true) } }, new List {2, 45, 3, 1}) - } - } - }, - WorldData = new WorldData() - { - GameData = new GameData() - { - PDAState = new PDAStateData() - { - KnownTechTypes = { new NitroxTechType("Knife") }, - AnalyzedTechTypes = { new("Fragment"), new("TESSST") }, - PdaLog = { new PDALogEntry("key1", 1.1234f) }, - EncyclopediaEntries = { "TestEntry1", "TestEntry2" }, - ScannerFragments = { new("eda14b58-cfe0-4a56-aa4a-47942567d897"), new("342deb58-cfe0-4a56-aa4a-47942567d897") }, - ScannerPartial = { new(new NitroxTechType("Moonpool"), 2), new(new NitroxTechType("Fragment"), 1) }, - ScannerComplete = { new NitroxTechType("Knife1"), new NitroxTechType("Knife2"), new NitroxTechType("Knife3") } - }, - StoryGoals = new StoryGoalData() - { - CompletedGoals = { "Goal1", "Goal2" }, - GoalUnlocks = { "Goal3", "Goal4" }, - RadioQueue = { "Queue1" } - }, - StoryTiming = new StoryTimingData() - { - ElapsedSeconds = 10, - AuroraCountdownTime = 10000, - AuroraWarningTime = 20 - }, - }, - ParsedBatchCells = new List() { new NitroxInt3(10, 1, 10), new NitroxInt3(15, 4, 12) }, - Seed = "NITROXSEED" - } - }; - } } +[AttributeUsage(AttributeTargets.Method)] public class DynamicWorldDataAfterAttribute : Attribute, ITestDataSource { public IEnumerable GetData(MethodInfo methodInfo) diff --git a/NitroxClient/Communication/Packets/Processors/ExosuitArmActionProcessor.cs b/NitroxClient/Communication/Packets/Processors/ExosuitArmActionProcessor.cs index 16e7adf86d..3e8f75ac91 100644 --- a/NitroxClient/Communication/Packets/Processors/ExosuitArmActionProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/ExosuitArmActionProcessor.cs @@ -1,7 +1,9 @@ -using NitroxClient.Communication.Abstract; +using System; +using NitroxClient.Communication.Abstract; using NitroxClient.Communication.Packets.Processors.Abstract; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; +using NitroxModel_Subnautica.DataStructures; using NitroxModel.DataStructures.Util; using NitroxModel_Subnautica.Packets; using UnityEngine; @@ -38,10 +40,10 @@ public override void Process(ExosuitArmActionPacket packet) exosuitModuleEvent.UseDrill(gameObject.GetComponent(), packet.ArmAction); break; case TechType.ExosuitGrapplingArmModule: - exosuitModuleEvent.UseGrappling(gameObject.GetComponent(), packet.ArmAction, packet.OpVector); + exosuitModuleEvent.UseGrappling(gameObject.GetComponent(), packet.ArmAction, packet.OpVector?.ToUnity()); break; case TechType.ExosuitTorpedoArmModule: - exosuitModuleEvent.UseTorpedo(gameObject.GetComponent(), packet.ArmAction, packet.OpVector, packet.OpRotation); + exosuitModuleEvent.UseTorpedo(gameObject.GetComponent(), packet.ArmAction, packet.OpVector?.ToUnity(), packet.OpRotation?.ToUnity()); break; default: Log.Error($"Got an arm tech that is not handled: {packet.TechType} with action: {packet.ArmAction} for id {packet.ArmId}"); diff --git a/NitroxClient/GameLogic/Cyclops.cs b/NitroxClient/GameLogic/Cyclops.cs index 4363f56bdf..218a89f055 100644 --- a/NitroxClient/GameLogic/Cyclops.cs +++ b/NitroxClient/GameLogic/Cyclops.cs @@ -5,6 +5,7 @@ using NitroxClient.Communication.Abstract; using NitroxClient.MonoBehaviours; using NitroxClient.Unity.Helper; +using NitroxModel_Subnautica.DataStructures; using NitroxModel.DataStructures; using NitroxModel.DataStructures.Util; using NitroxModel.Packets; @@ -141,7 +142,7 @@ private void BroadcastDamageState(SubRoot subRoot, Optional info) damage.dealer != null ? NitroxEntity.GetId(damage.dealer) : null, damage.originalDamage, damage.damage, - damage.position, + damage.position.ToDto(), damage.type); } diff --git a/NitroxClient/GameLogic/ExosuitModuleEvent.cs b/NitroxClient/GameLogic/ExosuitModuleEvent.cs index 21dba2dafa..ac1b1104f9 100644 --- a/NitroxClient/GameLogic/ExosuitModuleEvent.cs +++ b/NitroxClient/GameLogic/ExosuitModuleEvent.cs @@ -1,5 +1,6 @@ using NitroxClient.Communication.Abstract; using NitroxClient.MonoBehaviours; +using NitroxModel_Subnautica.DataStructures; using NitroxModel.DataStructures; using NitroxModel_Subnautica.Packets; using UnityEngine; @@ -70,7 +71,7 @@ public void UseDrill(ExosuitDrillArm drillArm, ExosuitArmAction armAction) public void BroadcastArmAction(TechType techType, IExosuitArm exosuitArm, ExosuitArmAction armAction, Vector3? opVector, Quaternion? opRotation) { NitroxId id = NitroxEntity.GetId(exosuitArm.GetGameObject()); - ExosuitArmActionPacket packet = new ExosuitArmActionPacket(techType, id, armAction, opVector, opRotation); + ExosuitArmActionPacket packet = new ExosuitArmActionPacket(techType, id, armAction, opVector?.ToDto(), opRotation?.ToDto()); packetSender.Send(packet); } diff --git a/NitroxModel-Subnautica/DataStructures/GameLogic/Creatures/Actions/SerializableCreatureAction.cs b/NitroxModel-Subnautica/DataStructures/GameLogic/Creatures/Actions/SerializableCreatureAction.cs index a8d4f23d92..747109c7e3 100644 --- a/NitroxModel-Subnautica/DataStructures/GameLogic/Creatures/Actions/SerializableCreatureAction.cs +++ b/NitroxModel-Subnautica/DataStructures/GameLogic/Creatures/Actions/SerializableCreatureAction.cs @@ -1,4 +1,5 @@ -using UnityEngine; +using System; +using UnityEngine; namespace NitroxModel_Subnautica.DataStructures.GameLogic.Creatures.Actions { @@ -6,4 +7,11 @@ public interface SerializableCreatureAction { CreatureAction GetCreatureAction(GameObject gameObject); } + + // SerializableCreatureAction is not implemented yet but test require that at least one class inherits it + [Serializable] + public class EmptyCreatureAction: SerializableCreatureAction + { + public CreatureAction GetCreatureAction(GameObject gameObject) => null; + } } diff --git a/NitroxModel-Subnautica/DataStructures/GameLogic/CyclopsDamageInfoData.cs b/NitroxModel-Subnautica/DataStructures/GameLogic/CyclopsDamageInfoData.cs index 7d27027980..7d2a6295b9 100644 --- a/NitroxModel-Subnautica/DataStructures/GameLogic/CyclopsDamageInfoData.cs +++ b/NitroxModel-Subnautica/DataStructures/GameLogic/CyclopsDamageInfoData.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.Serialization; using NitroxModel.DataStructures; +using NitroxModel.DataStructures.Unity; using UnityEngine; namespace NitroxModel_Subnautica.DataStructures.GameLogic @@ -22,7 +23,7 @@ public class CyclopsDamageInfoData public float Damage { get; set; } [DataMember(Order = 5)] - public Vector3 Position { get; set; } + public NitroxVector3 Position { get; set; } [DataMember(Order = 6)] public DamageType Type { get; set; } @@ -32,7 +33,7 @@ protected CyclopsDamageInfoData() // Constructor for serialization. Has to be "protected" for json serialization. } - public CyclopsDamageInfoData(NitroxId receiverId, NitroxId dealerId, float originalDamage, float damage, Vector3 position, DamageType type) + public CyclopsDamageInfoData(NitroxId receiverId, NitroxId dealerId, float originalDamage, float damage, NitroxVector3 position, DamageType type) { ReceiverId = receiverId; DealerId = dealerId; diff --git a/NitroxModel-Subnautica/Packets/ExosuitArmActionPacket.cs b/NitroxModel-Subnautica/Packets/ExosuitArmActionPacket.cs index 46438a9b98..b2e9a6bb62 100644 --- a/NitroxModel-Subnautica/Packets/ExosuitArmActionPacket.cs +++ b/NitroxModel-Subnautica/Packets/ExosuitArmActionPacket.cs @@ -1,5 +1,6 @@ using System; using NitroxModel.DataStructures; +using NitroxModel.DataStructures.Unity; using NitroxModel.Packets; using UnityEngine; @@ -11,10 +12,10 @@ public class ExosuitArmActionPacket : Packet public TechType TechType { get; } public NitroxId ArmId { get; } public ExosuitArmAction ArmAction { get; } - public Vector3? OpVector { get; } - public Quaternion? OpRotation { get; } + public NitroxVector3? OpVector { get; } + public NitroxQuaternion? OpRotation { get; } - public ExosuitArmActionPacket(TechType techType, NitroxId armId, ExosuitArmAction armAction, Vector3? opVector, Quaternion? opRotation) + public ExosuitArmActionPacket(TechType techType, NitroxId armId, ExosuitArmAction armAction, NitroxVector3? opVector, NitroxQuaternion? opRotation) { TechType = techType; ArmId = armId; diff --git a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs index bc6a9f2623..b5b62be43e 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs @@ -28,6 +28,7 @@ namespace NitroxModel.DataStructures.GameLogic.Entities.Metadata [ProtoInclude(240, typeof(RocketMetadata))] [ProtoInclude(250, typeof(CyclopsLightingMetadata))] [ProtoInclude(260, typeof(FireExtinguisherHolderMetadata))] + [ProtoInclude(270, typeof(PlayerMetadata))] public abstract class EntityMetadata { } diff --git a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/PlayerMetadata.cs b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/PlayerMetadata.cs index 80eace9c5c..5a3645dc8b 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/PlayerMetadata.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/PlayerMetadata.cs @@ -21,7 +21,7 @@ protected PlayerMetadata() public PlayerMetadata(List equippedItems) { EquippedItems = equippedItems; - } + } public override string ToString() { @@ -41,6 +41,12 @@ public class EquippedItem [DataMember(Order = 3)] public NitroxTechType TechType { get; } + [IgnoreConstructor] + protected EquippedItem() + { + // Constructor for serialization. Has to be "protected" for json serialization. + } + public EquippedItem(NitroxId id, string slot, NitroxTechType techType) { Id = id; diff --git a/NitroxModel/DataStructures/GameLogic/Entity.cs b/NitroxModel/DataStructures/GameLogic/Entity.cs index 73fc6275f1..56e8772d79 100644 --- a/NitroxModel/DataStructures/GameLogic/Entity.cs +++ b/NitroxModel/DataStructures/GameLogic/Entity.cs @@ -17,6 +17,7 @@ namespace NitroxModel.DataStructures.GameLogic [ProtoInclude(90, typeof(InventoryItemEntity))] [ProtoInclude(100, typeof(PathBasedChildEntity))] [ProtoInclude(110, typeof(InstalledBatteryEntity))] + [ProtoInclude(120, typeof(InstalledModuleEntity))] public abstract class Entity { [DataMember(Order = 1)] diff --git a/NitroxModel/DataStructures/NitroxVersion.cs b/NitroxModel/DataStructures/NitroxVersion.cs index 1bb6715f70..b165916215 100644 --- a/NitroxModel/DataStructures/NitroxVersion.cs +++ b/NitroxModel/DataStructures/NitroxVersion.cs @@ -5,6 +5,7 @@ namespace NitroxModel.DataStructures; /// /// Serializable version of with only major and minor properties. /// +[Serializable] public readonly struct NitroxVersion : IComparable { public ushort Major { get; init; } diff --git a/NitroxModel/Packets/VehicleMovement.cs b/NitroxModel/Packets/VehicleMovement.cs index 3b4c96b5e1..5f1a9498ee 100644 --- a/NitroxModel/Packets/VehicleMovement.cs +++ b/NitroxModel/Packets/VehicleMovement.cs @@ -1,4 +1,5 @@ using System; +using BinaryPack.Attributes; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.Unity; using NitroxModel.Networking; @@ -10,9 +11,14 @@ public class VehicleMovement : Movement { public override ushort PlayerId { get; } public VehicleMovementData VehicleMovementData { get; } + + [IgnoredMember] public override NitroxVector3 Position => VehicleMovementData.Position; + [IgnoredMember] public override NitroxVector3 Velocity => VehicleMovementData.Velocity; + [IgnoredMember] public override NitroxQuaternion BodyRotation => VehicleMovementData.Rotation; + [IgnoredMember] public override NitroxQuaternion AimingRotation => VehicleMovementData.Rotation; public VehicleMovement(ushort playerId, VehicleMovementData vehicleMovementData) diff --git a/NitroxServer/Serialization/World/WorldPersistence.cs b/NitroxServer/Serialization/World/WorldPersistence.cs index 1c4d973fcc..48b6e072d2 100644 --- a/NitroxServer/Serialization/World/WorldPersistence.cs +++ b/NitroxServer/Serialization/World/WorldPersistence.cs @@ -53,12 +53,12 @@ internal void UpdateSerializer(ServerSerializerMode mode) Serializer = (mode == ServerSerializerMode.PROTOBUF) ? protoBufSerializer : jsonSerializer; } - public bool Save(World world, string saveDir) + public bool Save(World world, string saveDir) => Save(PersistedWorldData.From(world), saveDir); + + internal bool Save(PersistedWorldData persistedData, string saveDir) { try { - PersistedWorldData persistedData = PersistedWorldData.From(world); - if (!Directory.Exists(saveDir)) { Directory.CreateDirectory(saveDir); @@ -93,26 +93,38 @@ internal Optional LoadFromFile(string saveDir) return Optional.Empty; } - try + UpgradeSave(saveDir); + + PersistedWorldData persistedData = LoadDataFromPath(saveDir); + + if (persistedData == null) { - PersistedWorldData persistedData = new(); + return Optional.Empty; + } + World world = CreateWorld(persistedData, config.GameMode); - UpgradeSave(saveDir); + return Optional.Of(world); + } - persistedData.BaseData = Serializer.Deserialize(Path.Combine(saveDir, $"BaseData{FileEnding}")); - persistedData.PlayerData = Serializer.Deserialize(Path.Combine(saveDir, $"PlayerData{FileEnding}")); - persistedData.WorldData = Serializer.Deserialize(Path.Combine(saveDir, $"WorldData{FileEnding}")); - persistedData.EntityData = Serializer.Deserialize(Path.Combine(saveDir, $"EntityData{FileEnding}")); + internal PersistedWorldData LoadDataFromPath(string saveDir) + { + try + { + PersistedWorldData persistedData = new() + { + BaseData = Serializer.Deserialize(Path.Combine(saveDir, $"BaseData{FileEnding}")), + PlayerData = Serializer.Deserialize(Path.Combine(saveDir, $"PlayerData{FileEnding}")), + WorldData = Serializer.Deserialize(Path.Combine(saveDir, $"WorldData{FileEnding}")), + EntityData = Serializer.Deserialize(Path.Combine(saveDir, $"EntityData{FileEnding}")) + }; if (!persistedData.IsValid()) { throw new InvalidDataException("Save files are not valid"); } - World world = CreateWorld(persistedData, config.GameMode); - - return Optional.Of(world); + return persistedData; } catch (Exception ex) { @@ -128,9 +140,9 @@ internal Optional LoadFromFile(string saveDir) } } - return Optional.Empty; + return null; } - + public World Load() { Optional fileLoadedWorld = LoadFromFile(Path.Combine(WorldManager.SavesFolderDir, config.SaveName)); @@ -223,7 +235,17 @@ public World CreateWorld(PersistedWorldData pWorldData, ServerGameMode gameMode) private void UpgradeSave(string saveDir) { - SaveFileVersion saveFileVersion = Serializer.Deserialize(Path.Combine(saveDir, $"Version{FileEnding}")); + SaveFileVersion saveFileVersion; + + try + { + saveFileVersion = Serializer.Deserialize(Path.Combine(saveDir, $"Version{FileEnding}")); + } + catch (Exception ex) + { + Log.Error(ex, $"Error while upgrading save file. \"Version{FileEnding}\" couldn't be read."); + return; + } if (saveFileVersion.Version == NitroxEnvironment.Version) { From fa7cc9baeb517f327dec9189b7c4ae00a1147dc9 Mon Sep 17 00:00:00 2001 From: Jannify <23176718+Jannify@users.noreply.github.com> Date: Mon, 24 Apr 2023 22:52:58 +0200 Subject: [PATCH 3/5] Upgrade LiteNetLib to v1.0.1.1 (#2023) * Upgrade LiteNetLib to v0.9.5.2 * Upgrade to LiteNetLib 1.0.1.1 * QoL * Replace WrapperPacket for direct byte sending * Added extra NR checking in WorldPersistence * Using bitwise operation in NitroxConnectionStateExtensions --- .../DeferredPacketReceiverTest.cs | 2 +- Nitrox.sln.DotSettings | 1 + .../Communication/Abstract/IClient.cs | 1 + .../Communication/LANBroadcastClient.cs | 141 +++++++++++++ .../Communication/LANDiscoveryClient.cs | 142 ------------- .../LiteNetLib/LiteNetLibClient.cs | 154 +++++++------- NitroxClient/Communication/PacketReceiver.cs | 47 ++--- .../Gui/MainMenu/MainMenuMultiplayerPanel.cs | 6 +- NitroxClient/MonoBehaviours/Multiplayer.cs | 4 + NitroxModel/Logger/LiteNetLibLogger.cs | 30 +++ NitroxModel/Logger/Log.cs | 4 +- NitroxModel/NitroxModel.csproj | 2 +- NitroxModel/Packets/Packet.cs | 11 +- NitroxModel/Packets/WrapperPacket.cs | 23 --- NitroxServer/Communication/ConnectionState.cs | 33 ++- .../Communication/LANBroadcastServer.cs | 64 ++++++ .../Communication/LANDiscoveryServer.cs | 65 ------ .../LiteNetLib/LiteNetLibConnection.cs | 114 +++++----- .../LiteNetLib/LiteNetLibServer.cs | 195 +++++++++--------- NitroxServer/Communication/NitroxServer.cs | 4 +- .../Serialization/World/WorldPersistence.cs | 2 +- 21 files changed, 527 insertions(+), 518 deletions(-) create mode 100644 NitroxClient/Communication/LANBroadcastClient.cs delete mode 100644 NitroxClient/Communication/LANDiscoveryClient.cs create mode 100644 NitroxModel/Logger/LiteNetLibLogger.cs delete mode 100644 NitroxModel/Packets/WrapperPacket.cs create mode 100644 NitroxServer/Communication/LANBroadcastServer.cs delete mode 100644 NitroxServer/Communication/LANDiscoveryServer.cs diff --git a/Nitrox.Test/Client/Communication/DeferredPacketReceiverTest.cs b/Nitrox.Test/Client/Communication/DeferredPacketReceiverTest.cs index 6a09162188..e7ee7cc735 100644 --- a/Nitrox.Test/Client/Communication/DeferredPacketReceiverTest.cs +++ b/Nitrox.Test/Client/Communication/DeferredPacketReceiverTest.cs @@ -43,7 +43,7 @@ public void TestInitialize() public void NonActionPacket() { TestNonActionPacket packet = new TestNonActionPacket(PLAYER_ID); - packetReceiver.PacketReceived(packet, 0); + packetReceiver.PacketReceived(packet); Queue packets = packetReceiver.GetReceivedPackets(); diff --git a/Nitrox.sln.DotSettings b/Nitrox.sln.DotSettings index 97c7dfa892..9224770077 100644 --- a/Nitrox.sln.DotSettings +++ b/Nitrox.sln.DotSettings @@ -103,6 +103,7 @@ False <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /><ExtraRule Prefix="__" Suffix="" Style="aaBb" /><ExtraRule Prefix="___" Suffix="" Style="aaBb" /><ExtraRule Prefix="____" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> diff --git a/NitroxClient/Communication/Abstract/IClient.cs b/NitroxClient/Communication/Abstract/IClient.cs index 141f0979cc..2d7dd370dd 100644 --- a/NitroxClient/Communication/Abstract/IClient.cs +++ b/NitroxClient/Communication/Abstract/IClient.cs @@ -13,6 +13,7 @@ public interface IClient bool IsConnected { get; } Task StartAsync(string ipAddress, int serverPort); void Stop(); + void PollEvents(); void Send(Packet packet); } } diff --git a/NitroxClient/Communication/LANBroadcastClient.cs b/NitroxClient/Communication/LANBroadcastClient.cs new file mode 100644 index 0000000000..85c0991642 --- /dev/null +++ b/NitroxClient/Communication/LANBroadcastClient.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using LiteNetLib; +using LiteNetLib.Utils; +using NitroxModel.Constants; + +namespace NitroxClient.Communication; + +public static class LANBroadcastClient +{ + private static event Action serverFound; + public static event Action ServerFound + { + add + { + serverFound += value; + + // Trigger event for servers already found. + foreach (IPEndPoint server in discoveredServers) + { + value?.Invoke(server); + } + } + remove => serverFound -= value; + } + private static Task> lastTask; + private static ConcurrentBag discoveredServers = new(); + + public static async Task> SearchAsync(bool force = false, CancellationToken cancellationToken = default) + { + if (!force && lastTask != null) + { + return await lastTask; + } + + discoveredServers = new ConcurrentBag(); + return await (lastTask = SearchInternalAsync(cancellationToken)); + } + + private static async Task> SearchInternalAsync(CancellationToken cancellationToken = default) + { + static void ReceivedResponse(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) + { + if (messageType != UnconnectedMessageType.Broadcast) + { + return; + } + string responseString = reader.GetString(); + if (responseString != LANDiscoveryConstants.BROADCAST_RESPONSE_STRING) + { + return; + } + int serverPort = reader.GetInt(); + IPEndPoint serverEndPoint = new(remoteEndPoint.Address, serverPort); + if (discoveredServers.Contains(serverEndPoint)) + { + return; + } + + Log.Info($"Found LAN server at {serverEndPoint}."); + discoveredServers.Add(serverEndPoint); + OnServerFound(serverEndPoint); + } + + cancellationToken = cancellationToken == default ? new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token : cancellationToken; + EventBasedNetListener listener = new(); + NetManager client = new(listener) { + AutoRecycle = true, + BroadcastReceiveEnabled = true, + UnconnectedMessagesEnabled = true + }; + // Try start client on an available, predefined, port + foreach (int port in LANDiscoveryConstants.BROADCAST_PORTS) + { + if (client.Start(port)) + { + break; + } + } + if (!client.IsRunning) + { + Log.Warn("Failed to start LAN discover client: none of the defined ports are available"); + return Enumerable.Empty(); + } + + Log.Info("Searching for LAN servers..."); + listener.NetworkReceiveUnconnectedEvent += ReceivedResponse; + Task broadcastTask = Task.Run(async () => + { + while (!cancellationToken.IsCancellationRequested) + { + NetDataWriter writer = new(); + writer.Put(LANDiscoveryConstants.BROADCAST_REQUEST_STRING); + foreach (int port in LANDiscoveryConstants.BROADCAST_PORTS) + { + client.SendBroadcast(writer, port); + } + try + { + await Task.Delay(5000, cancellationToken); + } + catch (TaskCanceledException) + { + // ignore + } + } + }, cancellationToken); + Task receiveTask = Task.Run(async () => + { + while (!cancellationToken.IsCancellationRequested) + { + client.PollEvents(); + try + { + await Task.Delay(100, cancellationToken); + } + catch (TaskCanceledException) + { + // ignore + } + } + }, cancellationToken); + + await Task.WhenAll(broadcastTask, receiveTask); + // Cleanup + listener.ClearNetworkReceiveUnconnectedEvent(); + client.Stop(); + listener.NetworkReceiveUnconnectedEvent -= ReceivedResponse; + return discoveredServers; + } + + private static void OnServerFound(IPEndPoint obj) + { + serverFound?.Invoke(obj); + } +} diff --git a/NitroxClient/Communication/LANDiscoveryClient.cs b/NitroxClient/Communication/LANDiscoveryClient.cs deleted file mode 100644 index 770cb3eee1..0000000000 --- a/NitroxClient/Communication/LANDiscoveryClient.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using LiteNetLib; -using LiteNetLib.Utils; -using NitroxModel.Constants; - -namespace NitroxClient.Communication -{ - public static class LANDiscoveryClient - { - private static event Action serverFound; - public static event Action ServerFound - { - add - { - serverFound += value; - - // Trigger event for servers already found. - foreach (IPEndPoint server in discoveredServers) - { - value?.Invoke(server); - } - } - remove => serverFound -= value; - } - private static Task> lastTask; - private static ConcurrentBag discoveredServers = new(); - - public static async Task> SearchAsync(bool force = false, CancellationToken cancellationToken = default) - { - if (!force && lastTask != null) - { - return await lastTask; - } - - discoveredServers = new ConcurrentBag(); - return await (lastTask = SearchInternalAsync(cancellationToken)); - } - - private static async Task> SearchInternalAsync(CancellationToken cancellationToken = default) - { - static void ReceivedResponse(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) - { - if (messageType != UnconnectedMessageType.DiscoveryResponse) - { - return; - } - string responseString = reader.GetString(); - if (responseString != LANDiscoveryConstants.BROADCAST_RESPONSE_STRING) - { - return; - } - int serverPort = reader.GetInt(); - IPEndPoint serverEndPoint = new(remoteEndPoint.Address, serverPort); - if (discoveredServers.Contains(serverEndPoint)) - { - return; - } - - Log.Info($"Found LAN server at {serverEndPoint}."); - discoveredServers.Add(serverEndPoint); - OnServerFound(serverEndPoint); - } - - cancellationToken = cancellationToken == default ? new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token : cancellationToken; - EventBasedNetListener listener = new(); - NetManager client = new(listener) { - AutoRecycle = true, - DiscoveryEnabled = true, - UnconnectedMessagesEnabled = true - }; - // Try start client on an available, predefined, port - foreach (int port in LANDiscoveryConstants.BROADCAST_PORTS) - { - if (client.Start(port)) - { - break; - } - } - if (!client.IsRunning) - { - Log.Warn("Failed to start LAN discover client: none of the defined ports are available"); - return Enumerable.Empty(); - } - - Log.Info("Searching for LAN servers..."); - listener.NetworkReceiveUnconnectedEvent += ReceivedResponse; - Task broadcastTask = Task.Run(async () => - { - while (!cancellationToken.IsCancellationRequested) - { - NetDataWriter writer = new(); - writer.Put(LANDiscoveryConstants.BROADCAST_REQUEST_STRING); - foreach (int port in LANDiscoveryConstants.BROADCAST_PORTS) - { - client.SendDiscoveryRequest(writer, port); - } - try - { - await Task.Delay(5000, cancellationToken); - } - catch (TaskCanceledException) - { - // ignore - } - } - }, cancellationToken); - Task receiveTask = Task.Run(async () => - { - while (!cancellationToken.IsCancellationRequested) - { - client.PollEvents(); - try - { - await Task.Delay(100, cancellationToken); - } - catch (TaskCanceledException) - { - // ignore - } - } - }, cancellationToken); - - await Task.WhenAll(broadcastTask, receiveTask); - // Cleanup - listener.ClearNetworkReceiveUnconnectedEvent(); - client.Stop(); - listener.NetworkReceiveUnconnectedEvent -= ReceivedResponse; - return discoveredServers; - } - - private static void OnServerFound(IPEndPoint obj) - { - serverFound?.Invoke(obj); - } - } -} diff --git a/NitroxClient/Communication/NetworkingLayer/LiteNetLib/LiteNetLibClient.cs b/NitroxClient/Communication/NetworkingLayer/LiteNetLib/LiteNetLibClient.cs index 5f56bac0a0..f4f688fe90 100644 --- a/NitroxClient/Communication/NetworkingLayer/LiteNetLib/LiteNetLibClient.cs +++ b/NitroxClient/Communication/NetworkingLayer/LiteNetLib/LiteNetLibClient.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; using LiteNetLib; using LiteNetLib.Utils; @@ -9,100 +9,104 @@ using NitroxModel.Networking; using NitroxModel.Packets; -namespace NitroxClient.Communication.NetworkingLayer.LiteNetLib -{ - public class LiteNetLibClient : IClient - { - public bool IsConnected { get; private set; } +namespace NitroxClient.Communication.NetworkingLayer.LiteNetLib; - private readonly NetPacketProcessor netPacketProcessor = new NetPacketProcessor(); - private readonly AutoResetEvent connectedEvent = new AutoResetEvent(false); - private readonly PacketReceiver packetReceiver; - private readonly INetworkDebugger networkDebugger; +public class LiteNetLibClient : IClient +{ + public bool IsConnected { get; private set; } - private NetManager client; + private readonly AutoResetEvent connectedEvent = new(false); + private readonly NetDataWriter dataWriter = new(); + private readonly PacketReceiver packetReceiver; + private readonly INetworkDebugger networkDebugger; - public LiteNetLibClient(PacketReceiver packetReceiver, INetworkDebugger networkDebugger = null) - { - this.packetReceiver = packetReceiver; - this.networkDebugger = networkDebugger; - } + private NetManager client; - public async Task StartAsync(string ipAddress, int serverPort) - { - Log.Info("Initializing LiteNetLibClient..."); + public LiteNetLibClient(PacketReceiver packetReceiver, INetworkDebugger networkDebugger = null) + { + this.packetReceiver = packetReceiver; + this.networkDebugger = networkDebugger; + } - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + public async Task StartAsync(string ipAddress, int serverPort) + { + Log.Info("Initializing LiteNetLibClient..."); - netPacketProcessor.SubscribeReusable(OnPacketReceived); + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - EventBasedNetListener listener = new EventBasedNetListener(); - listener.PeerConnectedEvent += Connected; - listener.PeerDisconnectedEvent += Disconnected; - listener.NetworkReceiveEvent += ReceivedNetworkData; + EventBasedNetListener listener = new EventBasedNetListener(); + listener.PeerConnectedEvent += Connected; + listener.PeerDisconnectedEvent += Disconnected; + listener.NetworkReceiveEvent += ReceivedNetworkData; - client = new NetManager(listener) - { - UpdateTime = 15, - UnsyncedEvents = true, //experimental feature, may need to replace with calls to client.PollEvents(); + client = new NetManager(listener) + { + UpdateTime = 15, #if DEBUG - DisconnectTimeout = 300000 //Disables Timeout (for 5 min) for debug purpose (like if you jump though the server code) + DisconnectTimeout = 300000 //Disables Timeout (for 5 min) for debug purpose (like if you jump though the server code) #endif - }; + }; - await Task.Run(() => - { - client.Start(); - client.Connect(ipAddress, serverPort, "nitrox"); - }); + await Task.Run(() => + { + client.Start(); + client.Connect(ipAddress, serverPort, "nitrox"); + }); - connectedEvent.WaitOne(2000); - connectedEvent.Reset(); - } + connectedEvent.WaitOne(2000); + connectedEvent.Reset(); + } - public void Send(Packet packet) - { - byte[] bytes = netPacketProcessor.Write(packet.ToWrapperPacket()); + public void Send(Packet packet) + { + byte[] packetData = packet.Serialize(); + dataWriter.Reset(); + dataWriter.Put(packetData.Length); + dataWriter.Put(packetData); - networkDebugger?.PacketSent(packet, bytes.Length); - client.SendToAll(bytes, NitroxDeliveryMethod.ToLiteNetLib(packet.DeliveryMethod)); - client.Flush(); - } + networkDebugger?.PacketSent(packet, dataWriter.Length); + client.SendToAll(dataWriter, NitroxDeliveryMethod.ToLiteNetLib(packet.DeliveryMethod)); + } - public void Stop() - { - IsConnected = false; - client.Stop(); - } + public void Stop() + { + IsConnected = false; + client.Stop(); + } - private void ReceivedNetworkData(NetPeer peer, NetDataReader reader, DeliveryMethod deliveryMethod) - { - netPacketProcessor.ReadAllPackets(reader, peer); - } + /// + /// This should be called once each game tick + /// + public void PollEvents() => client.PollEvents(); - private void OnPacketReceived(WrapperPacket wrapperPacket, NetPeer peer) - { - Packet packet = Packet.Deserialize(wrapperPacket.packetData); - packetReceiver.PacketReceived(packet, wrapperPacket.packetData.Length); - } + private void ReceivedNetworkData(NetPeer peer, NetDataReader reader, byte channel, DeliveryMethod deliveryMethod) + { + int packetDataLength = reader.GetInt(); + byte[] packetData = new byte[packetDataLength]; + reader.GetBytes(packetData, packetDataLength); - private void Connected(NetPeer peer) - { - // IsConnected must happen before Set() so that its state is noticed WHEN we unblock the thread (cf. connectedEvent.WaitOne(...)) - IsConnected = true; - connectedEvent.Set(); - Log.Info("Connected to server"); - } + Packet packet = Packet.Deserialize(packetData); + packetReceiver.PacketReceived(packet); + networkDebugger?.PacketReceived(packet, packetDataLength); + } + + private void Connected(NetPeer peer) + { + // IsConnected must happen before Set() so that its state is noticed WHEN we unblock the thread (cf. connectedEvent.WaitOne(...)) + IsConnected = true; + connectedEvent.Set(); + Log.Info("Connected to server"); + } - private void Disconnected(NetPeer peer, DisconnectInfo disconnectInfo) + private void Disconnected(NetPeer peer, DisconnectInfo disconnectInfo) + { + // Check must happen before IsConnected is set to false, so that it doesn't send an exception when we aren't even ingame + if (Multiplayer.Active) { - // Check must happen before IsConnected is set to false, so that it doesn't send an exception when we aren't even ingame - if (Multiplayer.Active) - { - Modal.Get()?.Show(); - } - IsConnected = false; - Log.Info("Disconnected from server"); + Modal.Get()?.Show(); } + + IsConnected = false; + Log.Info("Disconnected from server"); } } diff --git a/NitroxClient/Communication/PacketReceiver.cs b/NitroxClient/Communication/PacketReceiver.cs index 805ccc223e..545ea13a8f 100644 --- a/NitroxClient/Communication/PacketReceiver.cs +++ b/NitroxClient/Communication/PacketReceiver.cs @@ -1,43 +1,34 @@ using System.Collections.Generic; -using NitroxClient.Debuggers; using NitroxModel.Packets; -namespace NitroxClient.Communication +namespace NitroxClient.Communication; + +// TODO: Spinlocks don't seem to be necessary here, but I don't know for certain. +public class PacketReceiver { - // TODO: Spinlocks don't seem to be necessary here, but I don't know for certain. - public class PacketReceiver - { - private readonly INetworkDebugger networkDebugger; - private readonly Queue receivedPackets; + private readonly object receivedPacketsLock = new(); + private readonly Queue receivedPackets = new(); - public PacketReceiver(INetworkDebugger networkDebugger = null) + public void PacketReceived(Packet packet) + { + lock (receivedPacketsLock) { - receivedPackets = new Queue(); - this.networkDebugger = networkDebugger; + receivedPackets.Enqueue(packet); } + } - public void PacketReceived(Packet packet, int byteSize) - { - lock (receivedPackets) - { - networkDebugger?.PacketReceived(packet, byteSize); - receivedPackets.Enqueue(packet); - } - } + public Queue GetReceivedPackets() + { + Queue packets = new(); - public Queue GetReceivedPackets() + lock (receivedPacketsLock) { - Queue packets = new Queue(); - - lock (receivedPackets) + while (receivedPackets.Count > 0) { - while (receivedPackets.Count > 0) - { - packets.Enqueue(receivedPackets.Dequeue()); - } + packets.Enqueue(receivedPackets.Dequeue()); } - - return packets; } + + return packets; } } diff --git a/NitroxClient/MonoBehaviours/Gui/MainMenu/MainMenuMultiplayerPanel.cs b/NitroxClient/MonoBehaviours/Gui/MainMenu/MainMenuMultiplayerPanel.cs index a982a0c511..3725594db2 100644 --- a/NitroxClient/MonoBehaviours/Gui/MainMenu/MainMenuMultiplayerPanel.cs +++ b/NitroxClient/MonoBehaviours/Gui/MainMenu/MainMenuMultiplayerPanel.cs @@ -168,9 +168,9 @@ void AddButton(IPEndPoint serverEndPoint) CreateServerButton(MakeButtonText("LAN Server", serverEndPoint.Address, serverEndPoint.Port), $"{serverEndPoint.Address}", $"{serverEndPoint.Port}", true); } - LANDiscoveryClient.ServerFound += AddButton; - await LANDiscoveryClient.SearchAsync(); - LANDiscoveryClient.ServerFound -= AddButton; + LANBroadcastClient.ServerFound += AddButton; + await LANBroadcastClient.SearchAsync(); + LANBroadcastClient.ServerFound -= AddButton; } private string MakeButtonText(string serverName, object address, object port) diff --git a/NitroxClient/MonoBehaviours/Multiplayer.cs b/NitroxClient/MonoBehaviours/Multiplayer.cs index a003f2f4ee..e8da392905 100644 --- a/NitroxClient/MonoBehaviours/Multiplayer.cs +++ b/NitroxClient/MonoBehaviours/Multiplayer.cs @@ -25,6 +25,7 @@ public class Multiplayer : MonoBehaviour { public static Multiplayer Main; private readonly Dictionary packetProcessorCache = new(); + private IClient client; private IMultiplayerSession multiplayerSession; private PacketReceiver packetReceiver; private ThrottledPacketSender throttledPacketSender; @@ -40,6 +41,7 @@ public class Multiplayer : MonoBehaviour public void Awake() { NitroxServiceLocator.LifetimeScopeEnded += (_, _) => packetProcessorCache.Clear(); + client = NitroxServiceLocator.LocateService(); multiplayerSession = NitroxServiceLocator.LocateService(); packetReceiver = NitroxServiceLocator.LocateService(); throttledPacketSender = NitroxServiceLocator.LocateService(); @@ -53,6 +55,8 @@ public void Awake() public void Update() { + client.PollEvents(); + if (multiplayerSession.CurrentState.CurrentStage != MultiplayerSessionConnectionStage.DISCONNECTED) { ProcessPackets(); diff --git a/NitroxModel/Logger/LiteNetLibLogger.cs b/NitroxModel/Logger/LiteNetLibLogger.cs new file mode 100644 index 0000000000..5cff379bb7 --- /dev/null +++ b/NitroxModel/Logger/LiteNetLibLogger.cs @@ -0,0 +1,30 @@ +using System; +using LiteNetLib; + +namespace NitroxModel.Logger; + +public class LiteNetLibLogger : INetLogger +{ + public void WriteNet(NetLogLevel level, string str, params object[] args) + { + string message = $"[LiteNetLib] {string.Format(str, args)}"; + + switch (level) + { + case NetLogLevel.Error: + Log.Error(message); + break; + case NetLogLevel.Warning: + Log.Warn(message); + break; + case NetLogLevel.Info: + Log.Info(message); + break; + case NetLogLevel.Trace: + Log.Debug(message); + break; + default: + throw new ArgumentOutOfRangeException(nameof(level), level, string.Empty); + } + } +} diff --git a/NitroxModel/Logger/Log.cs b/NitroxModel/Logger/Log.cs index f595a7611c..04a0e7a4e2 100644 --- a/NitroxModel/Logger/Log.cs +++ b/NitroxModel/Logger/Log.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Threading; +using LiteNetLib; using NitroxModel.Helper; using Serilog; using Serilog.Context; @@ -37,7 +38,8 @@ public static void Setup(bool asyncConsoleWriter = false, InGameLogger inGameLog return; } isSetup = true; - + NetDebug.Logger = new LiteNetLibLogger(); + PlayerName = ""; logger = new LoggerConfiguration() .MinimumLevel.Debug() diff --git a/NitroxModel/NitroxModel.csproj b/NitroxModel/NitroxModel.csproj index d886a3a06a..47e216adf9 100644 --- a/NitroxModel/NitroxModel.csproj +++ b/NitroxModel/NitroxModel.csproj @@ -13,7 +13,7 @@ - + diff --git a/NitroxModel/Packets/Packet.cs b/NitroxModel/Packets/Packet.cs index d5f5ea5598..b49142aa3f 100644 --- a/NitroxModel/Packets/Packet.cs +++ b/NitroxModel/Packets/Packet.cs @@ -36,10 +36,10 @@ static IEnumerable FindTypesInModelAssemblies() } }); } - + static IEnumerable FindUnionBaseTypes() => FindTypesInModelAssemblies() .Where(t => t.IsAbstract && !t.IsSealed && (!t.BaseType?.IsAbstract ?? true) && !t.ContainsGenericParameters); - + lock (lockObject) { foreach (Type type in FindUnionBaseTypes()) @@ -92,11 +92,6 @@ public static Packet Deserialize(byte[] data) return BinaryConverter.Deserialize(data).Packet; } - public WrapperPacket ToWrapperPacket() - { - return new WrapperPacket(Serialize()); - } - public override string ToString() { Type packetType = GetType(); @@ -124,7 +119,7 @@ public override string ToString() } toStringBuilder.Remove(toStringBuilder.Length - 2, 2); - toStringBuilder.Append("]"); + toStringBuilder.Append(']'); return toStringBuilder.ToString(); } diff --git a/NitroxModel/Packets/WrapperPacket.cs b/NitroxModel/Packets/WrapperPacket.cs deleted file mode 100644 index 98bb1e5a57..0000000000 --- a/NitroxModel/Packets/WrapperPacket.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace NitroxModel.Packets -{ - /** - * WrapperPacket for LiteNetLib implementation - * Because of the LiteNetLib serializer we can't deserialize the incoming bytes and have - * to use their serializer. This packet is the bridge between the Nitrox packet serializer - * and the LiteNetLib serializer. In the LiteNetLib implementation, every packet is wrapped - * and then uses Nitrox serializer with the packetData. - */ - public class WrapperPacket - { - public byte[] packetData { get; set; } - - public WrapperPacket() - { - } - - public WrapperPacket(byte[] packetData) - { - this.packetData = packetData; - } - } -} diff --git a/NitroxServer/Communication/ConnectionState.cs b/NitroxServer/Communication/ConnectionState.cs index f5271d980b..3b567f5f86 100644 --- a/NitroxServer/Communication/ConnectionState.cs +++ b/NitroxServer/Communication/ConnectionState.cs @@ -1,11 +1,30 @@ -namespace NitroxServer.Communication +using LiteNetLib; + +namespace NitroxServer.Communication; + +public enum NitroxConnectionState { - public enum NitroxConnectionState + Unknown, + Disconnected, + Connected, + Reserved, + InGame +} + +public static class NitroxConnectionStateExtensions +{ + public static NitroxConnectionState ToNitrox(this ConnectionState connectionState) { - Unknown, - Disconnected, - Connected, - Reserved, - InGame + if ((connectionState & ConnectionState.Connected) == ConnectionState.Connected) + { + return NitroxConnectionState.Connected; + } + + if ((connectionState & ConnectionState.Disconnected) == ConnectionState.Disconnected) + { + return NitroxConnectionState.Disconnected; + } + + return NitroxConnectionState.Unknown; } } diff --git a/NitroxServer/Communication/LANBroadcastServer.cs b/NitroxServer/Communication/LANBroadcastServer.cs new file mode 100644 index 0000000000..b6f7d0727f --- /dev/null +++ b/NitroxServer/Communication/LANBroadcastServer.cs @@ -0,0 +1,64 @@ +using System.Net; +using System.Threading; +using LiteNetLib; +using LiteNetLib.Utils; +using NitroxModel.Constants; + +namespace NitroxServer.Communication; + +public static class LANBroadcastServer +{ + private static NetManager server; + private static EventBasedNetListener listener; + + private static Timer pollTimer; + + public static void Start() + { + listener = new EventBasedNetListener(); + server = new NetManager(listener); + + server.AutoRecycle = true; + server.BroadcastReceiveEnabled = true; + server.UnconnectedMessagesEnabled = true; + + for (int i = 0; i < LANDiscoveryConstants.BROADCAST_PORTS.Length; i++) + { + if (server.Start(LANDiscoveryConstants.BROADCAST_PORTS[i])) + { + break; + } + } + + listener.NetworkReceiveUnconnectedEvent += NetworkReceiveUnconnected; + + pollTimer = new Timer((state) => + { + server.PollEvents(); + }); + pollTimer.Change(0, 100); + } + + private static void NetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) + { + if (messageType == UnconnectedMessageType.Broadcast) + { + string requestString = reader.GetString(); + if (requestString == LANDiscoveryConstants.BROADCAST_REQUEST_STRING) + { + NetDataWriter writer = new(); + writer.Put(LANDiscoveryConstants.BROADCAST_RESPONSE_STRING); + writer.Put(Server.Instance.Port); + + server.SendBroadcast(writer, remoteEndPoint.Port); + } + } + } + + public static void Stop() + { + listener?.ClearNetworkReceiveUnconnectedEvent(); + server?.Stop(); + pollTimer?.Dispose(); + } +} diff --git a/NitroxServer/Communication/LANDiscoveryServer.cs b/NitroxServer/Communication/LANDiscoveryServer.cs deleted file mode 100644 index 695ebde415..0000000000 --- a/NitroxServer/Communication/LANDiscoveryServer.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Net; -using System.Threading; -using LiteNetLib; -using LiteNetLib.Utils; -using NitroxModel.Constants; - -namespace NitroxServer.Communication -{ - public static class LANDiscoveryServer - { - private static NetManager server; - private static EventBasedNetListener listener; - - private static Timer pollTimer; - - public static void Start() - { - listener = new EventBasedNetListener(); - server = new NetManager(listener); - - server.AutoRecycle = true; - server.DiscoveryEnabled = true; - server.UnconnectedMessagesEnabled = true; - - for (int i = 0; i < LANDiscoveryConstants.BROADCAST_PORTS.Length; i++) - { - if (server.Start(LANDiscoveryConstants.BROADCAST_PORTS[i])) - { - break; - } - } - - listener.NetworkReceiveUnconnectedEvent += NetworkReceiveUnconnected; - - pollTimer = new Timer((state) => - { - server.PollEvents(); - }); - pollTimer.Change(0, 100); - } - - private static void NetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) - { - if (messageType == UnconnectedMessageType.DiscoveryRequest) - { - string requestString = reader.GetString(); - if (requestString == LANDiscoveryConstants.BROADCAST_REQUEST_STRING) - { - NetDataWriter writer = new(); - writer.Put(LANDiscoveryConstants.BROADCAST_RESPONSE_STRING); - writer.Put(Server.Instance.Port); - - server.SendDiscoveryResponse(writer, remoteEndPoint); - } - } - } - - public static void Stop() - { - listener?.ClearNetworkReceiveUnconnectedEvent(); - server?.Stop(); - pollTimer?.Dispose(); - } - } -} diff --git a/NitroxServer/Communication/LiteNetLib/LiteNetLibConnection.cs b/NitroxServer/Communication/LiteNetLib/LiteNetLibConnection.cs index 1818433070..9e174aed01 100644 --- a/NitroxServer/Communication/LiteNetLib/LiteNetLibConnection.cs +++ b/NitroxServer/Communication/LiteNetLib/LiteNetLibConnection.cs @@ -1,89 +1,79 @@ -using System.Net; +using System; +using System.Net; using LiteNetLib; using LiteNetLib.Utils; using NitroxModel.Networking; using NitroxModel.Packets; -namespace NitroxServer.Communication.LiteNetLib -{ - public class LiteNetLibConnection : NitroxConnection - { - private readonly NetPacketProcessor netPacketProcessor = new(); - private readonly NetPeer peer; +namespace NitroxServer.Communication.LiteNetLib; - public IPEndPoint Endpoint => peer.EndPoint; - public NitroxConnectionState State => MapConnectionState(peer.ConnectionState); +public class LiteNetLibConnection : NitroxConnection, IEquatable +{ + private readonly NetDataWriter dataWriter = new(); + private readonly NetPeer peer; - private NitroxConnectionState MapConnectionState(ConnectionState connectionState) - { - NitroxConnectionState state = NitroxConnectionState.Unknown; + public IPEndPoint Endpoint => peer.EndPoint; + public NitroxConnectionState State => peer.ConnectionState.ToNitrox(); - if (connectionState.HasFlag(ConnectionState.Connected)) - { - state = NitroxConnectionState.Connected; - } + public LiteNetLibConnection(NetPeer peer) + { + this.peer = peer; + } - if (connectionState.HasFlag(ConnectionState.Disconnected)) - { - state = NitroxConnectionState.Disconnected; - } + public void SendPacket(Packet packet) + { + if (peer.ConnectionState == ConnectionState.Connected) + { + byte[] packetData = packet.Serialize(); + dataWriter.Reset(); + dataWriter.Put(packetData.Length); + dataWriter.Put(packetData); - return state; + peer.Send(dataWriter, NitroxDeliveryMethod.ToLiteNetLib(packet.DeliveryMethod)); } - - public LiteNetLibConnection(NetPeer peer) + else { - this.peer = peer; + Log.Warn($"Cannot send packet {packet?.GetType()} to a closed connection {peer.EndPoint}"); } + } - public static bool operator ==(LiteNetLibConnection left, LiteNetLibConnection right) - { - return Equals(left, right); - } + public static bool operator ==(LiteNetLibConnection left, LiteNetLibConnection right) + { + return Equals(left, right); + } - public static bool operator !=(LiteNetLibConnection left, LiteNetLibConnection right) - { - return !Equals(left, right); - } + public static bool operator !=(LiteNetLibConnection left, LiteNetLibConnection right) + { + return !Equals(left, right); + } - public override bool Equals(object obj) + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, obj)) - { - return false; - } - if (ReferenceEquals(this, obj)) - { - return true; - } - if (obj.GetType() != GetType()) - { - return false; - } - return Equals((LiteNetLibConnection)obj); + return false; } - public override int GetHashCode() + if (ReferenceEquals(this, obj)) { - return peer?.Id.GetHashCode() ?? 0; + return true; } - public void SendPacket(Packet packet) + if (obj.GetType() != GetType()) { - if (peer.ConnectionState == ConnectionState.Connected) - { - peer.Send(netPacketProcessor.Write(packet.ToWrapperPacket()), NitroxDeliveryMethod.ToLiteNetLib(packet.DeliveryMethod)); - peer.Flush(); - } - else - { - Log.Warn($"Cannot send packet {packet?.GetType()} to a closed connection {peer?.EndPoint}"); - } + return false; } - protected bool Equals(LiteNetLibConnection other) - { - return peer?.Id == other.peer?.Id; - } + return Equals((LiteNetLibConnection)obj); + } + + public override int GetHashCode() + { + return peer?.Id.GetHashCode() ?? 0; + } + + public bool Equals(LiteNetLibConnection other) + { + return peer?.Id == other?.peer?.Id; } } diff --git a/NitroxServer/Communication/LiteNetLib/LiteNetLibServer.cs b/NitroxServer/Communication/LiteNetLib/LiteNetLibServer.cs index b676931483..d92553cf51 100644 --- a/NitroxServer/Communication/LiteNetLib/LiteNetLibServer.cs +++ b/NitroxServer/Communication/LiteNetLib/LiteNetLibServer.cs @@ -10,136 +10,133 @@ using NitroxServer.GameLogic.Entities; using NitroxServer.Serialization; -namespace NitroxServer.Communication.LiteNetLib +namespace NitroxServer.Communication.LiteNetLib; + +public class LiteNetLibServer : NitroxServer { - public class LiteNetLibServer : NitroxServer + private readonly EventBasedNetListener listener; + private readonly NetManager server; + + public LiteNetLibServer(PacketHandler packetHandler, PlayerManager playerManager, EntitySimulation entitySimulation, ServerConfig serverConfig) : base(packetHandler, playerManager, entitySimulation, serverConfig) { - private readonly EventBasedNetListener listener; - private readonly NetPacketProcessor netPacketProcessor = new(); - private readonly NetManager server; + listener = new EventBasedNetListener(); + server = new NetManager(listener); + } - public LiteNetLibServer(PacketHandler packetHandler, PlayerManager playerManager, EntitySimulation entitySimulation, ServerConfig serverConfig) : base(packetHandler, playerManager, entitySimulation, serverConfig) + public override bool Start() + { + listener.PeerConnectedEvent += PeerConnected; + listener.PeerDisconnectedEvent += PeerDisconnected; + listener.NetworkReceiveEvent += NetworkDataReceived; + listener.ConnectionRequestEvent += OnConnectionRequest; + + server.BroadcastReceiveEnabled = true; + server.UnconnectedMessagesEnabled = true; + server.UpdateTime = 15; + server.UnsyncedEvents = true; +#if DEBUG + server.DisconnectTimeout = 300000; //Disables Timeout (for 5 min) for debug purpose (like if you jump though the server code) +#endif + + if (!server.Start(portNumber)) { - netPacketProcessor.SubscribeReusable(OnPacketReceived); - listener = new EventBasedNetListener(); - server = new NetManager(listener); + return false; } - public override bool Start() + if (useUpnpPortForwarding) { - listener.PeerConnectedEvent += PeerConnected; - listener.PeerDisconnectedEvent += PeerDisconnected; - listener.NetworkReceiveEvent += NetworkDataReceived; - listener.ConnectionRequestEvent += OnConnectionRequest; - - server.DiscoveryEnabled = true; - server.UnconnectedMessagesEnabled = true; - server.UpdateTime = 15; - server.UnsyncedEvents = true; -#if DEBUG - server.DisconnectTimeout = 300000; //Disables Timeout (for 5 min) for debug purpose (like if you jump though the server code) -#endif - - if (!server.Start(portNumber)) - { - return false; - } - - if (useUpnpPortForwarding) - { - PortForwardAsync((ushort)portNumber).ConfigureAwait(false); - } - - if (useLANDiscovery) - { - LANDiscoveryServer.Start(); - } - - return true; + PortForwardAsync((ushort)portNumber).ConfigureAwait(false); } - private async Task PortForwardAsync(ushort port) + if (useLANBroadcast) { - if (await NatHelper.GetPortMappingAsync(port, Protocol.Udp) != null) - { - Log.Info($"Port {port} UDP is already port forwarded"); - return; - } - - bool isMapped = await NatHelper.AddPortMappingAsync(port, Protocol.Udp); - if (isMapped) - { - Log.Info($"Server port {port} UDP has been automatically opened on your router (port is closed when server closes)"); - } - else - { - Log.Warn($"Failed to automatically port forward {port} UDP through UPnP. If using Hamachi or manually port-forwarding, please disregard this warning. To disable this feature you can go into the server settings."); - } + LANBroadcastServer.Start(); } - public override void Stop() + return true; + } + + private async Task PortForwardAsync(ushort port) + { + if (await NatHelper.GetPortMappingAsync(port, Protocol.Udp) != null) { - playerManager.SendPacketToAllPlayers(new ServerStopped()); - // We want every player to receive this packet - Thread.Sleep(500); - server.Stop(); - if (useUpnpPortForwarding) - { - NatHelper.DeletePortMappingAsync((ushort)portNumber, Protocol.Udp).ConfigureAwait(false).GetAwaiter().GetResult(); - } - - if (useLANDiscovery) - { - LANDiscoveryServer.Stop(); - } + Log.Info($"Port {port} UDP is already port forwarded"); + return; } - public void OnConnectionRequest(ConnectionRequest request) + bool isMapped = await NatHelper.AddPortMappingAsync(port, Protocol.Udp); + if (isMapped) { - if (server.PeersCount < maxConnections) - { - request.AcceptIfKey("nitrox"); - } - else - { - request.Reject(); - } + Log.Info($"Server port {port} UDP has been automatically opened on your router (port is closed when server closes)"); } + else + { + Log.Warn($"Failed to automatically port forward {port} UDP through UPnP. If using Hamachi or manually port-forwarding, please disregard this warning. To disable this feature you can go into the server settings."); + } + } - private void PeerConnected(NetPeer peer) + public override void Stop() + { + playerManager.SendPacketToAllPlayers(new ServerStopped()); + // We want every player to receive this packet + Thread.Sleep(500); + server.Stop(); + if (useUpnpPortForwarding) { - LiteNetLibConnection connection = new(peer); - lock (connectionsByRemoteIdentifier) - { - connectionsByRemoteIdentifier[peer.Id] = connection; - } + NatHelper.DeletePortMappingAsync((ushort)portNumber, Protocol.Udp).ConfigureAwait(false).GetAwaiter().GetResult(); } - private void PeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + if (useLANBroadcast) { - ClientDisconnected(GetConnection(peer.Id)); + LANBroadcastServer.Stop(); } + } - private void NetworkDataReceived(NetPeer peer, NetDataReader reader, DeliveryMethod deliveryMethod) + public void OnConnectionRequest(ConnectionRequest request) + { + if (server.ConnectedPeersCount < maxConnections) + { + request.AcceptIfKey("nitrox"); + } + else { - netPacketProcessor.ReadAllPackets(reader, peer); + request.Reject(); } + } - private void OnPacketReceived(WrapperPacket wrapperPacket, NetPeer peer) + private void PeerConnected(NetPeer peer) + { + LiteNetLibConnection connection = new(peer); + lock (connectionsByRemoteIdentifier) { - NitroxConnection connection = GetConnection(peer.Id); - Packet packet = Packet.Deserialize(wrapperPacket.packetData); - ProcessIncomingData(connection, packet); + connectionsByRemoteIdentifier[peer.Id] = connection; } + } + + private void PeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + { + ClientDisconnected(GetConnection(peer.Id)); + } + + private void NetworkDataReceived(NetPeer peer, NetDataReader reader, byte channel, DeliveryMethod deliveryMethod) + { + int packetDataLength = reader.GetInt(); + byte[] packetData = new byte[packetDataLength]; + reader.GetBytes(packetData, packetDataLength); - private NitroxConnection GetConnection(int remoteIdentifier) + Packet packet = Packet.Deserialize(packetData); + NitroxConnection connection = GetConnection(peer.Id); + ProcessIncomingData(connection, packet); + } + + private NitroxConnection GetConnection(int remoteIdentifier) + { + NitroxConnection connection; + lock (connectionsByRemoteIdentifier) { - NitroxConnection connection; - lock (connectionsByRemoteIdentifier) - { - connectionsByRemoteIdentifier.TryGetValue(remoteIdentifier, out connection); - } - return connection; + connectionsByRemoteIdentifier.TryGetValue(remoteIdentifier, out connection); } + + return connection; } } diff --git a/NitroxServer/Communication/NitroxServer.cs b/NitroxServer/Communication/NitroxServer.cs index 2828ba0cbc..cac7bc4581 100644 --- a/NitroxServer/Communication/NitroxServer.cs +++ b/NitroxServer/Communication/NitroxServer.cs @@ -19,7 +19,7 @@ static NitroxServer() protected readonly int portNumber; protected readonly int maxConnections; protected readonly bool useUpnpPortForwarding; - protected readonly bool useLANDiscovery; + protected readonly bool useLANBroadcast; protected readonly PacketHandler packetHandler; protected readonly EntitySimulation entitySimulation; @@ -35,7 +35,7 @@ public NitroxServer(PacketHandler packetHandler, PlayerManager playerManager, En portNumber = serverConfig.ServerPort; maxConnections = serverConfig.MaxConnections; useUpnpPortForwarding = serverConfig.AutoPortForward; - useLANDiscovery = serverConfig.LANDiscoveryEnabled; + useLANBroadcast = serverConfig.LANDiscoveryEnabled; } public abstract bool Start(); diff --git a/NitroxServer/Serialization/World/WorldPersistence.cs b/NitroxServer/Serialization/World/WorldPersistence.cs index 48b6e072d2..96415250c3 100644 --- a/NitroxServer/Serialization/World/WorldPersistence.cs +++ b/NitroxServer/Serialization/World/WorldPersistence.cs @@ -247,7 +247,7 @@ private void UpgradeSave(string saveDir) return; } - if (saveFileVersion.Version == NitroxEnvironment.Version) + if (saveFileVersion == null || saveFileVersion.Version == NitroxEnvironment.Version) { return; } From 3881aa14bcbb513effd49be11f0d71260d039f38 Mon Sep 17 00:00:00 2001 From: Measurity Date: Wed, 26 Apr 2023 13:04:55 +0200 Subject: [PATCH 4/5] Added error for Subnautica legacy branch on master --- Nitrox.BuildTool/Program.cs | 47 ++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/Nitrox.BuildTool/Program.cs b/Nitrox.BuildTool/Program.cs index 9752c7c959..07e728cff0 100644 --- a/Nitrox.BuildTool/Program.cs +++ b/Nitrox.BuildTool/Program.cs @@ -22,20 +22,21 @@ public static class Program public static string GeneratedOutputDir => Path.Combine(ProcessDir, "generated_files"); + private const int LEGACY_BRANCH_SUBNAUTICA_VERSION = 68598; + public static async Task Main(string[] args) { + AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(eventArgs.ExceptionObject); - Console.ResetColor(); - + LogError(eventArgs.ExceptionObject.ToString()); Exit((eventArgs.ExceptionObject as Exception)?.HResult ?? 1); }; - GameInstallData game = await Task.Factory.StartNew(EnsureGame).ConfigureAwait(false); + GameInstallData game = await Task.Run(EnsureGame); Console.WriteLine($"Found game at {game.InstallDir}"); - await EnsurePublicizedAssembliesAsync(game).ConfigureAwait(false); + AbortIfInvalidGameVersion(game); + await EnsurePublicizedAssembliesAsync(game); Exit(); } @@ -48,6 +49,40 @@ private static void Exit(int exitCode = 0) Environment.Exit(exitCode); } + private static void LogError(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + } + + private static void AbortIfInvalidGameVersion(GameInstallData game) + { + string gameVersionFile = Path.Combine(game.InstallDir, "Subnautica_Data", "StreamingAssets", "SNUnmanagedData", "plastic_status.ignore"); + if (!File.Exists(gameVersionFile)) + { + return; + } + if (!int.TryParse(File.ReadAllText(gameVersionFile), out int version)) + { + return; + } + if (version == -1) + { + return; + } + if (version > LEGACY_BRANCH_SUBNAUTICA_VERSION) + { + return; + } + + LogError($""" + Game version is {version}, which is not supported by Nitrox. + Please update your game to the latest version. + """); + Exit(2); + } + private static GameInstallData EnsureGame() { static bool ValidateUnityGame(GameInstallData game, out string error) From 03a221bff440b42e9f65380436efd8760d76b4ab Mon Sep 17 00:00:00 2001 From: NinjaPedroX <32976499+NinjaPedroX@users.noreply.github.com> Date: Thu, 27 Apr 2023 17:05:22 -0400 Subject: [PATCH 5/5] Restore Launcher Window Animations (#2031) * Restoring Window Animations Code for restoring the window animations * Refactored window animation enable code to WindowsApi.cs --------- Co-authored-by: Measurity --- NitroxLauncher/MainWindow.xaml | 3 +- NitroxLauncher/MainWindow.xaml.cs | 15 ++++++-- .../OS/Windows/Internal/Win32Native.cs | 38 +++++++++++++++++++ .../Platforms/OS/Windows/WindowsApi.cs | 26 +++++++++++++ 4 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 NitroxModel/Platforms/OS/Windows/WindowsApi.cs diff --git a/NitroxLauncher/MainWindow.xaml b/NitroxLauncher/MainWindow.xaml index 9a96b36674..0caaef9120 100644 --- a/NitroxLauncher/MainWindow.xaml +++ b/NitroxLauncher/MainWindow.xaml @@ -11,7 +11,8 @@ Title="Nitrox Launcher" Height="642" MinHeight="642" Width="1024" MinWidth="1024" WindowStyle="None" WindowStartupLocation="CenterScreen" Closing="OnClosing" - Background="Black"> + Background="Black" + Loaded="Window_Loaded"> diff --git a/NitroxLauncher/MainWindow.xaml.cs b/NitroxLauncher/MainWindow.xaml.cs index 401b32a5ce..f4321f055d 100644 --- a/NitroxLauncher/MainWindow.xaml.cs +++ b/NitroxLauncher/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.IO; using System.Net.NetworkInformation; @@ -7,11 +7,13 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; +using System.Windows.Interop; using NitroxLauncher.Models.Events; using NitroxLauncher.Models.Properties; using NitroxLauncher.Pages; using NitroxModel.Discovery; using NitroxModel.Helper; +using NitroxModel.Platforms.OS.Windows; namespace NitroxLauncher { @@ -41,7 +43,7 @@ public MainWindow() { Log.Setup(); LauncherNotifier.Setup(); - + logic = new LauncherLogic(); MaxHeight = SystemParameters.VirtualScreenHeight; @@ -66,7 +68,7 @@ public MainWindow() MessageBoxImage.Error); Environment.Exit(1); } - + // This pirate detection subscriber is immediately invoked if pirate has been detected right now. PirateDetection.PirateDetected += (o, eventArgs) => { @@ -78,7 +80,7 @@ public MainWindow() HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch, Margin = new Thickness(0), - + Height = MinHeight * 0.7, Width = MinWidth * 0.7 }; @@ -216,5 +218,10 @@ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + private void Window_Loaded(object sender, RoutedEventArgs e) + { + WindowsApi.EnableDefaultWindowAnimations(new WindowInteropHelper(this).Handle); + } } } diff --git a/NitroxModel/Platforms/OS/Windows/Internal/Win32Native.cs b/NitroxModel/Platforms/OS/Windows/Internal/Win32Native.cs index 57a1295e44..66835367b8 100644 --- a/NitroxModel/Platforms/OS/Windows/Internal/Win32Native.cs +++ b/NitroxModel/Platforms/OS/Windows/Internal/Win32Native.cs @@ -334,6 +334,44 @@ private enum UIContext Install } + [DllImport("user32.dll", EntryPoint = "SetWindowLong")] + internal static extern int SetWindowLong32(HandleRef hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] + internal static extern IntPtr SetWindowLongPtr64(HandleRef hWnd, int nIndex, IntPtr dwNewLong); + + [Flags] + public enum WS : long + { + WS_BORDER = 0x00800000L, + WS_CAPTION = 0x00C00000L, + WS_CHILD = 0x40000000L, + WS_CHILDWINDOW = 0x40000000L, + WS_CLIPCHILDREN = 0x02000000L, + WS_CLIPSIBLINGS = 0x04000000L, + WS_DISABLED = 0x08000000L, + WS_DLGFRAME = 0x00400000L, + WS_GROUP = 0x00020000L, + WS_HSCROLL = 0x00100000L, + WS_ICONIC = 0x20000000L, + WS_MAXIMIZE = 0x01000000L, + WS_MAXIMIZEBOX = 0x00010000L, + WS_MINIMIZE = 0x20000000L, + WS_MINIMIZEBOX = 0x00020000L, + WS_OVERLAPPED = 0x00000000L, + WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, + WS_POPUP = 0x80000000L, + WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU, + WS_SIZEBOX = 0x00040000L, + WS_SYSMENU = 0x00080000L, + WS_TABSTOP = 0x00010000L, + WS_THICKFRAME = 0x00040000L, + WS_TILED = 0x00000000L, + WS_TILEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, + WS_VISIBLE = 0x10000000L, + WS_VSCROLL = 0x00200000L + } + [StructLayout(LayoutKind.Sequential)] private struct WINTRUST_DATA : IDisposable { diff --git a/NitroxModel/Platforms/OS/Windows/WindowsApi.cs b/NitroxModel/Platforms/OS/Windows/WindowsApi.cs new file mode 100644 index 0000000000..ff3493215b --- /dev/null +++ b/NitroxModel/Platforms/OS/Windows/WindowsApi.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.InteropServices; +using NitroxModel.Platforms.OS.Windows.Internal; + +namespace NitroxModel.Platforms.OS.Windows; + +public class WindowsApi +{ + public static void EnableDefaultWindowAnimations(IntPtr hWnd, int nIndex = -16) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + IntPtr dwNewLong = new((long)(Win32Native.WS.WS_CAPTION | Win32Native.WS.WS_CLIPCHILDREN | Win32Native.WS.WS_MINIMIZEBOX | Win32Native.WS.WS_MAXIMIZEBOX | Win32Native.WS.WS_SYSMENU | Win32Native.WS.WS_SIZEBOX)); + HandleRef handle = new(null, hWnd); + switch (IntPtr.Size) + { + case 8: + Win32Native.SetWindowLongPtr64(handle, nIndex, dwNewLong); + break; + default: + Win32Native.SetWindowLong32(handle, nIndex, dwNewLong.ToInt32()); + break; + } + } + } +}