diff --git a/benchmarks/Backdash.Benchmarks.Ping/Backdash.Benchmarks.Ping.csproj b/benchmarks/Backdash.Benchmarks.Ping/Backdash.Benchmarks.Ping.csproj index 5fd943a2..efd3474d 100644 --- a/benchmarks/Backdash.Benchmarks.Ping/Backdash.Benchmarks.Ping.csproj +++ b/benchmarks/Backdash.Benchmarks.Ping/Backdash.Benchmarks.Ping.csproj @@ -1,4 +1,4 @@ - + Exe net8.0 diff --git a/benchmarks/Backdash.Benchmarks/Backdash.Benchmarks.csproj b/benchmarks/Backdash.Benchmarks/Backdash.Benchmarks.csproj index 74ba97a5..a60e70bd 100644 --- a/benchmarks/Backdash.Benchmarks/Backdash.Benchmarks.csproj +++ b/benchmarks/Backdash.Benchmarks/Backdash.Benchmarks.csproj @@ -1,4 +1,4 @@ - + Exe net8.0 diff --git a/src/Backdash.Analyzers/Backdash.Analyzers.csproj b/src/Backdash.Analyzers/Backdash.Analyzers.csproj index d77fc889..c66946e5 100644 --- a/src/Backdash.Analyzers/Backdash.Analyzers.csproj +++ b/src/Backdash.Analyzers/Backdash.Analyzers.csproj @@ -1,6 +1,7 @@ - + - net8.0 + netstandard2.0 + preview true diff --git a/src/Backdash.Analyzers/IsExternalInit.cs b/src/Backdash.Analyzers/IsExternalInit.cs new file mode 100644 index 00000000..3ae28b70 --- /dev/null +++ b/src/Backdash.Analyzers/IsExternalInit.cs @@ -0,0 +1,7 @@ +namespace System.Runtime.CompilerServices +{ + /// + /// This is needed to make the analyzers work in .netstandard2.0 (so they are compatible with visual studio) + /// + static class IsExternalInit { } +} diff --git a/src/Backdash.Analyzers/SourceGenerationHelper.cs b/src/Backdash.Analyzers/SourceGenerationHelper.cs index 30fc3e25..9cc91364 100644 --- a/src/Backdash.Analyzers/SourceGenerationHelper.cs +++ b/src/Backdash.Analyzers/SourceGenerationHelper.cs @@ -188,7 +188,7 @@ static bool IsTypeArrayCopiable(ITypeSymbol type) { Debug.Assert(type != null); - if (!type.IsUnmanagedType || type is INamedTypeSymbol { EnumUnderlyingType: not null }) + if (!(type?.IsUnmanagedType ?? false) || type is INamedTypeSymbol { EnumUnderlyingType: not null }) return false; return type.SpecialType switch diff --git a/src/Backdash.Utils/Backdash.Utils.csproj b/src/Backdash.Utils/Backdash.Utils.csproj index 66fdbfdf..fc0b4e0a 100644 --- a/src/Backdash.Utils/Backdash.Utils.csproj +++ b/src/Backdash.Utils/Backdash.Utils.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -9,6 +9,7 @@ Backdash network utilities network, endpoint, multiplayer, json, input CS1591 + true diff --git a/src/Backdash/Backdash.csproj b/src/Backdash/Backdash.csproj index 454c8757..f569d9fc 100644 --- a/src/Backdash/Backdash.csproj +++ b/src/Backdash/Backdash.csproj @@ -1,4 +1,4 @@ - + net8.0 enable @@ -6,11 +6,12 @@ Backdash Rollback netcode library rollback, netcode, network, peer to peer, online, game, multiplayer + true + Speed - true - Speed + true diff --git a/src/Backdash/Core/LogInterpolatedStringHandler.cs b/src/Backdash/Core/LogInterpolatedStringHandler.cs index 1d57e5e0..264feaa0 100644 --- a/src/Backdash/Core/LogInterpolatedStringHandler.cs +++ b/src/Backdash/Core/LogInterpolatedStringHandler.cs @@ -124,4 +124,17 @@ struct LogStringBuffer #endif byte elemenet0; + + /// + public override readonly int GetHashCode() => Mem.GetHashCode(this); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public readonly bool Equals(LogStringBuffer other) => this[..].SequenceEqual(other); + + /// + public override readonly bool Equals(object? obj) => obj is LogStringBuffer other && Equals(other); } diff --git a/src/Backdash/Options/SyncTestOptions.cs b/src/Backdash/Options/SyncTestOptions.cs index dff7ecf8..00477a1e 100644 --- a/src/Backdash/Options/SyncTestOptions.cs +++ b/src/Backdash/Options/SyncTestOptions.cs @@ -1,4 +1,6 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Backdash.Synchronizing.Input; using Backdash.Synchronizing.State; @@ -50,12 +52,24 @@ public SyncTestOptions UseRandomInputProvider() /// /// Use as state viewer. /// + /// For AoT compatibility, use instead. + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo instead.")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] public SyncTestOptions UseJsonStateViewer(JsonSerializerOptions? options = null) { StateStringParser = new JsonStateStringParser(options); return this; } + /// + /// Use as state viewer. + /// + public SyncTestOptions UseJsonStateViewer(JsonTypeInfo stateTypeInfo) + { + StateStringParser = new JsonStateStringParser(stateTypeInfo); + return this; + } + /// public SyncTestOptions UseStateViewer() where T : IStateStringParser, new() { diff --git a/src/Backdash/Synchronizing/State/IStateStringParser.cs b/src/Backdash/Synchronizing/State/IStateStringParser.cs index 75cf8b07..64a9a36b 100644 --- a/src/Backdash/Synchronizing/State/IStateStringParser.cs +++ b/src/Backdash/Synchronizing/State/IStateStringParser.cs @@ -1,4 +1,6 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Backdash.Core; using Backdash.Serialization; @@ -34,17 +36,35 @@ public string GetStateString(in Frame frame, ref readonly BinaryBufferReader rea /// Try to get the json string representation of the state. /// public sealed class JsonStateStringParser( - JsonSerializerOptions? options = null, + JsonTypeInfo stateTypeInfo, IStateStringParser? stateStringFallback = null ) : IStateStringParser { + /// + /// Try to get the json string representation of the state. + /// + /// For AoT compatibility, use instead. + /// + /// + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo instead.")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + public JsonStateStringParser( + JsonSerializerOptions? options = null, + IStateStringParser? stateStringFallback = null + ) : this( + (options ?? new() + { + WriteIndented = true, + IncludeFields = true, + }).GetTypeInfo(typeof(object)), + stateStringFallback) + { + + } + internal Logger? Logger = null; - readonly JsonSerializerOptions jsonOptions = options ?? new() - { - WriteIndented = true, - IncludeFields = true, - }; + readonly JsonTypeInfo typeInfo = stateTypeInfo ?? throw new ArgumentNullException(nameof(stateTypeInfo)); readonly IStateStringParser fallback = stateStringFallback ?? new HexStateStringParser(); @@ -56,7 +76,7 @@ public string GetStateString(in Frame frame, ref readonly BinaryBufferReader rea try { - return JsonSerializer.Serialize(currentState, jsonOptions); + return JsonSerializer.Serialize(currentState, typeInfo); } catch (Exception e) { diff --git a/tests/Backdash.Tests/Backdash.Tests.csproj b/tests/Backdash.Tests/Backdash.Tests.csproj index beea4555..6ca1d589 100644 --- a/tests/Backdash.Tests/Backdash.Tests.csproj +++ b/tests/Backdash.Tests/Backdash.Tests.csproj @@ -7,6 +7,7 @@ true false CS1591;NU1903 + true diff --git a/tests/Backdash.Tests/Specs/Unit/Network/Protocol/ProtocolInputBufferTests.cs b/tests/Backdash.Tests/Specs/Unit/Network/Protocol/ProtocolInputBufferTests.cs index 76aab81e..ea44afde 100644 --- a/tests/Backdash.Tests/Specs/Unit/Network/Protocol/ProtocolInputBufferTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Network/Protocol/ProtocolInputBufferTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Backdash.Core; using Backdash.Data; using Backdash.Network; @@ -46,7 +47,7 @@ public void ValidateTestSampleSerialization() decompressedInput.Should().BeEquivalentTo(GetSampleInputs()); } - [Fact] + [DynamicFact(nameof(AutoFakeIt) + " requires dynamic code, even if the library is not annotated with " + nameof(RequiresDynamicCodeAttribute))] public void ShouldSendSingleInput() { var faker = GetFaker(); @@ -72,7 +73,7 @@ public void ShouldSendSingleInput() queue.SendInput(input).Should().Be(SendInputResult.Ok); } - [Fact] + [DynamicFact(nameof(AutoFakeIt) + " requires dynamic code, even if the library is not annotated with " + nameof(RequiresDynamicCodeAttribute))] public void ShouldCompressMultipleBufferedInputs() { var faker = GetFakerWithSender(); @@ -89,7 +90,7 @@ public void ShouldCompressMultipleBufferedInputs() lastMessageSent.Should().BeEquivalentTo(GetSampleMessage()); } - [Fact] + [DynamicFact(nameof(AutoFakeIt) + " requires dynamic code, even if the library is not annotated with " + nameof(RequiresDynamicCodeAttribute))] public void ShouldSkipAckedInputs() { var faker = GetFakerWithSender(); @@ -119,7 +120,7 @@ public void ShouldSkipAckedInputs() lastSentMessage.Should().BeEquivalentTo(expectedMessage); } - [Fact] + [DynamicFact(nameof(AutoFakeIt) + " requires dynamic code, even if the library is not annotated with " + nameof(RequiresDynamicCodeAttribute))] public void ShouldHandleWhenMaxInputSizeReached() { var faker = GetFakerWithSender(); diff --git a/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPAddressConverterTests.cs b/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPAddressConverterTests.cs index 9345dd72..d752b54e 100644 --- a/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPAddressConverterTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPAddressConverterTests.cs @@ -1,6 +1,8 @@ using System.Net; using System.Text.Json; +using System.Text.Json.Serialization; using Backdash.JsonConverters; +using Backdash.Tests.TestUtils; namespace Backdash.Tests.Specs.Unit.Utils; @@ -10,21 +12,14 @@ public class JsonIPAddressConverterTests { static readonly Faker faker = new(); - static readonly JsonSerializerOptions options = new() - { - Converters = - { - new JsonIPAddressConverter(), - }, - }; - record TestType(IPAddress Data); + public record IpAddressTestType([property: JsonConverter(typeof(JsonIPAddressConverter))] IPAddress Data); [Fact] public void ShouldParseIPv4() { var expected = faker.Internet.IpAddress(); - var value = Deserialize($$"""{"Data": "{{expected.ToString()}}"}""", options); + var value = Deserialize($$"""{"Data": "{{expected.ToString()}}"}""", JsonSourceGenerationContext.Default.IpAddressTestType); value!.Data.Should().Be(expected); } @@ -32,7 +27,7 @@ public void ShouldParseIPv4() public void ShouldParseIPv6() { var expected = faker.Internet.Ipv6Address(); - var value = Deserialize($$"""{"Data": "{{expected.ToString()}}"}""", options); + var value = Deserialize($$"""{"Data": "{{expected.ToString()}}"}""", JsonSourceGenerationContext.Default.IpAddressTestType); value!.Data.Should().Be(expected); } @@ -40,7 +35,7 @@ public void ShouldParseIPv6() public void ShouldSerializeIPv4() { var value = faker.Internet.IpAddress(); - var result = Serialize(new TestType(value), options); + var result = Serialize(new IpAddressTestType(value), JsonSourceGenerationContext.Default.IpAddressTestType); var expected = $$"""{"Data":"{{value}}"}"""; result.Should().Be(expected); } @@ -49,7 +44,7 @@ public void ShouldSerializeIPv4() public void ShouldSerializeIPv6() { var value = faker.Internet.Ipv6Address(); - var result = Serialize(new TestType(value), options); + var result = Serialize(new IpAddressTestType(value), JsonSourceGenerationContext.Default.IpAddressTestType); var expected = $$"""{"Data":"{{value}}"}"""; result.Should().Be(expected); } diff --git a/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPEndpointConverterTests.cs b/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPEndpointConverterTests.cs index 54f5dd4f..6b006600 100644 --- a/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPEndpointConverterTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPEndpointConverterTests.cs @@ -1,6 +1,8 @@ using System.Net; using System.Text.Json; +using System.Text.Json.Serialization; using Backdash.JsonConverters; +using Backdash.Tests.TestUtils; namespace Backdash.Tests.Specs.Unit.Utils; @@ -10,21 +12,13 @@ public class JsonIPEndpointConverterTests { static readonly Faker faker = new(); - static readonly JsonSerializerOptions options = new() - { - Converters = - { - new JsonIPEndPointConverter(), - }, - }; - - record TestType(IPEndPoint Data); + public record IpEndPointTestType([property: JsonConverter(typeof(JsonIPEndPointConverter))] IPEndPoint Data); [Fact] public void ShouldParseIPv4() { var expected = faker.Internet.IpEndPoint(); - var value = Deserialize($$"""{"Data": "{{expected}}"}""", options); + var value = Deserialize($$"""{"Data": "{{expected}}"}""", JsonSourceGenerationContext.Default.IpEndPointTestType); value!.Data.Should().Be(expected); } @@ -32,7 +26,7 @@ public void ShouldParseIPv4() public void ShouldParseIPv6() { var expected = faker.Internet.Ipv6EndPoint(); - var value = Deserialize($$"""{"Data": "{{expected}}"}""", options); + var value = Deserialize($$"""{"Data": "{{expected}}"}""", JsonSourceGenerationContext.Default.IpEndPointTestType); value!.Data.Should().Be(expected); } @@ -40,7 +34,7 @@ public void ShouldParseIPv6() public void ShouldSerializeIPv4() { var value = faker.Internet.IpEndPoint(); - var result = Serialize(new TestType(value), options); + var result = Serialize(new IpEndPointTestType(value), JsonSourceGenerationContext.Default.IpEndPointTestType); var expected = $$"""{"Data":"{{value}}"}"""; result.Should().Be(expected); } @@ -49,7 +43,7 @@ public void ShouldSerializeIPv4() public void ShouldSerializeIPv6() { var value = faker.Internet.Ipv6EndPoint(); - var result = Serialize(new TestType(value), options); + var result = Serialize(new IpEndPointTestType(value), JsonSourceGenerationContext.Default.IpEndPointTestType); var expected = $$"""{"Data":"{{value}}"}"""; result.Should().Be(expected); } diff --git a/tests/Backdash.Tests/TestUtils/DynamicFactAttribute.cs b/tests/Backdash.Tests/TestUtils/DynamicFactAttribute.cs new file mode 100644 index 00000000..98613113 --- /dev/null +++ b/tests/Backdash.Tests/TestUtils/DynamicFactAttribute.cs @@ -0,0 +1,22 @@ +using System.Runtime.CompilerServices; + +namespace Backdash.Tests.TestUtils +{ + /// + /// Attribute that is applied to a method to indicate that it is a fact that should be run + /// by the test runner only when dynamic code is supported. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + class DynamicFactAttribute : FactAttribute + { + public DynamicFactAttribute(string? message = null) + { + if (!RuntimeFeature.IsDynamicCodeSupported) + { + Skip = "Skipped due to no dynamic code support"; + if (message != null) + Skip += ": " + message; + } + } + } +} diff --git a/tests/Backdash.Tests/TestUtils/JsonSourceGenerationContext.cs b/tests/Backdash.Tests/TestUtils/JsonSourceGenerationContext.cs new file mode 100644 index 00000000..b5c0395e --- /dev/null +++ b/tests/Backdash.Tests/TestUtils/JsonSourceGenerationContext.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using Backdash.Tests.Specs.Unit.Utils; + +namespace Backdash.Tests.TestUtils +{ + + [JsonSerializable(typeof(JsonIPAddressConverterTests.IpAddressTestType))] + [JsonSerializable(typeof(JsonIPEndpointConverterTests.IpEndPointTestType))] + partial class JsonSourceGenerationContext : JsonSerializerContext + { + } +} diff --git a/tests/Backdash.Tests/TestUtils/TestInput.cs b/tests/Backdash.Tests/TestUtils/TestInput.cs index 6abe316a..f524643c 100644 --- a/tests/Backdash.Tests/TestUtils/TestInput.cs +++ b/tests/Backdash.Tests/TestUtils/TestInput.cs @@ -15,6 +15,19 @@ public readonly string ToString(bool trimZeros) => Mem.GetBitString(this, trimRightZeros: trimZeros); public override readonly string ToString() => ToString(trimZeros: true); + + /// + public override readonly int GetHashCode() => Mem.GetHashCode(this); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public readonly bool Equals(TestInputBuffer other) => this[..].SequenceEqual(other); + + /// + public override readonly bool Equals(object? obj) => obj is TestInputBuffer other && Equals(other); } [Serializable]