From 7a210393ee48b3752d1cfd0181d9a67158270ce8 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Thu, 22 Oct 2020 00:08:14 +0200 Subject: [PATCH] Solved GameOptionsData TODO (#64) * Ignoring .Vs folder * GameOptionsData deserialization requires x16 less time & no extra alloc * Super-micro optimisation (variable localization) * JoinGame probably faster * Added Tests * Renamed swap -> advance * Removed submodule Hazel * Removed launchsettings Co-authored-by: Weirdo --- .gitignore | 1 + .../Extensions/SpanReaderExtensions.cs | 70 +++++++++++ src/Impostor.Api/Impostor.Api.csproj | 8 ++ .../Innersloth/GameOptionsData.cs | 116 +++++++++--------- .../Net/Messages/C2S/Message01JoinGameC2S.cs | 11 +- .../Net/Messages/C2S/Message10AlterGameC2S.cs | 9 +- src/Impostor.Hazel/MessageReader.cs | 4 +- src/Impostor.Hazel/MessageWriter.cs | 28 +++-- .../Impostor.Plugins.Debugger.csproj | 6 +- .../Objects/Components/InnerVoteBanSystem.cs | 5 +- src/Impostor.Tests/GameCodeTests.cs | 7 +- .../Hazel/MessageWriterTests.cs | 38 ++++++ 12 files changed, 225 insertions(+), 78 deletions(-) create mode 100644 src/Impostor.Api/Extensions/SpanReaderExtensions.cs create mode 100644 src/Impostor.Tests/Hazel/MessageWriterTests.cs diff --git a/.gitignore b/.gitignore index 95e2d57..3878f3e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /build /.vs /src/Impostor.Plugins.Debugger/Properties/launchSettings.json + diff --git a/src/Impostor.Api/Extensions/SpanReaderExtensions.cs b/src/Impostor.Api/Extensions/SpanReaderExtensions.cs new file mode 100644 index 0000000..97365ac --- /dev/null +++ b/src/Impostor.Api/Extensions/SpanReaderExtensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace Impostor.Api.Extensions +{ + /// + /// Priovides a StreamReader-like api throught extensions + /// + public static class SpanReaderExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadByte(this ref ReadOnlySpan input) + { + var original = Advance(ref input); + return original[0]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadInt32(this ref ReadOnlySpan input) + { + var original = Advance(ref input); + return BinaryPrimitives.ReadInt32LittleEndian(original); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32(this ref ReadOnlySpan input) + { + var original = Advance(ref input); + return BinaryPrimitives.ReadUInt32LittleEndian(original); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ReadSingle(this ref ReadOnlySpan input) + { + var original = Advance(ref input); + + // BitConverter.Int32BitsToSingle + // Doesn't exist in net 2.0 for some reason + return Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(original)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ReadBoolean(this ref ReadOnlySpan input) + { + return input.ReadByte() != 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe float Int32BitsToSingle(int value) + { + return *((float*)&value); + } + + /// + /// Advances the position of by the size of . + /// + /// Type that will be read. + /// input "stream"/span. + /// The original input + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ReadOnlySpan Advance(ref ReadOnlySpan input) + where T : unmanaged + { + var original = input; + input = input.Slice(sizeof(T)); + return original; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Api/Impostor.Api.csproj b/src/Impostor.Api/Impostor.Api.csproj index 9f65ba6..071adf1 100644 --- a/src/Impostor.Api/Impostor.Api.csproj +++ b/src/Impostor.Api/Impostor.Api.csproj @@ -7,6 +7,14 @@ 9 + + true + + + + true + + diff --git a/src/Impostor.Api/Innersloth/GameOptionsData.cs b/src/Impostor.Api/Innersloth/GameOptionsData.cs index cd26653..e420e53 100644 --- a/src/Impostor.Api/Innersloth/GameOptionsData.cs +++ b/src/Impostor.Api/Innersloth/GameOptionsData.cs @@ -1,7 +1,9 @@ -using System; -using System.IO; +using Impostor.Api.Extensions; using Impostor.Api.Innersloth.Data; +using System; +using System.IO; + namespace Impostor.Api.Innersloth { public class GameOptionsData @@ -29,78 +31,78 @@ namespace Impostor.Api.Innersloth public bool ConfirmImpostor { get; set; } public bool VisualTasks { get; set; } public bool IsDefaults { get; set; } - + public void Serialize(BinaryWriter writer, byte version) { - writer.Write((byte) version); - writer.Write((byte) MaxPlayers); - writer.Write((uint) Keywords); - writer.Write((byte) MapId); - writer.Write((float) PlayerSpeedMod); - writer.Write((float) CrewLightMod); - writer.Write((float) ImpostorLightMod); - writer.Write((float) KillCooldown); - writer.Write((byte) NumCommonTasks); - writer.Write((byte) NumLongTasks); - writer.Write((byte) NumShortTasks); - writer.Write((int) NumEmergencyMeetings); - writer.Write((byte) NumImpostors); - writer.Write((byte) KillDistance); - writer.Write((uint) DiscussionTime); - writer.Write((uint) VotingTime); - writer.Write((bool) IsDefaults); + writer.Write((byte)version); + writer.Write((byte)MaxPlayers); + writer.Write((uint)Keywords); + writer.Write((byte)MapId); + writer.Write((float)PlayerSpeedMod); + writer.Write((float)CrewLightMod); + writer.Write((float)ImpostorLightMod); + writer.Write((float)KillCooldown); + writer.Write((byte)NumCommonTasks); + writer.Write((byte)NumLongTasks); + writer.Write((byte)NumShortTasks); + writer.Write((int)NumEmergencyMeetings); + writer.Write((byte)NumImpostors); + writer.Write((byte)KillDistance); + writer.Write((uint)DiscussionTime); + writer.Write((uint)VotingTime); + writer.Write((bool)IsDefaults); if (version > 1) { - writer.Write((byte) EmergencyCooldown); + writer.Write((byte)EmergencyCooldown); } if (version > 2) { - writer.Write((bool) ConfirmImpostor); - writer.Write((bool) VisualTasks); + writer.Write((bool)ConfirmImpostor); + writer.Write((bool)VisualTasks); } } - public static GameOptionsData Deserialize(ReadOnlyMemory bytes) + public static GameOptionsData Deserialize(ReadOnlyMemory memory) { - // TODO: Remove memory allocation. + var bytes = memory.Span; - using (var stream = new MemoryStream(bytes.ToArray())) - using (var reader = new BinaryReader(stream)) - { - var result = new GameOptionsData(); + var result = new GameOptionsData(); + result.Version = bytes.ReadByte(); + result.MaxPlayers = bytes.ReadByte(); + result.Keywords = (GameKeywords)bytes.ReadUInt32(); + result.MapId = bytes.ReadByte(); + result.PlayerSpeedMod = bytes.ReadSingle(); + + result.CrewLightMod = bytes.ReadSingle(); + result.ImpostorLightMod = bytes.ReadSingle(); + result.KillCooldown = bytes.ReadSingle(); + + result.NumCommonTasks = bytes.ReadByte(); + result.NumLongTasks = bytes.ReadByte(); + result.NumShortTasks = bytes.ReadByte(); - result.Version = reader.ReadByte(); - result.MaxPlayers = reader.ReadByte(); - result.Keywords = (GameKeywords) reader.ReadUInt32(); - result.MapId = reader.ReadByte(); - result.PlayerSpeedMod = reader.ReadSingle(); - result.CrewLightMod = reader.ReadSingle(); - result.ImpostorLightMod = reader.ReadSingle(); - result.KillCooldown = reader.ReadSingle(); - result.NumCommonTasks = reader.ReadByte(); - result.NumLongTasks = reader.ReadByte(); - result.NumShortTasks = reader.ReadByte(); - result.NumEmergencyMeetings = reader.ReadInt32(); - result.NumImpostors = reader.ReadByte(); - result.KillDistance = reader.ReadByte(); - result.DiscussionTime = reader.ReadInt32(); - result.VotingTime = reader.ReadInt32(); - result.IsDefaults = reader.ReadBoolean(); + result.NumEmergencyMeetings = bytes.ReadInt32(); - if (result.Version > 1) - { - result.EmergencyCooldown = reader.ReadByte(); - } + result.NumImpostors = bytes.ReadByte(); + result.KillDistance = bytes.ReadByte(); + result.DiscussionTime = bytes.ReadInt32(); + result.VotingTime = bytes.ReadInt32(); - if (result.Version > 2) - { - result.ConfirmImpostor = reader.ReadBoolean(); - result.VisualTasks = reader.ReadBoolean(); - } - - return result; + result.IsDefaults = bytes.ReadBoolean(); + + if (result.Version > 1) + { + result.EmergencyCooldown = bytes.ReadByte(); + } + + if (result.Version > 2) + { + result.ConfirmImpostor = bytes.ReadBoolean(); + result.VisualTasks = bytes.ReadBoolean(); } + + return result; } } } \ No newline at end of file diff --git a/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs b/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs index 7e6baf3..c713790 100644 --- a/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs +++ b/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs @@ -1,4 +1,7 @@ -namespace Impostor.Api.Net.Messages.C2S +using System; +using Impostor.Api.Extensions; + +namespace Impostor.Api.Net.Messages.C2S { public static class Message01JoinGameC2S { @@ -9,8 +12,10 @@ public static void Deserialize(IMessageReader reader, out int gameCode, out byte unknown) { - gameCode = reader.ReadInt32(); - unknown = reader.ReadByte(); + var slice = reader.ReadBytes(sizeof(Int32) + sizeof(byte)).Span; + + gameCode = slice.ReadInt32(); + unknown = slice.ReadByte(); } } } \ No newline at end of file diff --git a/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs b/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs index 3a92e58..e9ad8e8 100644 --- a/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs +++ b/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs @@ -1,4 +1,5 @@ -using Impostor.Api.Innersloth.Data; +using Impostor.Api.Extensions; +using Impostor.Api.Innersloth.Data; namespace Impostor.Api.Net.Messages.C2S { @@ -11,8 +12,10 @@ namespace Impostor.Api.Net.Messages.C2S public static void Deserialize(IMessageReader reader, out AlterGameTags gameTag, out bool isPublic) { - gameTag = (AlterGameTags)reader.ReadByte(); - isPublic = reader.ReadBoolean(); + var slice = reader.ReadBytes(sizeof(byte) + sizeof(byte)).Span; + + gameTag = (AlterGameTags)slice.ReadByte(); + isPublic = slice.ReadBoolean(); } } } \ No newline at end of file diff --git a/src/Impostor.Hazel/MessageReader.cs b/src/Impostor.Hazel/MessageReader.cs index ffed6ca..f81c482 100644 --- a/src/Impostor.Hazel/MessageReader.cs +++ b/src/Impostor.Hazel/MessageReader.cs @@ -28,7 +28,7 @@ namespace Impostor.Hazel public IMessageReader ReadMessage() { var length = ReadUInt16(); - var tag = ReadByte(); + var tag = FastByte(); var pos = Position; Position += length; @@ -121,7 +121,7 @@ namespace Impostor.Hazel while (readMore) { - byte b = ReadByte(); + byte b = FastByte(); if (b >= 0x80) { readMore = true; diff --git a/src/Impostor.Hazel/MessageWriter.cs b/src/Impostor.Hazel/MessageWriter.cs index bf8dbcd..10f0065 100644 --- a/src/Impostor.Hazel/MessageWriter.cs +++ b/src/Impostor.Hazel/MessageWriter.cs @@ -1,9 +1,10 @@ -using System; +using Impostor.Api.Games; +using Impostor.Api.Net.Messages; + +using System; using System.Collections.Generic; using System.Net; using System.Text; -using Impostor.Api.Games; -using Impostor.Api.Net.Messages; namespace Impostor.Hazel { @@ -16,7 +17,7 @@ namespace Impostor.Hazel public MessageType SendOption { get; private set; } private Stack messageStarts = new Stack(); - + public MessageWriter(byte[] buffer) { this.Buffer = buffer; @@ -56,6 +57,8 @@ namespace Impostor.Hazel System.Buffer.BlockCopy(this.Buffer, 1, output, 0, this.Length - 1); return output; } + default: + throw new ArgumentOutOfRangeException(); } } @@ -122,6 +125,7 @@ namespace Impostor.Hazel case MessageType.Unreliable: this.Length = this.Position = 1; break; + case MessageType.Reliable: this.Length = this.Position = 3; break; @@ -235,7 +239,15 @@ namespace Impostor.Hazel public void Write(ReadOnlyMemory data) { - this.Write(data.ToArray()); // TODO: Fix memory allocation. + Write(data.Span); + } + + public void Write(ReadOnlySpan bytes) + { + bytes.CopyTo(this.Buffer.AsSpan(this.Position, bytes.Length)); + + this.Position += bytes.Length; + if (this.Position > this.Length) this.Length = this.Position; } public void Write(byte[] bytes) @@ -280,7 +292,8 @@ namespace Impostor.Hazel value >>= 7; } while (value > 0); } - #endregion + + #endregion WriteMethods public void Write(MessageWriter msg, bool includeHeader) { @@ -292,6 +305,7 @@ namespace Impostor.Hazel case MessageType.Unreliable: offset = 1; break; + case MessageType.Reliable: offset = 3; break; @@ -319,4 +333,4 @@ namespace Impostor.Hazel Recycle(); } } -} +} \ No newline at end of file diff --git a/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj b/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj index 084174e..9518e48 100644 --- a/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj +++ b/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj @@ -1,4 +1,4 @@ - + net5.0 @@ -9,4 +9,8 @@ + + + + \ No newline at end of file diff --git a/src/Impostor.Server/GameData/Objects/Components/InnerVoteBanSystem.cs b/src/Impostor.Server/GameData/Objects/Components/InnerVoteBanSystem.cs index e4a156b..72253ba 100644 --- a/src/Impostor.Server/GameData/Objects/Components/InnerVoteBanSystem.cs +++ b/src/Impostor.Server/GameData/Objects/Components/InnerVoteBanSystem.cs @@ -25,6 +25,7 @@ namespace Impostor.Server.GameData.Objects.Components public override void Deserialize(IMessageReader reader, bool initialState) { + var votes = _votes; var unknown = reader.ReadBoolean(); if (unknown) { @@ -32,10 +33,10 @@ namespace Impostor.Server.GameData.Objects.Components { var v4 = reader.ReadInt32(); - if (!_votes.TryGetValue(v4, out var v12)) + if (!votes.TryGetValue(v4, out var v12)) { v12 = new int[3]; - _votes[v4] = v12; + votes[v4] = v12; } for (var i = 0; i < 3; i++) diff --git a/src/Impostor.Tests/GameCodeTests.cs b/src/Impostor.Tests/GameCodeTests.cs index 93321e2..de98123 100644 --- a/src/Impostor.Tests/GameCodeTests.cs +++ b/src/Impostor.Tests/GameCodeTests.cs @@ -1,4 +1,5 @@ using Impostor.Api.Innersloth; + using Xunit; namespace Impostor.Tests @@ -10,17 +11,17 @@ namespace Impostor.Tests { const string code = "ABCD"; const int codeInt = 0x44434241; - + Assert.Equal(code, GameCodeParser.IntToGameName(codeInt)); Assert.Equal(codeInt, GameCodeParser.GameNameToInt(code)); } - + [Fact] public void CodeV2() { const string code = "ABCDEF"; const int codeInt = -1943683525; - + Assert.Equal(code, GameCodeParser.IntToGameName(codeInt)); Assert.Equal(codeInt, GameCodeParser.GameNameToInt(code)); } diff --git a/src/Impostor.Tests/Hazel/MessageWriterTests.cs b/src/Impostor.Tests/Hazel/MessageWriterTests.cs new file mode 100644 index 0000000..83d67da --- /dev/null +++ b/src/Impostor.Tests/Hazel/MessageWriterTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using Impostor.Hazel; +using Xunit; + +namespace Impostor.Tests.Hazel +{ + public class MessageWriterTests + { + [Fact] + public void ReadOnlyMemoryWriteWorksTheSameAsArray() + { + var oldVer = new MessageWriter(1024); + var newVer = new MessageWriter(1024); + + var data = Enumerable.Repeat + ( + Enumerable.Range(0, byte.MaxValue) + .Select(x => (byte)x), + 2 + ).SelectMany(x => x).ToArray(); + + WriteSomeData(oldVer); + WriteSomeData(newVer); + + oldVer.Write(data); + newVer.Write(data.AsMemory()); + + Assert.True(oldVer.Buffer.AsSpan().SequenceEqual(newVer.Buffer.AsSpan())); + + static void WriteSomeData(MessageWriter oldVer) + { + oldVer.WritePacked(99); + oldVer.WritePacked(101); + } + } + } +} \ No newline at end of file -- 2.39.5