diff --git a/Nitrox.BuildTool/Program.cs b/Nitrox.BuildTool/Program.cs index 3926ee1f21..fed0af688a 100644 --- a/Nitrox.BuildTool/Program.cs +++ b/Nitrox.BuildTool/Program.cs @@ -23,21 +23,22 @@ 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); }; Log.Setup(false, null, true, true, false); - 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(); } @@ -50,6 +51,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) 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.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 new file mode 100644 index 0000000000..dc000788b8 --- /dev/null +++ b/Nitrox.Test/Model/Packets/PacketsSerializableTest.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using BinaryPack.Attributes; +using KellermanSoftware.CompareNetObjects; +using KellermanSoftware.CompareNetObjects.TypeComparers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Nitrox.Test.Helper.Faker; +using NitroxModel_Subnautica.Logger; +using NitroxModel.DataStructures; + +namespace NitroxModel.Packets; + +[TestClass] +public class PacketsSerializableTest +{ + [TestMethod] + public void InitSerializerTest() + { + Packet.InitSerializer(); + } + + [TestMethod] + public void PacketSerializationTest() + { + 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 + 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<(Packet, Packet)> generatedPackets = new(); + + foreach (Type type in packetTypes) + { + dynamic faker = NitroxFaker.GetOrCreateFaker(type); + + Packet packet = faker.Generate(); + Packet packet2 = null; + + if (!emptyPackets.Contains(type)) + { + ComparisonResult result; + do + { + packet2 = faker.Generate(); + result = comparer.Compare(packet, packet2); + } while (result == null || result.AreEqual); + } + + generatedPackets.Add(new ValueTuple(packet, packet2)); + } + + Packet.InitSerializer(); + + foreach (ValueTuple packet in generatedPackets) + { + Packet deserialized = Packet.Deserialize(packet.Item1.Serialize()); + + packet.Item1.ShouldCompare(deserialized, $"with {packet.Item1.GetType()}", config); + + if (!emptyPackets.Contains(packet.Item1.GetType())) + { + packet.Item2.ShouldNotCompare(deserialized, $"with {packet.Item1.GetType()}", config); + } + } + } +} diff --git a/Nitrox.Test/Nitrox.Test.csproj b/Nitrox.Test/Nitrox.Test.csproj index daae29731b..b6850a2ba2 100644 --- a/Nitrox.Test/Nitrox.Test.csproj +++ b/Nitrox.Test/Nitrox.Test.csproj @@ -7,8 +7,7 @@ - - + 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/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/Nitrox.sln.DotSettings b/Nitrox.sln.DotSettings index b67daa9d84..5fc4b0c7fc 100644 --- a/Nitrox.sln.DotSettings +++ b/Nitrox.sln.DotSettings @@ -105,6 +105,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/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/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/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/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/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/NitroxLauncher/NitroxLauncher.csproj b/NitroxLauncher/NitroxLauncher.csproj index bcb3e849b9..000b808869 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/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/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 f79d7e6d8d..06c7f84beb 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.Configuration; @@ -38,9 +39,10 @@ public static void Setup(bool asyncConsoleWriter = false, InGameLogger inGameLog return; } isSetup = true; -  + NetDebug.Logger = new LiteNetLibLogger(); + PlayerName = ""; -  + // Configure logger and create an instance of it. LoggerConfiguration loggerConfig = new LoggerConfiguration().MinimumLevel.Debug(); if (useConsoleLogging) @@ -101,7 +103,7 @@ private static LoggerConfiguration AppendConsoleSink(this LoggerSinkConfiguratio cnf.WriteTo.ColoredConsole(outputTemplate: consoleTemplate); } }); - + [Conditional("DEBUG")] public static void Debug(string message) { @@ -159,7 +161,7 @@ public static void WarnOnce(string message) { return; } - + Warn(message); logOnceCache.Add(hash); } diff --git a/NitroxModel/NitroxModel.csproj b/NitroxModel/NitroxModel.csproj index 9e0e4482a0..e05bcd3d4c 100644 --- a/NitroxModel/NitroxModel.csproj +++ b/NitroxModel/NitroxModel.csproj @@ -14,7 +14,7 @@ - + diff --git a/NitroxModel/Packets/Packet.cs b/NitroxModel/Packets/Packet.cs index d69a2beff8..b0d2339d25 100644 --- a/NitroxModel/Packets/Packet.cs +++ b/NitroxModel/Packets/Packet.cs @@ -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/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/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/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; + } + } + } +} 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); +} 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 1c4d973fcc..96415250c3 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,9 +235,19 @@ 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) + if (saveFileVersion == null || saveFileVersion.Version == NitroxEnvironment.Version) { return; }