summaryrefslogtreecommitdiff
path: root/Impostor-dev/src
diff options
context:
space:
mode:
Diffstat (limited to 'Impostor-dev/src')
-rw-r--r--Impostor-dev/src/.editorconfig230
-rw-r--r--Impostor-dev/src/.gitattributes63
-rw-r--r--Impostor-dev/src/.gitignore263
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Attributes/EventListenerAttribute.cs34
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/EventPriority.cs12
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/IGameAlterEvent.cs7
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/IGameCreatedEvent.cs11
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/IGameDestroyedEvent.cs11
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/IGameEndedEvent.cs9
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/IGameEvent.cs12
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerJoinedEvent.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerLeftEvent.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/IGameStartedEvent.cs9
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/IGameStartingEvent.cs11
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEvent.cs9
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingStartedEvent.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerChatEvent.cs10
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerCompletedTaskEvent.cs10
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerDestroyedEvent.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerEvent.cs19
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerExileEvent.cs9
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs12
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSetStartCounterEvent.cs10
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSpawnedEvent.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerStartMeetingEvent.cs12
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerVentEvent.cs17
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/IEvent.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/IEventCancelable.cs10
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/IEventListener.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/IManualEventListener.cs13
-rw-r--r--Impostor-dev/src/Impostor.Api/Events/Managers/IEventManager.cs35
-rw-r--r--Impostor-dev/src/Impostor.Api/Exceptions/ImpostorCheatException.cs24
-rw-r--r--Impostor-dev/src/Impostor.Api/Exceptions/ImpostorConfigException.cs24
-rw-r--r--Impostor-dev/src/Impostor.Api/Exceptions/ImpostorException.cs24
-rw-r--r--Impostor-dev/src/Impostor.Api/Exceptions/ImpostorProtocolException.cs24
-rw-r--r--Impostor-dev/src/Impostor.Api/Extensions/SpanReaderExtensions.cs70
-rw-r--r--Impostor-dev/src/Impostor.Api/Extensions/SystemTypesExtensions.cs12
-rw-r--r--Impostor-dev/src/Impostor.Api/Games/Extensions/GameExtensions.cs42
-rw-r--r--Impostor-dev/src/Impostor.Api/Games/Extensions/GameManagerExtensions.cs14
-rw-r--r--Impostor-dev/src/Impostor.Api/Games/GameCode.cs74
-rw-r--r--Impostor-dev/src/Impostor.Api/Games/GameJoinError.cs48
-rw-r--r--Impostor-dev/src/Impostor.Api/Games/GameJoinResult.cs48
-rw-r--r--Impostor-dev/src/Impostor.Api/Games/IGame.cs88
-rw-r--r--Impostor-dev/src/Impostor.Api/Games/IGameCodeFactory.cs7
-rw-r--r--Impostor-dev/src/Impostor.Api/Games/Managers/IGameManager.cs11
-rw-r--r--Impostor-dev/src/Impostor.Api/Impostor.Api.csproj38
-rw-r--r--Impostor-dev/src/Impostor.Api/Impostor.Api.csproj.DotSettings8
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/AlterGameTags.cs7
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/ChatNoteType.cs7
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/Customization/ColorType.cs18
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/Customization/HatType.cs100
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/Customization/PetType.cs18
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/Customization/SkinType.cs22
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/DeathReason.cs9
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/DisconnectReason.cs54
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/FloatRange.cs22
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/GameCodeParser.cs142
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/GameKeywords.cs19
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/GameOptionsData.cs261
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/GameOverReason.cs15
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/GameStates.cs11
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/GameVersion.cs10
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/KillDistances.cs12
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/MapFlags.cs12
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/MapTypes.cs9
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/RegionInfo.cs48
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/ServerInfo.cs37
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/SystemTypeHelpers.cs28
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/SystemTypes.cs42
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/TaskBarUpdate.cs9
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/TaskTypes.cs49
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/TextBox.cs10
-rw-r--r--Impostor-dev/src/Impostor.Api/Innersloth/VentLocation.cs48
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/IClient.cs76
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/IClientPlayer.cs43
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/IConnection.cs7
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/IHazelConnection.cs41
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/IGameNet.cs18
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/IInnerNetObject.cs9
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerCustomNetworkTransform.cs15
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerPlayerPhysics.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerGameData.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerLobbyBehaviour.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs6
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs116
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs53
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerShipStatus.cs7
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerVoteBanSystem.cs7
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Inner/Objects/ITaskInfo.cs14
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/LimboStates.cs13
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Manager/IClientManager.cs9
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs27
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs20
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message04RemovePlayerC2S.cs16
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message08EndGameC2S.cs18
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs20
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message11KickPlayerC2S.cs16
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message16GetGameListC2S.cs18
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/IMessageReader.cs68
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriter.cs127
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriterProvider.cs16
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/MessageFlags.cs22
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/MessageType.cs32
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message00HostGameS2C.cs20
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message01JoinGameS2C.cs50
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message04RemovePlayerS2C.cs29
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message07JoinedGameS2C.cs31
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message10AlterGameS2C.cs26
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message11KickPlayerS2C.cs24
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message12WaitForHostS2C.cs23
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message13RedirectS2C.cs25
-rw-r--r--Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message16GetGameListS2C.cs46
-rw-r--r--Impostor-dev/src/Impostor.Api/Plugins/IPlugin.cs14
-rw-r--r--Impostor-dev/src/Impostor.Api/Plugins/IPluginStartup.cs12
-rw-r--r--Impostor-dev/src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs24
-rw-r--r--Impostor-dev/src/Impostor.Api/Plugins/PluginBase.cs22
-rw-r--r--Impostor-dev/src/Impostor.Api/ProjectRules.ruleset17
-rw-r--r--Impostor-dev/src/Impostor.Api/Properties/AssemblyInfo.cs3
-rw-r--r--Impostor-dev/src/Impostor.Api/Unity/Mathf.cs54
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/.gitignore1
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes.cs172
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled.cs220
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled_Improved.cs90
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Data/MessageWriter.cs311
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Data/Pool/MessageReader_Bytes_Pooled_ImprovedPolicy.cs26
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReaderOwner.cs22
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReader_Span.cs50
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Extensions/SpanExtensions.cs24
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Impostor.Benchmarks.csproj17
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Program.cs23
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Tests/EventManagerBenchmark.cs77
-rw-r--r--Impostor-dev/src/Impostor.Benchmarks/Tests/MessageReaderBenchmark.cs132
-rw-r--r--Impostor-dev/src/Impostor.Client.App/Impostor.Client.App.csproj16
-rw-r--r--Impostor-dev/src/Impostor.Client.App/Program.cs75
-rw-r--r--Impostor-dev/src/Impostor.Client/Impostor.Client.csproj12
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Connection.cs249
-rw-r--r--Impostor-dev/src/Impostor.Hazel/ConnectionListener.cs100
-rw-r--r--Impostor-dev/src/Impostor.Hazel/ConnectionState.cs23
-rw-r--r--Impostor-dev/src/Impostor.Hazel/ConnectionStatistics.cs566
-rw-r--r--Impostor-dev/src/Impostor.Hazel/DataReceivedEventArgs.cs26
-rw-r--r--Impostor-dev/src/Impostor.Hazel/DisconnectedEventArgs.cs25
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Extensions/ServiceProviderExtensions.cs21
-rw-r--r--Impostor-dev/src/Impostor.Hazel/HazelException.cs21
-rw-r--r--Impostor-dev/src/Impostor.Hazel/IPMode.cs24
-rw-r--r--Impostor-dev/src/Impostor.Hazel/IRecyclable.cs24
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Impostor.Hazel.csproj19
-rw-r--r--Impostor-dev/src/Impostor.Hazel/MessageReader.cs256
-rw-r--r--Impostor-dev/src/Impostor.Hazel/MessageReaderPolicy.cs27
-rw-r--r--Impostor-dev/src/Impostor.Hazel/MessageWriter.cs335
-rw-r--r--Impostor-dev/src/Impostor.Hazel/NetworkConnection.cs121
-rw-r--r--Impostor-dev/src/Impostor.Hazel/NetworkConnectionListener.cs21
-rw-r--r--Impostor-dev/src/Impostor.Hazel/NewConnectionEventArgs.cs24
-rw-r--r--Impostor-dev/src/Impostor.Hazel/ObjectPoolCustom.cs107
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Udp/SendOptionInternal.cs33
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcastListener.cs156
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcaster.cs79
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Udp/UdpClientConnection.cs225
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.KeepAlive.cs167
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.Reliable.cs491
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.cs312
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionListener.cs281
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionRateLimit.cs75
-rw-r--r--Impostor-dev/src/Impostor.Hazel/Udp/UdpServerConnection.cs97
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Directory.Build.props8
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj19
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Program.cs88
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/AmongUsModifier.cs247
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Configuration.cs65
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/ErrorEventArgs.cs14
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/SavedEventArgs.cs16
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Impostor.Patcher.Shared.csproj12
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/RegionInfo.cs48
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/ServerInfo.cs37
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/App.config7
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.Designer.cs141
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.cs114
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.resx2338
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Impostor.Patcher.WinForms.csproj22
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Program.cs17
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.Designer.cs69
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.resx117
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.Designer.cs26
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.settings7
-rw-r--r--Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/icon.icobin0 -> 132738 bytes
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Debugger/App.razor10
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Debugger/DebugPlugin.cs13
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Debugger/DebugPluginStartup.cs35
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj16
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Debugger/Pages/Index.razor69
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Debugger/Pages/_Host.cshtml19
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Debugger/Shared/MainLayout.razor5
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Debugger/_Imports.razor8
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Example/ExamplePlugin.cs33
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Example/ExamplePluginStartup.cs22
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Example/Handlers/GameEventListener.cs64
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs21
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs100
-rw-r--r--Impostor-dev/src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj11
-rw-r--r--Impostor-dev/src/Impostor.Server/Config/AntiCheatConfig.cs9
-rw-r--r--Impostor-dev/src/Impostor.Server/Config/DebugConfig.cs11
-rw-r--r--Impostor-dev/src/Impostor.Server/Config/DisconnectMessages.cs19
-rw-r--r--Impostor-dev/src/Impostor.Server/Config/ServerConfig.cs30
-rw-r--r--Impostor-dev/src/Impostor.Server/Config/ServerRedirectorConfig.cs24
-rw-r--r--Impostor-dev/src/Impostor.Server/Config/ServerRedirectorNode.cs9
-rw-r--r--Impostor-dev/src/Impostor.Server/Constants.cs8
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/EventHandler.cs24
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/EventManager.cs167
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/GameAlterEvent.cs18
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/GameCreatedEvent.cs15
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/GameDestroyedEvent.cs15
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/GameEndedEvent.cs19
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerJoinedEvent.cs19
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerLeftEvent.cs22
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/GameStartedEvent.cs15
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/GameStartingEvent.cs15
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs19
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingStartedEvent.cs19
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerChatEvent.cs26
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerCompletedTaskEvent.cs27
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerDestroyedEvent.cs23
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerExileEvent.cs23
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMovementEvent.cs24
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs26
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSetStartCounterEvent.cs26
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSpawnedEvent.cs23
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerStartMeetingEvent.cs26
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerVentEvent.cs30
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/MultiDisposable.cs26
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Register/IRegisteredEventListener.cs15
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Register/InvokedRegisteredEventListener.cs27
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Register/ManualRegisteredEventListener.cs30
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Register/RegisteredEventListener.cs166
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Register/TemporaryEventRegister.cs59
-rw-r--r--Impostor-dev/src/Impostor.Server/Events/Register/WrappedRegisteredEventListener.cs27
-rw-r--r--Impostor-dev/src/Impostor.Server/Extensions/MessageReaderExtensions.cs15
-rw-r--r--Impostor-dev/src/Impostor.Server/Extensions/NodeLocatorExtensions.cs13
-rw-r--r--Impostor-dev/src/Impostor.Server/Extensions/TypeExtensions.cs67
-rw-r--r--Impostor-dev/src/Impostor.Server/Impostor.Server.csproj58
-rw-r--r--Impostor-dev/src/Impostor.Server/Impostor.Server.csproj.DotSettings3
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Client.cs338
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/ClientBase.cs58
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs24
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs9
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs12
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs74
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs13
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs29
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs31
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs25
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs148
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs8
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs70
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs88
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs30
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs187
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs36
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs9
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs49
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs176
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs138
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs455
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs10
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs76
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs147
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs7
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs11
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs60
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs19
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs44
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs32
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs39
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs26
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs19
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs27
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs37
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs11
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs11
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs103
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs150
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs66
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs57
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs13
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs97
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs14
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs9
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs14
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs43
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs134
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs101
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs43
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs18
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs88
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs70
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs449
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs240
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs103
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs136
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.cs126
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs17
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs16
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs38
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs14
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs25
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs11
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs38
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs147
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs25
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs60
-rw-r--r--Impostor-dev/src/Impostor.Server/Program.cs207
-rw-r--r--Impostor-dev/src/Impostor.Server/ProjectRules.ruleset21
-rw-r--r--Impostor-dev/src/Impostor.Server/Properties/AssemblyInfo.cs5
-rw-r--r--Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs86
-rw-r--r--Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs201
-rw-r--r--Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs56
-rw-r--r--Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs18
-rw-r--r--Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs10
-rw-r--r--Impostor-dev/src/Impostor.Server/Utils/DotnetUtils.cs20
-rw-r--r--Impostor-dev/src/Impostor.Server/Utils/IpUtils.cs42
-rw-r--r--Impostor-dev/src/Impostor.Server/config-full.json29
-rw-r--r--Impostor-dev/src/Impostor.Server/config.json11
-rw-r--r--Impostor-dev/src/Impostor.Server/icon.icobin0 -> 132738 bytes
-rw-r--r--Impostor-dev/src/Impostor.Tests/Events/EventManagerTests.cs175
-rw-r--r--Impostor-dev/src/Impostor.Tests/GameCodeTests.cs29
-rw-r--r--Impostor-dev/src/Impostor.Tests/Hazel/MessageReaderTests.cs372
-rw-r--r--Impostor-dev/src/Impostor.Tests/Hazel/MessageWriterTests.cs38
-rw-r--r--Impostor-dev/src/Impostor.Tests/Impostor.Tests.csproj20
-rw-r--r--Impostor-dev/src/Impostor.Tools.Proxy/HexUtils.cs70
-rw-r--r--Impostor-dev/src/Impostor.Tools.Proxy/Impostor.Tools.Proxy.csproj17
-rw-r--r--Impostor-dev/src/Impostor.Tools.Proxy/Program.cs198
-rw-r--r--Impostor-dev/src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj16
-rw-r--r--Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs14
-rw-r--r--Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs31
-rw-r--r--Impostor-dev/src/Impostor.Tools.ServerReplay/Program.cs195
-rw-r--r--Impostor-dev/src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.datbin0 -> 47057 bytes
-rw-r--r--Impostor-dev/src/Impostor.sln177
336 files changed, 21087 insertions, 0 deletions
diff --git a/Impostor-dev/src/.editorconfig b/Impostor-dev/src/.editorconfig
new file mode 100644
index 0000000..ef7041d
--- /dev/null
+++ b/Impostor-dev/src/.editorconfig
@@ -0,0 +1,230 @@
+# top-most EditorConfig file
+root = true
+
+# Don't use tabs for indentation.
+[*]
+charset = utf-8
+end_of_line = crlf
+insert_final_newline = false
+indent_style = space
+
+# Code files
+[*.{cs,csx,vb,vbx}]
+indent_size = 4
+insert_final_newline = true
+
+# XML project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# XML config files
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+# JSON files
+[*.json]
+indent_size = 2
+
+# Dotnet code style settings:
+[*.{cs,vb}]
+
+# Sort using and Import directives with System.* appearing first
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+# Avoid "this." and "Me." if not necessary
+dotnet_style_qualification_for_field = false:refactoring
+dotnet_style_qualification_for_property = false:refactoring
+dotnet_style_qualification_for_method = false:refactoring
+dotnet_style_qualification_for_event = false:refactoring
+
+# Use language keywords instead of framework type names for type references
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# Suggest more modern language features when available
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+
+# Non-private static fields are PascalCase
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
+
+dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
+dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
+
+dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
+
+# Non-private readonly fields are PascalCase
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style
+
+dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
+dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
+
+dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
+
+# Constants are PascalCase
+dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
+dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
+
+dotnet_naming_symbols.constants.applicable_kinds = field, local
+dotnet_naming_symbols.constants.required_modifiers = const
+
+dotnet_naming_style.constant_style.capitalization = pascal_case
+
+# Static readonly fields are PascalCase
+dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
+dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
+
+dotnet_naming_symbols.static_fields.applicable_kinds = field
+dotnet_naming_symbols.static_fields.required_modifiers = static, readonly
+
+dotnet_naming_style.static_field_style.capitalization = pascal_case
+
+# Instance fields are camelCase and start with _
+dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
+dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
+
+dotnet_naming_symbols.instance_fields.applicable_kinds = field
+
+dotnet_naming_style.instance_field_style.capitalization = camel_case
+dotnet_naming_style.instance_field_style.required_prefix = _
+
+# Locals and parameters are camelCase
+dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
+dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
+
+dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
+
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+
+# Local functions are PascalCase
+dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+
+dotnet_naming_style.local_function_style.capitalization = pascal_case
+
+# By default, name items with PascalCase
+dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
+dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
+
+dotnet_naming_symbols.all_members.applicable_kinds = *
+
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+# Async methods should have "Async" suffix
+dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
+dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
+dotnet_naming_rule.async_methods_end_in_async.severity = warning
+
+dotnet_naming_symbols.any_async_methods.applicable_kinds = method
+dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
+dotnet_naming_symbols.any_async_methods.required_modifiers = async
+
+dotnet_naming_style.end_in_async.required_prefix =
+dotnet_naming_style.end_in_async.required_suffix = Async
+dotnet_naming_style.end_in_async.capitalization = pascal_case
+dotnet_naming_style.end_in_async.word_separator =
+
+# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}'
+dotnet_diagnostic.RS2008.severity = none
+
+# IDE0035: Remove unreachable code
+dotnet_diagnostic.IDE0035.severity = warning
+
+# IDE0036: Order modifiers
+dotnet_diagnostic.IDE0036.severity = warning
+
+# IDE0043: Format string contains invalid placeholder
+dotnet_diagnostic.IDE0043.severity = warning
+
+# IDE0044: Make field readonly
+dotnet_diagnostic.IDE0044.severity = warning
+
+# CSharp code style settings:
+[*.cs]
+# Newline settings
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+
+# Prefer "var" everywhere
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+
+# Prefer method-like constructs to have a block body
+csharp_style_expression_bodied_methods = false:none
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_operators = false:none
+
+# Prefer property-like constructs to have an expression-body
+csharp_style_expression_bodied_properties = true:none
+csharp_style_expression_bodied_indexers = true:none
+csharp_style_expression_bodied_accessors = true:none
+
+# Suggest more modern language features when available
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = do_not_ignore
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Blocks are allowed
+csharp_prefer_braces = true:silent
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+# warning RS0037: PublicAPI.txt is missing '#nullable enable'
+dotnet_diagnostic.RS0037.severity = none
diff --git a/Impostor-dev/src/.gitattributes b/Impostor-dev/src/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/Impostor-dev/src/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/Impostor-dev/src/.gitignore b/Impostor-dev/src/.gitignore
new file mode 100644
index 0000000..e0838ba
--- /dev/null
+++ b/Impostor-dev/src/.gitignore
@@ -0,0 +1,263 @@
+config.*.json
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+#*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Events/Attributes/EventListenerAttribute.cs b/Impostor-dev/src/Impostor.Api/Events/Attributes/EventListenerAttribute.cs
new file mode 100644
index 0000000..b31d2d1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Attributes/EventListenerAttribute.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace Impostor.Api.Events
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ public class EventListenerAttribute : Attribute
+ {
+ public EventListenerAttribute(EventPriority priority = EventPriority.Normal)
+ {
+ Priority = priority;
+ }
+
+ public EventListenerAttribute(Type @event, EventPriority priority = EventPriority.Normal)
+ {
+ Priority = priority;
+ Event = @event;
+ }
+
+ /// <summary>
+ /// The priority of the event listener.
+ /// </summary>
+ public EventPriority Priority { get; set; }
+
+ /// <summary>
+ /// The events that the listener is listening to.
+ /// </summary>
+ public Type? Event { get; set; }
+
+ /// <summary>
+ /// If set to true, the listener will be called regardless of the <see cref="IEventCancelable.IsCancelled"/>.
+ /// </summary>
+ public bool IgnoreCancelled { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Events/EventPriority.cs b/Impostor-dev/src/Impostor.Api/Events/EventPriority.cs
new file mode 100644
index 0000000..7acdbeb
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/EventPriority.cs
@@ -0,0 +1,12 @@
+namespace Impostor.Api.Events
+{
+ public enum EventPriority
+ {
+ Lowest = 0,
+ Low = 1,
+ Normal = 2,
+ High = 3,
+ Highest = 4,
+ Monitor = 5,
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameAlterEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameAlterEvent.cs
new file mode 100644
index 0000000..18956ea
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameAlterEvent.cs
@@ -0,0 +1,7 @@
+namespace Impostor.Api.Events
+{
+ public interface IGameAlterEvent : IGameEvent
+ {
+ bool IsPublic { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameCreatedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameCreatedEvent.cs
new file mode 100644
index 0000000..ef45f1d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameCreatedEvent.cs
@@ -0,0 +1,11 @@
+using Impostor.Api.Games;
+
+namespace Impostor.Api.Events
+{
+ /// <summary>
+ /// Called whenever a new <see cref="IGame"/> is created.
+ /// </summary>
+ public interface IGameCreatedEvent : IGameEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameDestroyedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameDestroyedEvent.cs
new file mode 100644
index 0000000..7ff7b46
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameDestroyedEvent.cs
@@ -0,0 +1,11 @@
+using Impostor.Api.Games;
+
+namespace Impostor.Api.Events
+{
+ /// <summary>
+ /// Called whenever a new <see cref="IGame"/> is destroyed.
+ /// </summary>
+ public interface IGameDestroyedEvent : IGameEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameEndedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameEndedEvent.cs
new file mode 100644
index 0000000..d8ae159
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameEndedEvent.cs
@@ -0,0 +1,9 @@
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Events
+{
+ public interface IGameEndedEvent : IGameEvent
+ {
+ public GameOverReason GameOverReason { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameEvent.cs
new file mode 100644
index 0000000..c9ae579
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameEvent.cs
@@ -0,0 +1,12 @@
+using Impostor.Api.Games;
+
+namespace Impostor.Api.Events
+{
+ public interface IGameEvent : IEvent
+ {
+ /// <summary>
+ /// Gets the <see cref="IGame"/> this event belongs to.
+ /// </summary>
+ IGame Game { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerJoinedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerJoinedEvent.cs
new file mode 100644
index 0000000..921568e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerJoinedEvent.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Events
+{
+ public interface IGamePlayerJoinedEvent : IGameEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerLeftEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerLeftEvent.cs
new file mode 100644
index 0000000..21d8b7c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerLeftEvent.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Events
+{
+ public interface IGamePlayerLeftEvent : IGameEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartedEvent.cs
new file mode 100644
index 0000000..b6e5111
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartedEvent.cs
@@ -0,0 +1,9 @@
+namespace Impostor.Api.Events
+{
+ /// <summary>
+ /// The game is started here and players have been initialized.
+ /// </summary>
+ public interface IGameStartedEvent : IGameEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartingEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartingEvent.cs
new file mode 100644
index 0000000..5998bf2
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartingEvent.cs
@@ -0,0 +1,11 @@
+namespace Impostor.Api.Events
+{
+ /// <summary>
+ /// Called when the game is going to start.
+ /// When this is called, not all players are initialized properly yet.
+ /// If you want to get correct player states, use <see cref="IGameStartedEvent"/>.
+ /// </summary>
+ public interface IGameStartingEvent : IGameEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs
new file mode 100644
index 0000000..a217580
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Events.Meeting
+{
+ public interface IMeetingEndedEvent : IMeetingEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEvent.cs
new file mode 100644
index 0000000..4461318
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEvent.cs
@@ -0,0 +1,9 @@
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Api.Events.Meeting
+{
+ public interface IMeetingEvent : IGameEvent
+ {
+ IInnerMeetingHud MeetingHud { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingStartedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingStartedEvent.cs
new file mode 100644
index 0000000..a237fff
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingStartedEvent.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Events.Meeting
+{
+ public interface IMeetingStartedEvent : IMeetingEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerChatEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerChatEvent.cs
new file mode 100644
index 0000000..52efe96
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerChatEvent.cs
@@ -0,0 +1,10 @@
+namespace Impostor.Api.Events.Player
+{
+ public interface IPlayerChatEvent : IPlayerEvent
+ {
+ /// <summary>
+ /// Gets the message sent by the player.
+ /// </summary>
+ string Message { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerCompletedTaskEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerCompletedTaskEvent.cs
new file mode 100644
index 0000000..78ccd2d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerCompletedTaskEvent.cs
@@ -0,0 +1,10 @@
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Api.Events.Player
+{
+ public interface IPlayerCompletedTaskEvent : IPlayerEvent
+ {
+ ITaskInfo Task { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerDestroyedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerDestroyedEvent.cs
new file mode 100644
index 0000000..ac80d64
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerDestroyedEvent.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Events.Player
+{
+ public interface IPlayerDestroyedEvent : IPlayerEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerEvent.cs
new file mode 100644
index 0000000..247fe64
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerEvent.cs
@@ -0,0 +1,19 @@
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Api.Events.Player
+{
+ public interface IPlayerEvent : IGameEvent
+ {
+ /// <summary>
+ /// Gets the <see cref="IClientPlayer"/> that triggered this <see cref="IPlayerEvent"/>.
+ /// </summary>
+ IClientPlayer ClientPlayer { get; }
+
+ /// <summary>
+ /// Gets the networked <see cref="IInnerPlayerControl"/> that triggered this <see cref="IPlayerEvent"/>.
+ /// This <see cref="IInnerPlayerControl"/> belongs to the <see cref="IClientPlayer"/>.
+ /// </summary>
+ IInnerPlayerControl PlayerControl { get; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerExileEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerExileEvent.cs
new file mode 100644
index 0000000..65ade4e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerExileEvent.cs
@@ -0,0 +1,9 @@
+namespace Impostor.Api.Events.Player
+{
+ /// <summary>
+ /// Called whenever a player gets exiled (voted out).
+ /// </summary>
+ public interface IPlayerExileEvent : IPlayerEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs
new file mode 100644
index 0000000..c47c00b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs
@@ -0,0 +1,12 @@
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Api.Events.Player
+{
+ public interface IPlayerMurderEvent : IPlayerEvent
+ {
+ /// <summary>
+ /// Gets the player who got murdered.
+ /// </summary>
+ IInnerPlayerControl Victim { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSetStartCounterEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSetStartCounterEvent.cs
new file mode 100644
index 0000000..c03d782
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSetStartCounterEvent.cs
@@ -0,0 +1,10 @@
+namespace Impostor.Api.Events.Player
+{
+ public interface IPlayerSetStartCounterEvent : IPlayerEvent
+ {
+ /// <summary>
+ /// Gets the current time of the start counter.
+ /// </summary>
+ byte SecondsLeft { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSpawnedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSpawnedEvent.cs
new file mode 100644
index 0000000..a3be654
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSpawnedEvent.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Events.Player
+{
+ public interface IPlayerSpawnedEvent : IPlayerEvent
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerStartMeetingEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerStartMeetingEvent.cs
new file mode 100644
index 0000000..1a28115
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerStartMeetingEvent.cs
@@ -0,0 +1,12 @@
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Api.Events.Player
+{
+ public interface IPlayerStartMeetingEvent : IPlayerEvent
+ {
+ /// <summary>
+ /// Gets the player who's body got reported. Is null when the meeting started by Emergency call button
+ /// </summary>
+ IInnerPlayerControl? Body { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerVentEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerVentEvent.cs
new file mode 100644
index 0000000..81f178b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerVentEvent.cs
@@ -0,0 +1,17 @@
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Events.Player
+{
+ public interface IPlayerVentEvent : IPlayerEvent
+ {
+ /// <summary>
+ /// Gets get the id of the used vent.
+ /// </summary>
+ public VentLocation VentId { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the vent was entered or exited.
+ /// </summary>
+ public bool VentEnter { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Events/IEvent.cs b/Impostor-dev/src/Impostor.Api/Events/IEvent.cs
new file mode 100644
index 0000000..796898e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/IEvent.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Events
+{
+ public interface IEvent
+ {
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Events/IEventCancelable.cs b/Impostor-dev/src/Impostor.Api/Events/IEventCancelable.cs
new file mode 100644
index 0000000..319f02a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/IEventCancelable.cs
@@ -0,0 +1,10 @@
+namespace Impostor.Api.Events
+{
+ public interface IEventCancelable : IEvent
+ {
+ /// <summary>
+ /// True if the event was cancelled.
+ /// </summary>
+ bool IsCancelled { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Events/IEventListener.cs b/Impostor-dev/src/Impostor.Api/Events/IEventListener.cs
new file mode 100644
index 0000000..76392fc
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/IEventListener.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Events
+{
+ public interface IEventListener
+ {
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Events/IManualEventListener.cs b/Impostor-dev/src/Impostor.Api/Events/IManualEventListener.cs
new file mode 100644
index 0000000..b5c140e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/IManualEventListener.cs
@@ -0,0 +1,13 @@
+using System.Threading.Tasks;
+
+namespace Impostor.Api.Events
+{
+ public interface IManualEventListener : IEventListener
+ {
+ public bool CanExecute<T>();
+
+ public ValueTask Execute(IEvent @event);
+
+ EventPriority Priority { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Events/Managers/IEventManager.cs b/Impostor-dev/src/Impostor.Api/Events/Managers/IEventManager.cs
new file mode 100644
index 0000000..07a7f7c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Events/Managers/IEventManager.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Impostor.Api.Events.Managers
+{
+ public interface IEventManager
+ {
+ /// <summary>
+ /// Register a temporary event listener.
+ /// </summary>
+ /// <param name="listener">Event listener.</param>
+ /// <param name="invoker">Middleware between the events, which can be used to swap to the correct thread dispatcher.</param>
+ /// <returns>Disposable that unregisters the callback from the event manager.</returns>
+ /// <typeparam name="TListener">Type of the event listener.</typeparam>
+ IDisposable RegisterListener<TListener>(TListener listener, Func<Func<Task>, Task>? invoker = null)
+ where TListener : IEventListener;
+
+ /// <summary>
+ /// Returns true if an event with the type <see cref="TEvent"/> is registered.
+ /// </summary>
+ /// <returns>True if the <see cref="TEvent"/> is registered.</returns>
+ /// <typeparam name="TEvent">Type of the event.</typeparam>
+ bool IsRegistered<TEvent>()
+ where TEvent : IEvent;
+
+ /// <summary>
+ /// Call all the event listeners for the type <see cref="TEvent"/>.
+ /// </summary>
+ /// <param name="event">The event argument.</param>
+ /// <typeparam name="TEvent">Type of the event.</typeparam>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+ ValueTask CallAsync<TEvent>(TEvent @event)
+ where TEvent : IEvent;
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorCheatException.cs b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorCheatException.cs
new file mode 100644
index 0000000..8eb72f8
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorCheatException.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Impostor.Api
+{
+ public class ImpostorCheatException : ImpostorException
+ {
+ public ImpostorCheatException()
+ {
+ }
+
+ protected ImpostorCheatException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+
+ public ImpostorCheatException(string? message) : base(message)
+ {
+ }
+
+ public ImpostorCheatException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorConfigException.cs b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorConfigException.cs
new file mode 100644
index 0000000..1e59a9b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorConfigException.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Impostor.Api
+{
+ public class ImpostorConfigException : ImpostorException
+ {
+ public ImpostorConfigException()
+ {
+ }
+
+ protected ImpostorConfigException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+
+ public ImpostorConfigException(string? message) : base(message)
+ {
+ }
+
+ public ImpostorConfigException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorException.cs b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorException.cs
new file mode 100644
index 0000000..188c50e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorException.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Impostor.Api
+{
+ public class ImpostorException : Exception
+ {
+ public ImpostorException()
+ {
+ }
+
+ protected ImpostorException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+
+ public ImpostorException(string? message) : base(message)
+ {
+ }
+
+ public ImpostorException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorProtocolException.cs b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorProtocolException.cs
new file mode 100644
index 0000000..864602d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorProtocolException.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Impostor.Api
+{
+ public class ImpostorProtocolException : ImpostorException
+ {
+ public ImpostorProtocolException()
+ {
+ }
+
+ protected ImpostorProtocolException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+
+ public ImpostorProtocolException(string? message) : base(message)
+ {
+ }
+
+ public ImpostorProtocolException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Extensions/SpanReaderExtensions.cs b/Impostor-dev/src/Impostor.Api/Extensions/SpanReaderExtensions.cs
new file mode 100644
index 0000000..c103ee7
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Extensions/SpanReaderExtensions.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
+
+namespace Impostor.Api
+{
+ /// <summary>
+ /// Priovides a StreamReader-like api throught extensions
+ /// </summary>
+ public static class SpanReaderExtensions
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static byte ReadByte(this ref ReadOnlySpan<byte> input)
+ {
+ var original = Advance<byte>(ref input);
+ return original[0];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int ReadInt32(this ref ReadOnlySpan<byte> input)
+ {
+ var original = Advance<int>(ref input);
+ return BinaryPrimitives.ReadInt32LittleEndian(original);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint ReadUInt32(this ref ReadOnlySpan<byte> input)
+ {
+ var original = Advance<uint>(ref input);
+ return BinaryPrimitives.ReadUInt32LittleEndian(original);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ReadSingle(this ref ReadOnlySpan<byte> input)
+ {
+ var original = Advance<float>(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<byte> input)
+ {
+ return input.ReadByte() != 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static unsafe float Int32BitsToSingle(int value)
+ {
+ return *((float*)&value);
+ }
+
+ /// <summary>
+ /// Advances the position of <see cref="input"/> by the size of <see cref="T"/>.
+ /// </summary>
+ /// <typeparam name="T">Type that will be read.</typeparam>
+ /// <param name="input">input "stream"/span.</param>
+ /// <returns>The original input</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static unsafe ReadOnlySpan<byte> Advance<T>(ref ReadOnlySpan<byte> input)
+ where T : unmanaged
+ {
+ var original = input;
+ input = input.Slice(sizeof(T));
+ return original;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Extensions/SystemTypesExtensions.cs b/Impostor-dev/src/Impostor.Api/Extensions/SystemTypesExtensions.cs
new file mode 100644
index 0000000..68f1efd
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Extensions/SystemTypesExtensions.cs
@@ -0,0 +1,12 @@
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api
+{
+ public static class SystemTypesExtensions
+ {
+ public static string GetFriendlyName(this SystemTypes type)
+ {
+ return SystemTypeHelpers.Names[(int)type];
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Games/Extensions/GameExtensions.cs b/Impostor-dev/src/Impostor.Api/Games/Extensions/GameExtensions.cs
new file mode 100644
index 0000000..a736978
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Games/Extensions/GameExtensions.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Threading.Tasks;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Api.Games
+{
+ public static class GameExtensions
+ {
+ public static ValueTask SendToAllExceptAsync(this IGame game, IMessageWriter writer, LimboStates states, int? id)
+ {
+ return id.HasValue
+ ? game.SendToAllExceptAsync(writer, id.Value, states)
+ : game.SendToAllAsync(writer, states);
+ }
+
+ public static ValueTask SendToAllExceptAsync(this IGame game, IMessageWriter writer, LimboStates states, IClient client)
+ {
+ if (client == null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return game.SendToAllExceptAsync(writer, client.Id, states);
+ }
+
+ public static ValueTask SendToAsync(this IGame game, IMessageWriter writer, IClient client)
+ {
+ if (client == null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return game.SendToAsync(writer, client.Id);
+ }
+
+ public static ValueTask SendToAsync(this IGame game, IMessageWriter writer, IClientPlayer player)
+ {
+ return game.SendToAsync(writer, player.Client);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Games/Extensions/GameManagerExtensions.cs b/Impostor-dev/src/Impostor.Api/Games/Extensions/GameManagerExtensions.cs
new file mode 100644
index 0000000..9a5a2b4
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Games/Extensions/GameManagerExtensions.cs
@@ -0,0 +1,14 @@
+using System.Linq;
+using Impostor.Api.Games.Managers;
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Games
+{
+ public static class GameManagerExtensions
+ {
+ public static int GetGameCount(this IGameManager manager, MapFlags map)
+ {
+ return manager.Games.Count(game => map.HasFlag((MapFlags)(1 << game.Options.MapId)));
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Games/GameCode.cs b/Impostor-dev/src/Impostor.Api/Games/GameCode.cs
new file mode 100644
index 0000000..2c30e7e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Games/GameCode.cs
@@ -0,0 +1,74 @@
+using System;
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Games
+{
+ public readonly struct GameCode : IEquatable<GameCode>
+ {
+ public GameCode(int value)
+ {
+ Value = value;
+ Code = GameCodeParser.IntToGameName(value);
+ }
+
+ public GameCode(string code)
+ {
+ Value = GameCodeParser.GameNameToInt(code);
+ Code = code;
+ }
+
+ public string Code { get; }
+
+ public int Value { get; }
+
+ public static implicit operator string(GameCode code) => code.Code;
+
+ public static implicit operator int(GameCode code) => code.Value;
+
+ public static implicit operator GameCode(string code) => From(code);
+
+ public static implicit operator GameCode(int value) => From(value);
+
+ public static bool operator ==(GameCode left, GameCode right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(GameCode left, GameCode right)
+ {
+ return !left.Equals(right);
+ }
+
+ public static GameCode Create()
+ {
+ return new GameCode(GameCodeParser.GenerateCode(6));
+ }
+
+ public static GameCode From(int value) => new GameCode(value);
+
+ public static GameCode From(string value) => new GameCode(value);
+
+ /// <inheritdoc/>
+ public bool Equals(GameCode other)
+ {
+ return Code == other.Code && Value == other.Value;
+ }
+
+ /// <inheritdoc/>
+ public override bool Equals(object? obj)
+ {
+ return obj is GameCode other && Equals(other);
+ }
+
+ /// <inheritdoc/>
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Code, Value);
+ }
+
+ public override string ToString()
+ {
+ return Code;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Games/GameJoinError.cs b/Impostor-dev/src/Impostor.Api/Games/GameJoinError.cs
new file mode 100644
index 0000000..4889ea9
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Games/GameJoinError.cs
@@ -0,0 +1,48 @@
+namespace Impostor.Api.Games
+{
+ public enum GameJoinError
+ {
+ /// <summary>
+ /// No error occured while joining the game.
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// The client is not registered in the client manager.
+ /// </summary>
+ InvalidClient,
+
+ /// <summary>
+ /// The client has been banned from the game.
+ /// </summary>
+ Banned,
+
+ /// <summary>
+ /// The game is full.
+ /// </summary>
+ GameFull,
+
+ /// <summary>
+ /// The limbo state of the player is incorrect.
+ /// </summary>
+ InvalidLimbo,
+
+ /// <summary>
+ /// The game is already started.
+ /// </summary>
+ GameStarted,
+
+ /// <summary>
+ /// The game has been destroyed.
+ /// </summary>
+ GameDestroyed,
+
+ /// <summary>
+ /// Custom error by a plugin.
+ /// </summary>
+ /// <remarks>
+ /// A custom message can be set in <see cref="GameJoinResult.Message"/>.
+ /// </remarks>
+ Custom,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Games/GameJoinResult.cs b/Impostor-dev/src/Impostor.Api/Games/GameJoinResult.cs
new file mode 100644
index 0000000..b33a2b6
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Games/GameJoinResult.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Impostor.Api.Net;
+
+namespace Impostor.Api.Games
+{
+ public readonly struct GameJoinResult
+ {
+ private GameJoinResult(GameJoinError error, string? message = null, IClientPlayer? player = null)
+ {
+ Error = error;
+ Message = message;
+ Player = player;
+ }
+
+ public GameJoinError Error { get; }
+
+ public bool IsSuccess => Error == GameJoinError.None;
+
+ public bool IsCustomError => Error == GameJoinError.Custom;
+
+ [MemberNotNullWhen(true, nameof(IsCustomError))]
+ public string? Message { get; }
+
+ [MemberNotNullWhen(true, nameof(IsSuccess))]
+ public IClientPlayer? Player { get; }
+
+ public static GameJoinResult CreateCustomError(string message)
+ {
+ return new GameJoinResult(GameJoinError.Custom, message);
+ }
+
+ public static GameJoinResult CreateSuccess(IClientPlayer player)
+ {
+ return new GameJoinResult(GameJoinError.None, player: player);
+ }
+
+ public static GameJoinResult FromError(GameJoinError error)
+ {
+ if (error == GameJoinError.Custom)
+ {
+ throw new InvalidOperationException($"Custom errors should provide a message, use {nameof(CreateCustomError)} instead.");
+ }
+
+ return new GameJoinResult(error);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Games/IGame.cs b/Impostor-dev/src/Impostor.Api/Games/IGame.cs
new file mode 100644
index 0000000..ad71986
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Games/IGame.cs
@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner;
+using Impostor.Api.Net.Inner.Objects;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Api.Games
+{
+ public interface IGame
+ {
+ GameOptionsData Options { get; }
+
+ GameCode Code { get; }
+
+ GameStates GameState { get; }
+
+ IGameNet GameNet { get; }
+
+ IEnumerable<IClientPlayer> Players { get; }
+
+ IPEndPoint PublicIp { get; }
+
+ int PlayerCount { get; }
+
+ IClientPlayer Host { get; }
+
+ bool IsPublic { get; }
+
+ IDictionary<object, object> Items { get; }
+
+ int HostId { get; }
+
+ IClientPlayer GetClientPlayer(int clientId);
+
+ /// <summary>
+ /// Adds an <see cref="IPAddress"/> to the ban list of this game.
+ /// Prevents all future joins from this <see cref="IPAddress"/>.
+ ///
+ /// This does not kick the player with that <see cref="IPAddress"/> from the lobby.
+ /// </summary>
+ /// <param name="ipAddress">
+ /// The <see cref="IPAddress"/> to ban.
+ /// </param>
+ void BanIp(IPAddress ipAddress);
+
+ /// <summary>
+ /// Syncs the internal <see cref="GameOptionsData"/> to all players.
+ /// Necessary to do if you modified it, otherwise it won't be used.
+ /// </summary>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+ ValueTask SyncSettingsAsync();
+
+ /// <summary>
+ /// Sets the specified list as Impostor on all connected players.
+ /// </summary>
+ /// <param name="players">List of players to be Impostor.</param>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+ ValueTask SetInfectedAsync(IEnumerable<IInnerPlayerControl> players);
+
+ /// <summary>
+ /// Send the message to all players.
+ /// </summary>
+ /// <param name="writer">Message to send.</param>
+ /// <param name="states">Required limbo state of the player.</param>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+ ValueTask SendToAllAsync(IMessageWriter writer, LimboStates states = LimboStates.NotLimbo);
+
+ /// <summary>
+ /// Send the message to all players except one.
+ /// </summary>
+ /// <param name="writer">Message to send.</param>
+ /// <param name="senderId">The player to exclude from sending the message.</param>
+ /// <param name="states">Required limbo state of the player.</param>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+ ValueTask SendToAllExceptAsync(IMessageWriter writer, int senderId, LimboStates states = LimboStates.NotLimbo);
+
+ /// <summary>
+ /// Send a message to a specific player.
+ /// </summary>
+ /// <param name="writer">Message to send.</param>
+ /// <param name="id">ID of the client.</param>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+ ValueTask SendToAsync(IMessageWriter writer, int id);
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Games/IGameCodeFactory.cs b/Impostor-dev/src/Impostor.Api/Games/IGameCodeFactory.cs
new file mode 100644
index 0000000..f264fe0
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Games/IGameCodeFactory.cs
@@ -0,0 +1,7 @@
+namespace Impostor.Api.Games
+{
+ public interface IGameCodeFactory
+ {
+ GameCode Create();
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Games/Managers/IGameManager.cs b/Impostor-dev/src/Impostor.Api/Games/Managers/IGameManager.cs
new file mode 100644
index 0000000..10a05f0
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Games/Managers/IGameManager.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace Impostor.Api.Games.Managers
+{
+ public interface IGameManager
+ {
+ IEnumerable<IGame> Games { get; }
+
+ IGame? Find(GameCode code);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj b/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj
new file mode 100644
index 0000000..8ad2582
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj
@@ -0,0 +1,38 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <CodeAnalysisRuleSet>ProjectRules.ruleset</CodeAnalysisRuleSet>
+ <LangVersion>9</LangVersion>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <Nullable>enable</Nullable>
+ <Version>1.0.0</Version>
+ <IncludeSymbols>true</IncludeSymbols>
+ <SymbolPackageFormat>snupkg</SymbolPackageFormat>
+ <AssemblyName>Impostor.Api</AssemblyName>
+ <AssemblyTitle>Impostor.Api</AssemblyTitle>
+ <Authors>AeonLucid</Authors>
+ <Description>An api library for Impostor, an Among Us private server. You need this package to write plugins for Impostor.</Description>
+ <PackageId>Impostor.Api</PackageId>
+ <PackageTags>Among Us;Impostor;Impostor Plugin</PackageTags>
+ <PackageIconUrl>https://raw.githubusercontent.com/Impostor/Impostor/dev/docs/images/logo_64.png</PackageIconUrl>
+ <PackageProjectUrl>https://github.com/Impostor/Impostor</PackageProjectUrl>
+ <RepositoryType>git</RepositoryType>
+ <RepositoryUrl>https://github.com/Impostor/Impostor</RepositoryUrl>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Nullable" Version="1.3.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+
+</Project> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj.DotSettings b/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj.DotSettings
new file mode 100644
index 0000000..2df7445
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj.DotSettings
@@ -0,0 +1,8 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=attributes/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cattributes/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cgame/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=games_005Cextensions/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=net_005Cextensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/AlterGameTags.cs b/Impostor-dev/src/Impostor.Api/Innersloth/AlterGameTags.cs
new file mode 100644
index 0000000..46d1b2e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/AlterGameTags.cs
@@ -0,0 +1,7 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum AlterGameTags : byte
+ {
+ ChangePrivacy = 1,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/ChatNoteType.cs b/Impostor-dev/src/Impostor.Api/Innersloth/ChatNoteType.cs
new file mode 100644
index 0000000..c163601
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/ChatNoteType.cs
@@ -0,0 +1,7 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum ChatNoteType : byte
+ {
+ DidVote = 0,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/Customization/ColorType.cs b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/ColorType.cs
new file mode 100644
index 0000000..fc7be4c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/ColorType.cs
@@ -0,0 +1,18 @@
+namespace Impostor.Api.Innersloth.Customization
+{
+ public enum ColorType : byte
+ {
+ Red = 0,
+ Blue = 1,
+ Green = 2,
+ Pink = 3,
+ Orange = 4,
+ Yellow = 5,
+ Black = 6,
+ White = 7,
+ Purple = 8,
+ Brown = 9,
+ Cyan = 10,
+ Lime = 11,
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/Customization/HatType.cs b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/HatType.cs
new file mode 100644
index 0000000..5e0a3ef
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/HatType.cs
@@ -0,0 +1,100 @@
+namespace Impostor.Api.Innersloth.Customization
+{
+ public enum HatType
+ {
+ NoHat = 0,
+ Astronaut = 1,
+ BaseballCap = 2,
+ BrainSlug = 3,
+ BushHat = 4,
+ CaptainsHat = 5,
+ DoubleTopHat = 6,
+ Flowerpot = 7,
+ Goggles = 8,
+ HardHat = 9,
+ Military = 10,
+ PaperHat = 11,
+ PartyHat = 12,
+ Police = 13,
+ Stethescope = 14,
+ TopHat = 15,
+ TowelWizard = 16,
+ Ushanka = 17,
+ Viking = 18,
+ WallCap = 19,
+ Snowman = 20,
+ Reindeer = 21,
+ Lights = 22,
+ Santa = 23,
+ Tree = 24,
+ Present = 25,
+ Candycanes = 26,
+ ElfHat = 27,
+ NewYears2018 = 28,
+ WhiteHat = 29,
+ Crown = 30,
+ Eyebrows = 31,
+ HaloHat = 32,
+ HeroCap = 33,
+ PipCap = 34,
+ PlungerHat = 35,
+ ScubaHat = 36,
+ StickminHat = 37,
+ StrawHat = 38,
+ TenGallonHat = 39,
+ ThirdEyeHat = 40,
+ ToiletPaperHat = 41,
+ Toppat = 42,
+ Fedora = 43,
+ Goggles2 = 44,
+ Headphones = 45,
+ MaskHat = 46,
+ PaperMask = 47,
+ Security = 48,
+ StrapHat = 49,
+ Banana = 50,
+ Beanie = 51,
+ Bear = 52,
+ Cheese = 53,
+ Cherry = 54,
+ Egg = 55,
+ Fedora2 = 56,
+ Flamingo = 57,
+ FlowerPin = 58,
+ Helmet = 59,
+ Plant = 60,
+ BatEyes = 61,
+ BatWings = 62,
+ Horns = 63,
+ Mohawk = 64,
+ Pumpkin = 65,
+ ScaryBag = 66,
+ Witch = 67,
+ Wolf = 68,
+ Pirate = 69,
+ Plague = 70,
+ Machete = 71,
+ Fred = 72,
+ MinerCap = 73,
+ WinterHat = 74,
+ Archae = 75,
+ Antenna = 76,
+ Balloon = 77,
+ BirdNest = 78,
+ BlackBelt = 79,
+ Caution = 80,
+ Chef = 81,
+ CopHat = 82,
+ DoRag = 83,
+ DumSticker = 84,
+ Fez = 85,
+ GeneralHat = 86,
+ GreyThing = 87,
+ HunterCap = 88,
+ JungleHat = 89,
+ MiniCrewmate = 90,
+ NinjaMask = 91,
+ RamHorns = 92,
+ Snowman2 = 93,
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/Customization/PetType.cs b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/PetType.cs
new file mode 100644
index 0000000..456e327
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/PetType.cs
@@ -0,0 +1,18 @@
+namespace Impostor.Api.Innersloth.Customization
+{
+ public enum PetType
+ {
+ NoPet = 0,
+ Alien = 1,
+ Crewmate = 2,
+ Doggy = 3,
+ Stickmin = 4,
+ Hamster = 5,
+ Robot = 6,
+ Ufo = 7,
+ Ellie = 8,
+ Squig = 9,
+ Bedcrab = 10,
+ Glitch = 11,
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/Customization/SkinType.cs b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/SkinType.cs
new file mode 100644
index 0000000..35da312
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/SkinType.cs
@@ -0,0 +1,22 @@
+namespace Impostor.Api.Innersloth.Customization
+{
+ public enum SkinType : byte
+ {
+ None = 0,
+ Astro = 1,
+ Capt = 2,
+ Mech = 3,
+ Military = 4,
+ Police = 5,
+ Science = 6,
+ SuitB = 7,
+ SuitW = 8,
+ Wall = 9,
+ Hazmat = 10,
+ Security = 11,
+ Tarmac = 12,
+ Miner = 13,
+ Winter = 14,
+ Archae = 15,
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/DeathReason.cs b/Impostor-dev/src/Impostor.Api/Innersloth/DeathReason.cs
new file mode 100644
index 0000000..a07ac05
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/DeathReason.cs
@@ -0,0 +1,9 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum DeathReason
+ {
+ Exile = 0,
+ Kill = 1,
+ Disconnect = 2,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/DisconnectReason.cs b/Impostor-dev/src/Impostor.Api/Innersloth/DisconnectReason.cs
new file mode 100644
index 0000000..9526f58
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/DisconnectReason.cs
@@ -0,0 +1,54 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum DisconnectReason
+ {
+ ExitGame = 0,
+ // The game you tried to join is full.
+ // Check with the host to see if you can join next round.
+ GameFull = 1,
+ // The game you tried to join already started.
+ // Check with the host to see if you can join next round.
+ GameStarted = 2,
+ // Could not find the game you're looking for.
+ GameMissing = 3,
+ IncorrectGame = 18,
+ // For these a message can be given, specifying an empty message shows
+ // "An unknown error disconnected you from the server."
+ CustomMessage1 = 4,
+ Custom = 8,
+ // CustomMessage3 = 11,
+ // CustomMessage4 = 12,
+ // CustomMessage5 = 13,
+ // CustomMessage6 = 14,
+ // CustomMessage7 = 15,
+ // You are running an older version of the game.
+ // Please update to play with others.
+ IncorrectVersion = 5,
+ // You cannot rejoin that room.
+ // You were banned
+ Banned = 6,
+ // You can rejoin if the room hasn't started
+ // You were kicked
+ Kicked = 7,
+ // You were banned for hacking.
+ // Please stop.
+ Hacking = 10,
+ Destroy = 16,
+ // You disconnected from the host.
+ // If this happens often, check your WiFi strength.
+ //
+ // You disconnected from the server.
+ // If this happens often, check your network strength.
+ // This may also be a server issue.
+ Error = 17,
+ // The server stopped this game. Possibly due to inactivity.
+ ServerRequest = 19,
+ // The Among Us servers are overloaded.
+ // Sorry! Please try again later!
+ ServerFull = 20,
+ FocusLostBackground = 207,
+ IntentionalLeaving = 208,
+ FocusLost = 209,
+ NewConnection = 210,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/FloatRange.cs b/Impostor-dev/src/Impostor.Api/Innersloth/FloatRange.cs
new file mode 100644
index 0000000..c8a0824
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/FloatRange.cs
@@ -0,0 +1,22 @@
+using Impostor.Api.Unity;
+
+namespace Impostor.Api.Innersloth
+{
+ public class FloatRange
+ {
+ private readonly float _min;
+ private readonly float _max;
+
+ public FloatRange(float min, float max)
+ {
+ _min = min;
+ _max = max;
+ }
+
+ public float Width => _max - _min;
+
+ public float Lerp(float v) => Mathf.Lerp(_min, _max, v);
+
+ public float ReverseLerp(float t) => Mathf.Clamp((t - _min) / Width, 0.0f, 1f);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameCodeParser.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameCodeParser.cs
new file mode 100644
index 0000000..9717cff
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameCodeParser.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Buffers.Binary;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Impostor.Api.Innersloth
+{
+ public static class GameCodeParser
+ {
+ private const string V2 = "QWXRTYLPESDFGHUJKZOCVBINMA";
+ private static readonly int[] V2Map = {
+ 25,
+ 21,
+ 19,
+ 10,
+ 8,
+ 11,
+ 12,
+ 13,
+ 22,
+ 15,
+ 16,
+ 6,
+ 24,
+ 23,
+ 18,
+ 7,
+ 0,
+ 3,
+ 9,
+ 4,
+ 14,
+ 20,
+ 1,
+ 2,
+ 5,
+ 17
+ };
+ private static readonly RNGCryptoServiceProvider Random = new RNGCryptoServiceProvider();
+
+ public static string IntToGameName(int input)
+ {
+ // V2.
+ if (input < -1)
+ {
+ return IntToGameNameV2(input);
+ }
+
+ // V1.
+ Span<byte> code = stackalloc byte[4];
+ BinaryPrimitives.WriteInt32LittleEndian(code, input);
+#if NETSTANDARD2_0
+ return Encoding.UTF8.GetString(code.Slice(0, 4).ToArray());
+#else
+ return Encoding.UTF8.GetString(code.Slice(0, 4));
+#endif
+ }
+
+ private static string IntToGameNameV2(int input)
+ {
+ var a = input & 0x3FF;
+ var b = (input >> 10) & 0xFFFFF;
+
+ return new string(new []
+ {
+ V2[a % 26],
+ V2[a / 26],
+ V2[b % 26],
+ V2[b / 26 % 26],
+ V2[b / (26 * 26) % 26],
+ V2[b / (26 * 26 * 26) % 26]
+ });
+ }
+
+ public static int GameNameToInt(string code)
+ {
+ var upper = code.ToUpperInvariant();
+ if (upper.Any(x => !char.IsLetter(x)))
+ {
+ return -1;
+ }
+
+ var len = code.Length;
+ if (len == 6)
+ {
+ return GameNameToIntV2(upper);
+ }
+
+ if (len == 4)
+ {
+ return code[0] | ((code[1] | ((code[2] | (code[3] << 8)) << 8)) << 8);
+ }
+
+ return -1;
+ }
+
+ private static int GameNameToIntV2(string code)
+ {
+ var a = V2Map[code[0] - 65];
+ var b = V2Map[code[1] - 65];
+ var c = V2Map[code[2] - 65];
+ var d = V2Map[code[3] - 65];
+ var e = V2Map[code[4] - 65];
+ var f = V2Map[code[5] - 65];
+
+ var one = (a + 26 * b) & 0x3FF;
+ var two = (c + 26 * (d + 26 * (e + 26 * f)));
+
+ return (int) (one | ((two << 10) & 0x3FFFFC00) | 0x80000000);
+ }
+
+ public static int GenerateCode(int len)
+ {
+ if (len != 4 && len != 6)
+ {
+ throw new ArgumentException("should be 4 or 6", nameof(len));
+ }
+
+ // Generate random bytes.
+#if NETSTANDARD2_0
+ var data = new byte[len];
+#else
+ Span<byte> data = stackalloc byte[len];
+#endif
+ Random.GetBytes(data);
+
+ // Convert to their char representation.
+ Span<char> dataChar = stackalloc char[len];
+ for (var i = 0; i < len; i++)
+ {
+ dataChar[i] = V2[V2Map[data[i] % 26]];
+ }
+
+#if NETSTANDARD2_0
+ return GameNameToInt(new string(dataChar.ToArray()));
+#else
+ return GameNameToInt(new string(dataChar));
+#endif
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameKeywords.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameKeywords.cs
new file mode 100644
index 0000000..bbb463f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameKeywords.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Impostor.Api.Innersloth
+{
+ [Flags]
+ public enum GameKeywords : uint
+ {
+ All = 0,
+ Other = 1,
+ Spanish = 2,
+ Korean = 4,
+ Russian = 8,
+ Portuguese = 16,
+ Arabic = 32,
+ Filipone = 64,
+ Polish = 128,
+ English = 256,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameOptionsData.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameOptionsData.cs
new file mode 100644
index 0000000..d18efd8
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameOptionsData.cs
@@ -0,0 +1,261 @@
+using System;
+using System.IO;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Api.Innersloth
+{
+ public class GameOptionsData
+ {
+ /// <summary>
+ /// The latest major version of the game client.
+ /// </summary>
+ public const int LatestVersion = 4;
+
+ /// <summary>
+ /// Gets or sets host's version of the game.
+ /// </summary>
+ public byte Version { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum amount of players for this lobby.
+ /// </summary>
+ public byte MaxPlayers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the language of the lobby as per <see cref="GameKeywords"/> enum.
+ /// </summary>
+ public GameKeywords Keywords { get; set; }
+
+ /// <summary>
+ /// Gets or sets the MapId selected for this lobby
+ /// </summary>
+ /// <remarks>
+ /// Skeld = 0, MiraHQ = 1, Polus = 2.
+ /// </remarks>
+ internal byte MapId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the map selected for this lobby
+ /// </summary>
+ public MapTypes Map
+ {
+ get => (MapTypes)MapId;
+ set => MapId = (byte)value;
+ }
+
+ /// <summary>
+ /// Gets or sets the Player speed modifier.
+ /// </summary>
+ public float PlayerSpeedMod { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Light modifier for the players that are members of the crew as a multiplier value.
+ /// </summary>
+ public float CrewLightMod { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Light modifier for the players that are Impostors as a multiplier value.
+ /// </summary>
+ public float ImpostorLightMod { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Impostor cooldown to kill in seconds.
+ /// </summary>
+ public float KillCooldown { get; set; }
+
+ /// <summary>
+ /// Gets or sets the number of common tasks.
+ /// </summary>
+ public int NumCommonTasks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the number of long tasks.
+ /// </summary>
+ public int NumLongTasks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the number of short tasks.
+ /// </summary>
+ public int NumShortTasks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum amount of emergency meetings each player can call during the game in seconds.
+ /// </summary>
+ public int NumEmergencyMeetings { get; set; }
+
+ /// <summary>
+ /// Gets or sets the cooldown between each time any player can call an emergency meeting in seconds.
+ /// </summary>
+ public int EmergencyCooldown { get; set; }
+
+ /// <summary>
+ /// Gets or sets the number of impostors for this lobby.
+ /// </summary>
+ public int NumImpostors { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether ghosts (dead crew members) can do tasks.
+ /// </summary>
+ public bool GhostsDoTasks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Kill as per values in <see cref="KillDistances"/>.
+ /// </summary>
+ /// <remarks>
+ /// Short = 0, Normal = 1, Long = 2.
+ /// </remarks>
+ public KillDistances KillDistance { get; set; }
+
+ /// <summary>
+ /// Gets or sets the time for discussion before voting time in seconds.
+ /// </summary>
+ public int DiscussionTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the time for voting in seconds.
+ /// </summary>
+ public int VotingTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether an ejected player is an impostor or not.
+ /// </summary>
+ public bool ConfirmImpostor { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether players are able to see tasks being performed by other players.
+ /// </summary>
+ /// <remarks>
+ /// By being set to true, tasks such as Empty Garbage, Submit Scan, Clear asteroids, Prime shields execution will be visible to other players.
+ /// </remarks>
+ public bool VisualTasks { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the vote is anonymous.
+ /// </summary>
+ public bool AnonymousVotes { get; set; }
+
+ /// <summary>
+ /// Gets or sets the task bar update mode as per values in <see cref="Innersloth.TaskBarUpdate"/>.
+ /// </summary>
+ public TaskBarUpdate TaskBarUpdate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the GameOptions are the default ones.
+ /// </summary>
+ public bool IsDefaults { get; set; }
+
+ /// <summary>
+ /// Deserialize a packet/message to a new GameOptionsData object.
+ /// </summary>
+ /// <param name="reader">Message reader object containing the raw message.</param>
+ /// <returns>GameOptionsData object.</returns>
+ public static GameOptionsData DeserializeCreate(IMessageReader reader)
+ {
+ var options = new GameOptionsData();
+ options.Deserialize(reader.ReadBytesAndSize());
+ return options;
+ }
+
+ /// <summary>
+ /// Serializes this instance of GameOptionsData object to a specified BinaryWriter.
+ /// </summary>
+ /// <param name="writer">The stream to write the message to.</param>
+ /// <param name="version">The version of the game.</param>
+ 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);
+
+ if (version > 1)
+ {
+ writer.Write((byte)EmergencyCooldown);
+ }
+
+ if (version > 2)
+ {
+ writer.Write((bool)ConfirmImpostor);
+ writer.Write((bool)VisualTasks);
+ }
+
+ if (version > 3)
+ {
+ writer.Write((bool)AnonymousVotes);
+ writer.Write((byte)TaskBarUpdate);
+ }
+
+ if (version > 4)
+ {
+ throw new ImpostorException($"Unknown GameOptionsData version {Version}.");
+ }
+ }
+
+ /// <summary>
+ /// Deserialize a ReadOnlyMemory object to this instance of the GameOptionsData object.
+ /// </summary>
+ /// <param name="memory">Memory containing the message/packet.</param>
+ public void Deserialize(ReadOnlyMemory<byte> memory)
+ {
+ var bytes = memory.Span;
+
+ Version = bytes.ReadByte();
+ MaxPlayers = bytes.ReadByte();
+ Keywords = (GameKeywords)bytes.ReadUInt32();
+ MapId = bytes.ReadByte();
+ PlayerSpeedMod = bytes.ReadSingle();
+
+ CrewLightMod = bytes.ReadSingle();
+ ImpostorLightMod = bytes.ReadSingle();
+ KillCooldown = bytes.ReadSingle();
+
+ NumCommonTasks = bytes.ReadByte();
+ NumLongTasks = bytes.ReadByte();
+ NumShortTasks = bytes.ReadByte();
+
+ NumEmergencyMeetings = bytes.ReadInt32();
+
+ NumImpostors = bytes.ReadByte();
+ KillDistance = (KillDistances)bytes.ReadByte();
+ DiscussionTime = bytes.ReadInt32();
+ VotingTime = bytes.ReadInt32();
+
+ IsDefaults = bytes.ReadBoolean();
+
+ if (Version > 1)
+ {
+ EmergencyCooldown = bytes.ReadByte();
+ }
+
+ if (Version > 2)
+ {
+ ConfirmImpostor = bytes.ReadBoolean();
+ VisualTasks = bytes.ReadBoolean();
+ }
+
+ if (Version > 3)
+ {
+ AnonymousVotes = bytes.ReadBoolean();
+ TaskBarUpdate = (TaskBarUpdate)bytes.ReadByte();
+ }
+
+ if (Version > 4)
+ {
+ throw new ImpostorException($"Unknown GameOptionsData version {Version}.");
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameOverReason.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameOverReason.cs
new file mode 100644
index 0000000..6a95d37
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameOverReason.cs
@@ -0,0 +1,15 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum GameOverReason : byte
+ {
+ HumansByVote = 0,
+ HumansByTask = 1,
+ ImpostorByVote = 2,
+ ImpostorByKill = 3,
+ ImpostorBySabotage = 4,
+
+ // Unused (?)
+ ImpostorDisconnect = 5,
+ HumansDisconnect = 6,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameStates.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameStates.cs
new file mode 100644
index 0000000..f5aaa9c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameStates.cs
@@ -0,0 +1,11 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum GameStates : byte
+ {
+ NotStarted = 0,
+ Starting = 1,
+ Started = 2,
+ Ended = 3,
+ Destroyed = 4,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameVersion.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameVersion.cs
new file mode 100644
index 0000000..c11933e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameVersion.cs
@@ -0,0 +1,10 @@
+namespace Impostor.Api.Innersloth
+{
+ public class GameVersion
+ {
+ public static int GetVersion(int year, int month, int day, int rev = 0)
+ {
+ return (year * 25000) + (month * 1800) + (day * 50) + rev;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/KillDistances.cs b/Impostor-dev/src/Impostor.Api/Innersloth/KillDistances.cs
new file mode 100644
index 0000000..b2ee5d9
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/KillDistances.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Impostor.Api.Innersloth
+{
+ [Flags]
+ public enum KillDistances : byte
+ {
+ Short = 0,
+ Normal = 1,
+ Long = 2,
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/MapFlags.cs b/Impostor-dev/src/Impostor.Api/Innersloth/MapFlags.cs
new file mode 100644
index 0000000..ad462a2
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/MapFlags.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Impostor.Api.Innersloth
+{
+ [Flags]
+ public enum MapFlags
+ {
+ Skeld = 1,
+ MiraHQ = 2,
+ Polus = 4,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/MapTypes.cs b/Impostor-dev/src/Impostor.Api/Innersloth/MapTypes.cs
new file mode 100644
index 0000000..8dc07b5
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/MapTypes.cs
@@ -0,0 +1,9 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum MapTypes
+ {
+ Skeld = 0,
+ MiraHQ = 1,
+ Polus = 2,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/RegionInfo.cs b/Impostor-dev/src/Impostor.Api/Innersloth/RegionInfo.cs
new file mode 100644
index 0000000..c78978b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/RegionInfo.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Impostor.Api.Innersloth
+{
+ public class RegionInfo
+ {
+ public RegionInfo(string name, string ping, IReadOnlyList<ServerInfo> servers)
+ {
+ Name = name;
+ Ping = ping;
+ Servers = servers;
+ }
+
+ public string Name { get; }
+ public string Ping { get; }
+ public IReadOnlyList<ServerInfo> Servers { get; }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.Write(0);
+ writer.Write(Name);
+ writer.Write(Ping);
+ writer.Write(Servers.Count);
+
+ foreach (var server in Servers)
+ {
+ server.Serialize(writer);
+ }
+ }
+
+ public static RegionInfo Deserialize(BinaryReader reader)
+ {
+ var unknown = reader.ReadInt32();
+ var name = reader.ReadString();
+ var ping = reader.ReadString();
+ var servers = new List<ServerInfo>();
+ var serverCount = reader.ReadInt32();
+
+ for (var i = 0; i < serverCount; i++)
+ {
+ servers.Add(ServerInfo.Deserialize(reader));
+ }
+
+ return new RegionInfo(name, ping, servers);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/ServerInfo.cs b/Impostor-dev/src/Impostor.Api/Innersloth/ServerInfo.cs
new file mode 100644
index 0000000..7785823
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/ServerInfo.cs
@@ -0,0 +1,37 @@
+using System.IO;
+using System.Net;
+
+namespace Impostor.Api.Innersloth
+{
+ public class ServerInfo
+ {
+ public string Name { get; }
+ public string Ip { get; }
+ public ushort Port { get; }
+
+ public ServerInfo(string name, string ip, ushort port)
+ {
+ Name = name;
+ Ip = ip;
+ Port = port;
+ }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.Write(Name);
+ writer.Write(IPAddress.Parse(Ip).GetAddressBytes());
+ writer.Write(Port);
+ writer.Write(0);
+ }
+
+ public static ServerInfo Deserialize(BinaryReader reader)
+ {
+ var name = reader.ReadString();
+ var ip = new IPAddress(reader.ReadBytes(4)).ToString();
+ var port = reader.ReadUInt16();
+ var unknown = reader.ReadInt32();
+
+ return new ServerInfo(name, ip, port);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypeHelpers.cs b/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypeHelpers.cs
new file mode 100644
index 0000000..ad88c28
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypeHelpers.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Linq;
+
+namespace Impostor.Api.Innersloth
+{
+ internal class SystemTypeHelpers
+ {
+ public static readonly SystemTypes[] AllTypes;
+ public static readonly string[] Names;
+
+ static SystemTypeHelpers()
+ {
+ AllTypes = Enum.GetValues(typeof(SystemTypes)).Cast<SystemTypes>().ToArray();
+ Names = AllTypes.Select(x =>
+ {
+ return x switch
+ {
+ SystemTypes.UpperEngine => "Upper Engine",
+ SystemTypes.Nav => "Navigations",
+ SystemTypes.LifeSupp => "O2",
+ SystemTypes.LowerEngine => "Lower Engine",
+ SystemTypes.LockerRoom => "Locker Room",
+ _ => x.ToString()
+ };
+ }).ToArray();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypes.cs b/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypes.cs
new file mode 100644
index 0000000..7f91718
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypes.cs
@@ -0,0 +1,42 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum SystemTypes : byte
+ {
+ Hallway = 0,
+ Storage = 1,
+ Cafeteria = 2,
+ Reactor = 3,
+ UpperEngine = 4,
+ Nav = 5,
+ Admin = 6,
+ Electrical = 7,
+ LifeSupp = 8,
+ Shields = 9,
+ MedBay = 10,
+ Security = 11,
+ Weapons = 12,
+ LowerEngine = 13,
+ Comms = 14,
+ ShipTasks = 15,
+ Doors = 16,
+ Sabotage = 17,
+ /// <summary>
+ /// Decontam on Mira and bottom decontam on Polus
+ /// </summary>
+ Decontamination = 18,
+ Launchpad = 19,
+ LockerRoom = 20,
+ Laboratory = 21,
+ Balcony = 22,
+ Office = 23,
+ Greenhouse = 24,
+ Dropship = 25,
+ /// <summary>
+ /// Top decontam on Polus
+ /// </summary>
+ Decontamination2 = 26,
+ Outside = 27,
+ Specimens = 28,
+ BoilerRoom = 29
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/TaskBarUpdate.cs b/Impostor-dev/src/Impostor.Api/Innersloth/TaskBarUpdate.cs
new file mode 100644
index 0000000..f4d7c1f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/TaskBarUpdate.cs
@@ -0,0 +1,9 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum TaskBarUpdate : byte
+ {
+ Always = 0,
+ Meetings = 1,
+ Never = 2
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/TaskTypes.cs b/Impostor-dev/src/Impostor.Api/Innersloth/TaskTypes.cs
new file mode 100644
index 0000000..8b15354
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/TaskTypes.cs
@@ -0,0 +1,49 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum TaskTypes : uint
+ {
+ SubmitScan = 0,
+ PrimeShields = 1,
+ FuelEngines = 2,
+ ChartCourse = 3,
+ StartReactor = 4,
+ SwipeCard = 5,
+ ClearAsteroids = 6,
+ UploadData = 7,
+ InspectSample = 8,
+ EmptyChute = 9,
+ EmptyGarbage = 10,
+ AlignEngineOutput = 11,
+ FixWiring = 12,
+ CalibrateDistributor = 13,
+ DivertPower = 14,
+ UnlockManifolds = 15,
+ ResetReactor = 16,
+ FixLights = 17,
+ Filter = 18,
+ FixComms = 19,
+ RestoreOxy = 20,
+ StabilizeSteering = 21,
+ AssembleArtifact = 22,
+ SortSamples = 23,
+ MeasureWeather = 24,
+ EnterIdCode = 25,
+ BuyBeverage = 26,
+ ProcessData = 27,
+ RunDiagnostics = 28,
+ WaterPlants = 29,
+ MonitorOxygen = 30,
+ StoreArtifact = 31,
+ FillCanisters = 32,
+ ActivateWeatherNodes = 33,
+ InsertKeys = 34,
+ ResetSeismic = 35,
+ ScanBoardingPass = 36,
+ OpenWaterways = 37,
+ ReplaceWaterJug = 38,
+ RepairDrill = 39,
+ AlignTelescope = 40,
+ RecordTemperature = 41,
+ RebootWifi = 42,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/TextBox.cs b/Impostor-dev/src/Impostor.Api/Innersloth/TextBox.cs
new file mode 100644
index 0000000..9533d83
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/TextBox.cs
@@ -0,0 +1,10 @@
+namespace Impostor.Api.Innersloth
+{
+ public static class TextBox
+ {
+ public static bool IsCharAllowed(char i)
+ {
+ return i == ' ' || (i >= 'A' && i <= 'Z') || (i >= 'a' && i <= 'z') || (i >= '0' && i <= '9') || (i >= 'À' && i <= 'ÿ') || (i >= 'Ѐ' && i <= 'џ') || (i >= 'ㄱ' && i <= 'ㆎ') || (i >= '가' && i <= '힣');
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/VentLocation.cs b/Impostor-dev/src/Impostor.Api/Innersloth/VentLocation.cs
new file mode 100644
index 0000000..f9b8567
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Innersloth/VentLocation.cs
@@ -0,0 +1,48 @@
+namespace Impostor.Api.Innersloth
+{
+ public enum VentLocation : uint
+ {
+ // Skeld
+ SkeldAdmin = 0,
+ SkeldRightHallway = 1,
+ SkeldCafeteria = 2,
+ SkeldElectrical = 3,
+ SkeldUpperEngine = 4,
+ SkeldSecurity = 5,
+ SkeldMedbay = 6,
+ SkeldWeapons = 7,
+ SkeldLowerReactor = 8,
+ SkeldLowerEngine = 9,
+ SkeldShields = 10,
+ SkeldUpperReactor = 11,
+ SkeldUpperNavigation = 12,
+ SkeldLowerNavigation = 13,
+
+ // Mira HQ
+ MiraBalcony = 1,
+ MiraCafeteria = 2,
+ MiraReactor = 3,
+ MiraLaboratory = 4,
+ MiraOffice = 5,
+ MiraAdmin = 6,
+ MiraGreenhouse = 7,
+ MiraMedbay = 8,
+ MiraDecontamination = 9,
+ MiraLockerRoom = 10,
+ MiraLaunchpad = 11,
+
+ // Polus
+ PolusSecurity = 0,
+ PolusElectrical = 1,
+ PolusO2 = 2,
+ PolusCommunications = 3,
+ PolusOffice = 4,
+ PolusAdmin = 5,
+ PolusLaboratory = 6,
+ PolusLava = 7,
+ PolusStorage = 8,
+ PolusRightStabilizer = 9,
+ PolusLeftStabilizer = 10,
+ PolusOutsideAdmin = 11,
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/IClient.cs b/Impostor-dev/src/Impostor.Api/Net/IClient.cs
new file mode 100644
index 0000000..48efeda
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/IClient.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Api.Net
+{
+ /// <summary>
+ /// Represents a connected game client.
+ /// </summary>
+ public interface IClient
+ {
+ /// <summary>
+ /// Gets or sets the unique ID of the client.
+ /// </summary>
+ /// <remarks>
+ /// This ID is generated when the client is registered in the client manager and should not be used
+ /// to store persisted data.
+ /// </remarks>
+ int Id { get; set; }
+
+ /// <summary>
+ /// Gets the name that was provided by the player in the client.
+ /// </summary>
+ /// <remarks>
+ /// The name is provided by the player and should not be used to store persisted data.
+ /// </remarks>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the connection of the client.
+ /// </summary>
+ /// <remarks>
+ /// Null when the client was not registered by the matchmaker.
+ /// </remarks>
+ IHazelConnection? Connection { get; }
+
+ /// <summary>
+ /// Gets a key/value collection that can be used to share data between messages.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The stored data will not be saved.
+ /// After the connection has been closed all data will be lost.
+ /// </para>
+ /// <para>
+ /// Note that the values will not be disposed after the connection has been closed.
+ /// This has to be implemented by the plugin.
+ /// </para>
+ /// </remarks>
+ IDictionary<object, object> Items { get; }
+
+ /// <summary>
+ /// Gets or sets the current game data of the <see cref="IClient"/>.
+ /// </summary>
+ IClientPlayer? Player { get; }
+
+ ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType);
+
+ ValueTask HandleDisconnectAsync(string reason);
+
+ /// <summary>
+ /// Disconnect the client with a <see cref="DisconnectReason"/>.
+ /// </summary>
+ /// <param name="reason">
+ /// The message to show to the player.
+ /// </param>
+ /// <param name="message">
+ /// Only used when <see cref="reason"/> is set to <see cref="DisconnectReason.Custom"/>.
+ /// </param>
+ /// <returns>
+ /// A <see cref="ValueTask"/> representing the asynchronous operation.
+ /// </returns>
+ ValueTask DisconnectAsync(DisconnectReason reason, string? message = null);
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/IClientPlayer.cs b/Impostor-dev/src/Impostor.Api/Net/IClientPlayer.cs
new file mode 100644
index 0000000..6070210
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/IClientPlayer.cs
@@ -0,0 +1,43 @@
+using System.Threading.Tasks;
+using Impostor.Api.Games;
+using Impostor.Api.Net.Inner;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Api.Net
+{
+ /// <summary>
+ /// Represents a player in <see cref="IGame"/>.
+ /// </summary>
+ public interface IClientPlayer
+ {
+ /// <summary>
+ /// Gets the client that belongs to the player.
+ /// </summary>
+ IClient Client { get; }
+
+ /// <summary>
+ /// Gets the game where the <see cref="IClientPlayer"/> belongs to.
+ /// </summary>
+ IGame Game { get; }
+
+ /// <summary>
+ /// Gets or sets the current limbo state of the player.
+ /// </summary>
+ LimboStates Limbo { get; set; }
+
+ IInnerPlayerControl? Character { get; }
+
+ public bool IsHost { get; }
+
+ /// <summary>
+ /// Checks if the specified <see cref="IInnerNetObject"/> is owned by <see cref="IClientPlayer"/>.
+ /// </summary>
+ /// <param name="netObject">The <see cref="IInnerNetObject"/>.</param>
+ /// <returns>Returns true if owned by <see cref="IClientPlayer"/>.</returns>
+ bool IsOwner(IInnerNetObject netObject);
+
+ ValueTask KickAsync();
+
+ ValueTask BanAsync();
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/IConnection.cs b/Impostor-dev/src/Impostor.Api/Net/IConnection.cs
new file mode 100644
index 0000000..94f9b8b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/IConnection.cs
@@ -0,0 +1,7 @@
+namespace Impostor.Api.Net
+{
+ public interface IConnection
+ {
+
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/IHazelConnection.cs b/Impostor-dev/src/Impostor.Api/Net/IHazelConnection.cs
new file mode 100644
index 0000000..4e6c4b3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/IHazelConnection.cs
@@ -0,0 +1,41 @@
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Api.Net
+{
+ /// <summary>
+ /// Represents the connection of the client.
+ /// </summary>
+ public interface IHazelConnection
+ {
+ /// <summary>
+ /// Gets the IP endpoint of the client.
+ /// </summary>
+ IPEndPoint EndPoint { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the client is connected to the server.
+ /// </summary>
+ bool IsConnected { get; }
+
+ /// <summary>
+ /// Gets the client of the connection.
+ /// </summary>
+ IClient? Client { get; set; }
+
+ /// <summary>
+ /// Sends a message writer to the connection.
+ /// </summary>
+ /// <param name="writer">The message.</param>
+ /// <returns></returns>
+ ValueTask SendAsync(IMessageWriter writer);
+
+ /// <summary>
+ /// Disconnects the client and invokes the disconnect handler.
+ /// </summary>
+ /// <param name="reason">A reason.</param>
+ /// <returns></returns>
+ ValueTask DisconnectAsync(string? reason);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/IGameNet.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/IGameNet.cs
new file mode 100644
index 0000000..933a4de
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/IGameNet.cs
@@ -0,0 +1,18 @@
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Api.Net.Inner
+{
+ /// <summary>
+ /// Holds all data that is serialized over the network through GameData packets.
+ /// </summary>
+ public interface IGameNet
+ {
+ IInnerLobbyBehaviour LobbyBehaviour { get; }
+
+ IInnerGameData GameData { get; }
+
+ IInnerVoteBanSystem VoteBan { get; }
+
+ IInnerShipStatus ShipStatus { get; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/IInnerNetObject.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/IInnerNetObject.cs
new file mode 100644
index 0000000..c171377
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/IInnerNetObject.cs
@@ -0,0 +1,9 @@
+namespace Impostor.Api.Net.Inner
+{
+ public interface IInnerNetObject
+ {
+ public uint NetId { get; }
+
+ public int OwnerId { get; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerCustomNetworkTransform.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerCustomNetworkTransform.cs
new file mode 100644
index 0000000..6d867e7
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerCustomNetworkTransform.cs
@@ -0,0 +1,15 @@
+using System.Numerics;
+using System.Threading.Tasks;
+
+namespace Impostor.Api.Net.Inner.Objects.Components
+{
+ public interface IInnerCustomNetworkTransform : IInnerNetObject
+ {
+ /// <summary>
+ /// Snaps the current to the given position <see cref="IInnerPlayerControl"/>.
+ /// </summary>
+ /// <param name="position">The target position.</param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask SnapToAsync(Vector2 position);
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerPlayerPhysics.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerPlayerPhysics.cs
new file mode 100644
index 0000000..9378c5b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerPlayerPhysics.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Net.Inner.Objects.Components
+{
+ public interface IInnerPlayerPhysics : IInnerNetObject
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerGameData.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerGameData.cs
new file mode 100644
index 0000000..6e41020
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerGameData.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Net.Inner.Objects
+{
+ public interface IInnerGameData : IInnerNetObject
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerLobbyBehaviour.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerLobbyBehaviour.cs
new file mode 100644
index 0000000..f05f4cf
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerLobbyBehaviour.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Net.Inner.Objects
+{
+ public interface IInnerLobbyBehaviour : IInnerNetObject
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs
new file mode 100644
index 0000000..9c89d05
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs
@@ -0,0 +1,6 @@
+namespace Impostor.Api.Net.Inner.Objects
+{
+ public interface IInnerMeetingHud
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs
new file mode 100644
index 0000000..04558b9
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs
@@ -0,0 +1,116 @@
+using System.Threading.Tasks;
+using Impostor.Api.Innersloth.Customization;
+using Impostor.Api.Net.Inner.Objects.Components;
+
+namespace Impostor.Api.Net.Inner.Objects
+{
+ public interface IInnerPlayerControl : IInnerNetObject
+ {
+ /// <summary>
+ /// Gets the <see cref="PlayerId"/> assigned by the client of the host of the game.
+ /// </summary>
+ byte PlayerId { get; }
+
+ /// <summary>
+ /// Gets the <see cref="IInnerPlayerPhysics"/> of the <see cref="IInnerPlayerControl"/>.
+ /// Contains vent logic.
+ /// </summary>
+ IInnerPlayerPhysics Physics { get; }
+
+ /// <summary>
+ /// Gets the <see cref="IInnerCustomNetworkTransform"/> of the <see cref="IInnerPlayerControl"/>.
+ /// Contains position data about the player.
+ /// </summary>
+ IInnerCustomNetworkTransform NetworkTransform { get; }
+
+ /// <summary>
+ /// Gets the <see cref="IInnerPlayerInfo"/> of the <see cref="IInnerPlayerControl"/>.
+ /// Contains metadata about the player.
+ /// </summary>
+ IInnerPlayerInfo PlayerInfo { get; }
+
+ /// <summary>
+ /// Sets the name of the current <see cref="IInnerPlayerControl"/>.
+ /// Visible to all players.
+ /// </summary>
+ /// <param name="name">A name for the player.</param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask SetNameAsync(string name);
+
+ /// <summary>
+ /// Sets the color of the current <see cref="IInnerPlayerControl"/>.
+ /// Visible to all players.
+ /// </summary>
+ /// <param name="colorId">A color for the player.</param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask SetColorAsync(byte colorId);
+
+ /// <param name="colorType">A color for the player.</param>
+ /// <inheritdoc cref="SetColorAsync(byte)" />
+ ValueTask SetColorAsync(ColorType colorType);
+
+ /// <summary>
+ /// Sets the hat of the current <see cref="IInnerPlayerControl"/>.
+ /// Visible to all players.
+ /// </summary>
+ /// <param name="hatId">An hat for the player.</param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask SetHatAsync(uint hatId);
+
+ /// <param name="hatType">An hat for the player.</param>
+ /// <inheritdoc cref="SetHatAsync(uint)" />
+ ValueTask SetHatAsync(HatType hatType);
+
+ /// <summary>
+ /// Sets the pet of the current <see cref="IInnerPlayerControl"/>.
+ /// Visible to all players.
+ /// </summary>
+ /// <param name="petId">A pet for the player.</param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask SetPetAsync(uint petId);
+
+ /// <param name="petType">A pet for the player.</param>
+ /// <inheritdoc cref="SetPetAsync(uint)" />
+ ValueTask SetPetAsync(PetType petType);
+
+ /// <summary>
+ /// Sets the skin of the current <see cref="IInnerPlayerControl"/>.
+ /// Visible to all players.
+ /// </summary>
+ /// <param name="skinId">A skin for the player.</param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask SetSkinAsync(uint skinId);
+
+ /// <param name="skinType">A skin for the player.</param>
+ /// <inheritdoc cref="SetSkinAsync(uint)" />
+ ValueTask SetSkinAsync(SkinType skinType);
+
+ /// <summary>
+ /// Send a chat message as the current <see cref="IInnerPlayerControl"/>.
+ /// Visible to all players.
+ /// </summary>
+ /// <param name="text">The message to send.</param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask SendChatAsync(string text);
+
+ /// <summary>
+ /// Send a chat message as the current <see cref="IInnerPlayerControl"/>.
+ /// Visible to only the current.
+ /// </summary>
+ /// <param name="text">The message to send.</param>
+ /// <param name="player">
+ /// The player that should receive this chat message.
+ /// When left as null, will send message to self.
+ /// </param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask SendChatToPlayerAsync(string text, IInnerPlayerControl? player = null);
+
+ /// <summary>
+ /// Sets the current to be murdered by an impostor <see cref="IInnerPlayerControl"/>.
+ /// Visible to all players.
+ /// </summary>
+ /// /// <param name="impostor">The Impostor who kill.</param>
+ /// <returns>Task that must be awaited.</returns>
+ ValueTask SetMurderedByAsync(IClientPlayer impostor);
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs
new file mode 100644
index 0000000..6cb3302
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Net.Inner.Objects
+{
+ public interface IInnerPlayerInfo
+ {
+ /// <summary>
+ /// Gets the name of the player as decided by the host.
+ /// </summary>
+ string PlayerName { get; }
+
+ /// <summary>
+ /// Gets the color of the player.
+ /// </summary>
+ byte ColorId { get; }
+
+ /// <summary>
+ /// Gets the hat of the player.
+ /// </summary>
+ uint HatId { get; }
+
+ /// <summary>
+ /// Gets the pet of the player.
+ /// </summary>
+ uint PetId { get; }
+
+ /// <summary>
+ /// Gets the skin of the player.
+ /// </summary>
+ uint SkinId { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the player is an impostor.
+ /// </summary>
+ bool IsImpostor { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the player is a dead in the current game.
+ /// </summary>
+ bool IsDead { get; }
+
+ /// <summary>
+ /// Gets the reason why the player is dead in the current game.
+ /// </summary>
+ DeathReason LastDeathReason { get; }
+
+ IEnumerable<ITaskInfo> Tasks { get; }
+
+ DateTimeOffset LastMurder { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerShipStatus.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerShipStatus.cs
new file mode 100644
index 0000000..c0a05ae
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerShipStatus.cs
@@ -0,0 +1,7 @@
+namespace Impostor.Api.Net.Inner.Objects
+{
+ public interface IInnerShipStatus : IInnerNetObject
+ {
+
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerVoteBanSystem.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerVoteBanSystem.cs
new file mode 100644
index 0000000..d0a816d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerVoteBanSystem.cs
@@ -0,0 +1,7 @@
+namespace Impostor.Api.Net.Inner.Objects
+{
+ public interface IInnerVoteBanSystem : IInnerNetObject
+ {
+
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/ITaskInfo.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/ITaskInfo.cs
new file mode 100644
index 0000000..2b6dd86
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/ITaskInfo.cs
@@ -0,0 +1,14 @@
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Api.Net.Inner.Objects
+{
+ public interface ITaskInfo
+ {
+ uint Id { get; }
+
+ TaskTypes Type { get; }
+
+ bool Complete { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/LimboStates.cs b/Impostor-dev/src/Impostor.Api/Net/LimboStates.cs
new file mode 100644
index 0000000..44c493e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/LimboStates.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Impostor.Api.Net
+{
+ [Flags]
+ public enum LimboStates
+ {
+ PreSpawn = 1,
+ NotLimbo = 2,
+ WaitingForHost = 4,
+ All = PreSpawn | NotLimbo | WaitingForHost,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Manager/IClientManager.cs b/Impostor-dev/src/Impostor.Api/Net/Manager/IClientManager.cs
new file mode 100644
index 0000000..92bf89f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Manager/IClientManager.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Impostor.Api.Net.Manager
+{
+ public interface IClientManager
+ {
+ IEnumerable<IClient> Clients { get; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs
new file mode 100644
index 0000000..4f5b39c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs
@@ -0,0 +1,27 @@
+using System.IO;
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Net.Messages.C2S
+{
+ public static class Message00HostGameC2S
+ {
+ public static void Serialize(IMessageWriter writer, GameOptionsData gameOptionsData)
+ {
+ writer.StartMessage(MessageFlags.HostGame);
+
+ using (var memory = new MemoryStream())
+ using (var writerBin = new BinaryWriter(memory))
+ {
+ gameOptionsData.Serialize(writerBin, GameOptionsData.LatestVersion);
+ writer.WriteBytesAndSize(memory.ToArray());
+ }
+
+ writer.EndMessage();
+ }
+
+ public static GameOptionsData Deserialize(IMessageReader reader)
+ {
+ return GameOptionsData.DeserializeCreate(reader);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs
new file mode 100644
index 0000000..f121b97
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Impostor.Api.Net.Messages.C2S
+{
+ public static class Message01JoinGameC2S
+ {
+ public static void Serialize(IMessageWriter writer)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public static void Deserialize(IMessageReader reader, out int gameCode, out byte unknown)
+ {
+ var slice = reader.ReadBytes(sizeof(Int32) + sizeof(byte)).Span;
+
+ gameCode = slice.ReadInt32();
+ unknown = slice.ReadByte();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message04RemovePlayerC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message04RemovePlayerC2S.cs
new file mode 100644
index 0000000..99cdcfa
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message04RemovePlayerC2S.cs
@@ -0,0 +1,16 @@
+namespace Impostor.Api.Net.Messages.C2S
+{
+ public class Message04RemovePlayerC2S
+ {
+ public static void Serialize(IMessageWriter writer)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public static void Deserialize(IMessageReader reader, out int playerId, out byte reason)
+ {
+ playerId = reader.ReadPackedInt32();
+ reason = reader.ReadByte();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message08EndGameC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message08EndGameC2S.cs
new file mode 100644
index 0000000..7ca5e3a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message08EndGameC2S.cs
@@ -0,0 +1,18 @@
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Net.Messages.C2S
+{
+ public class Message08EndGameC2S
+ {
+ public static void Serialize(IMessageWriter writer)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public static void Deserialize(IMessageReader reader, out GameOverReason gameOverReason)
+ {
+ gameOverReason = (GameOverReason)reader.ReadByte();
+ reader.ReadBoolean(); // showAd
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs
new file mode 100644
index 0000000..330f3b5
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs
@@ -0,0 +1,20 @@
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Net.Messages.C2S
+{
+ public class Message10AlterGameC2S
+ {
+ public static void Serialize(IMessageWriter writer)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public static void Deserialize(IMessageReader reader, out AlterGameTags gameTag, out bool isPublic)
+ {
+ 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/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message11KickPlayerC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message11KickPlayerC2S.cs
new file mode 100644
index 0000000..7c5b8b9
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message11KickPlayerC2S.cs
@@ -0,0 +1,16 @@
+namespace Impostor.Api.Net.Messages.C2S
+{
+ public class Message11KickPlayerC2S
+ {
+ public static void Serialize(IMessageWriter writer)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public static void Deserialize(IMessageReader reader, out int playerId, out bool isBan)
+ {
+ playerId = reader.ReadPackedInt32();
+ isBan = reader.ReadBoolean();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message16GetGameListC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message16GetGameListC2S.cs
new file mode 100644
index 0000000..2b7e12a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message16GetGameListC2S.cs
@@ -0,0 +1,18 @@
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Net.Messages.C2S
+{
+ public class Message16GetGameListC2S
+ {
+ public static void Serialize(IMessageWriter writer)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public static void Deserialize(IMessageReader reader, out GameOptionsData options)
+ {
+ reader.ReadPackedInt32(); // Hardcoded 0.
+ options = GameOptionsData.DeserializeCreate(reader);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageReader.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageReader.cs
new file mode 100644
index 0000000..87c06c4
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageReader.cs
@@ -0,0 +1,68 @@
+using System;
+
+namespace Impostor.Api.Net.Messages
+{
+ public interface IMessageReader : IDisposable
+ {
+ /// <summary>
+ /// Gets the tag of the message.
+ /// </summary>
+ byte Tag { get; }
+
+ /// <summary>
+ /// Gets the buffer of the message.
+ /// </summary>
+ byte[] Buffer { get; }
+
+ /// <summary>
+ /// Gets the offset of our current <see cref="IMessageReader"/> in the entire <see cref="Buffer"/>.
+ /// </summary>
+ int Offset { get; }
+
+ /// <summary>
+ /// Gets the current position of the reader.
+ /// </summary>
+ int Position { get; }
+
+ /// <summary>
+ /// Gets the length of the buffer.
+ /// </summary>
+ int Length { get; }
+
+ IMessageReader ReadMessage();
+
+ bool ReadBoolean();
+
+ sbyte ReadSByte();
+
+ byte ReadByte();
+
+ ushort ReadUInt16();
+
+ short ReadInt16();
+
+ uint ReadUInt32();
+
+ int ReadInt32();
+
+ float ReadSingle();
+
+ string ReadString();
+
+ ReadOnlyMemory<byte> ReadBytesAndSize();
+
+ ReadOnlyMemory<byte> ReadBytes(int length);
+
+ int ReadPackedInt32();
+
+ uint ReadPackedUInt32();
+
+ void CopyTo(IMessageWriter writer);
+
+ void Seek(int position);
+
+ void RemoveMessage(IMessageReader message);
+
+ IMessageReader Copy(int offset = 0);
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriter.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriter.cs
new file mode 100644
index 0000000..4f6765b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriter.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Net;
+using Impostor.Api.Games;
+
+namespace Impostor.Api.Net.Messages
+{
+ /// <summary>
+ /// Base message writer.
+ /// </summary>
+ public interface IMessageWriter : IDisposable
+ {
+ public byte[] Buffer { get; }
+
+ public int Length { get; set; }
+
+ public int Position { get; set; }
+
+ public MessageType SendOption { get; }
+
+ /// <summary>
+ /// Writes a boolean to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(bool value);
+
+ /// <summary>
+ /// Writes a sbyte to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(sbyte value);
+
+ /// <summary>
+ /// Writes a byte to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(byte value);
+
+ /// <summary>
+ /// Writes a short to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(short value);
+
+ /// <summary>
+ /// Writes an ushort to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(ushort value);
+
+ /// <summary>
+ /// Writes an uint to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(uint value);
+
+ /// <summary>
+ /// Writes an int to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(int value);
+
+ /// <summary>
+ /// Writes a float to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(float value);
+
+ /// <summary>
+ /// Writes a string to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(string value);
+
+ /// <summary>
+ /// Writes a <see cref="IPAddress"/> to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(IPAddress value);
+
+ /// <summary>
+ /// Writes an packed int to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void WritePacked(int value);
+
+ /// <summary>
+ /// Writes an packed uint to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void WritePacked(uint value);
+
+ /// <summary>
+ /// Writes raw bytes to the message.
+ /// </summary>
+ /// <param name="data">Bytes to write.</param>
+ void Write(ReadOnlyMemory<byte> data);
+
+ /// <summary>
+ /// Writes a game code to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(GameCode value);
+
+ void WriteBytesAndSize(byte[] bytes);
+
+ void WriteBytesAndSize(byte[] bytes, int length);
+
+ void WriteBytesAndSize(byte[] bytes, int offset, int length);
+
+ /// <summary>
+ /// Starts a new message.
+ /// </summary>
+ /// <param name="typeFlag">Message flag header.</param>
+ void StartMessage(byte typeFlag);
+
+ /// <summary>
+ /// Mark the end of the message.
+ /// </summary>
+ void EndMessage();
+
+ /// <summary>
+ /// Clear the message writer.
+ /// </summary>
+ /// <param name="type">New type of the message.</param>
+ void Clear(MessageType type);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriterProvider.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriterProvider.cs
new file mode 100644
index 0000000..f398939
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriterProvider.cs
@@ -0,0 +1,16 @@
+namespace Impostor.Api.Net.Messages
+{
+ public interface IMessageWriterProvider
+ {
+ /// <summary>
+ /// Retrieves a <see cref="IMessageWriter"/> from the internal pool.
+ /// Make sure to call <see cref="IMessageWriter.Dispose"/> when you are done!
+ /// </summary>
+ /// <param name="sendOption">
+ /// Whether to send the message as <see cref="MessageType.Reliable"/> or <see cref="MessageType.Unreliable"/>.
+ /// Reliable packets will ensure delivery while unreliable packets may be lost.
+ /// </param>
+ /// <returns>A <see cref="IMessageWriter"/> from the pool.</returns>
+ IMessageWriter Get(MessageType sendOption = MessageType.Unreliable);
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/MessageFlags.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/MessageFlags.cs
new file mode 100644
index 0000000..aea0c60
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/MessageFlags.cs
@@ -0,0 +1,22 @@
+namespace Impostor.Api.Net.Messages
+{
+ public static class MessageFlags
+ {
+ public const byte HostGame = 0;
+ public const byte JoinGame = 1;
+ public const byte StartGame = 2;
+ public const byte RemoveGame = 3;
+ public const byte RemovePlayer = 4;
+ public const byte GameData = 5;
+ public const byte GameDataTo = 6;
+ public const byte JoinedGame = 7;
+ public const byte EndGame = 8;
+ public const byte AlterGame = 10;
+ public const byte KickPlayer = 11;
+ public const byte WaitForHost = 12;
+ public const byte Redirect = 13;
+ public const byte ReselectServer = 14;
+ public const byte GetGameList = 9;
+ public const byte GetGameListV2 = 16;
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/MessageType.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/MessageType.cs
new file mode 100644
index 0000000..1604358
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/MessageType.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace Impostor.Api.Net.Messages
+{
+ /// <summary>
+ /// Specifies how a message should be sent between connections.
+ /// </summary>
+ [Flags]
+ public enum MessageType : byte
+ {
+ /// <summary>
+ /// Requests unreliable delivery with no fragmentation.
+ /// </summary>
+ /// <remarks>
+ /// Sending data using unreliable delivery means that data is not guaranteed to arrive at it's destination nor is
+ /// it guaranteed to arrive only once. However, unreliable delivery can be faster than other methods and it
+ /// typically requires a smaller number of protocol bytes than other methods. There is also typically less
+ /// processing involved and less memory needed as packets are not stored once sent.
+ /// </remarks>
+ Unreliable,
+
+ /// <summary>
+ /// Requests data be sent reliably but with no fragmentation.
+ /// </summary>
+ /// <remarks>
+ /// Sending data reliably means that data is guaranteed to arrive and to arrive only once. Reliable delivery
+ /// typically requires more processing, more memory (as packets need to be stored in case they need resending),
+ /// a larger number of protocol bytes and can be slower than unreliable delivery.
+ /// </remarks>
+ Reliable,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message00HostGameS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message00HostGameS2C.cs
new file mode 100644
index 0000000..8402d10
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message00HostGameS2C.cs
@@ -0,0 +1,20 @@
+using System;
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Net.Messages.S2C
+{
+ public static class Message00HostGameS2C
+ {
+ public static void Serialize(IMessageWriter writer, int gameCode)
+ {
+ writer.StartMessage(MessageFlags.HostGame);
+ writer.Write(gameCode);
+ writer.EndMessage();
+ }
+
+ public static GameOptionsData Deserialize(IMessageReader reader)
+ {
+ throw new NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message01JoinGameS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message01JoinGameS2C.cs
new file mode 100644
index 0000000..c455201
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message01JoinGameS2C.cs
@@ -0,0 +1,50 @@
+using System;
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Net.Messages.S2C
+{
+ public class Message01JoinGameS2C
+ {
+ public static void SerializeJoin(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId)
+ {
+ if (clear)
+ {
+ writer.Clear(MessageType.Reliable);
+ }
+
+ writer.StartMessage(MessageFlags.JoinGame);
+ writer.Write(gameCode);
+ writer.Write(playerId);
+ writer.Write(hostId);
+ writer.EndMessage();
+ }
+
+ public static void SerializeError(IMessageWriter writer, bool clear, DisconnectReason reason, string? message = null)
+ {
+ if (clear)
+ {
+ writer.Clear(MessageType.Reliable);
+ }
+
+ writer.StartMessage(MessageFlags.JoinGame);
+ writer.Write((int)reason);
+
+ if (reason == DisconnectReason.Custom)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+
+ writer.Write(message);
+ }
+
+ writer.EndMessage();
+ }
+
+ public static void Deserialize(IMessageReader reader)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message04RemovePlayerS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message04RemovePlayerS2C.cs
new file mode 100644
index 0000000..77b447d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message04RemovePlayerS2C.cs
@@ -0,0 +1,29 @@
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Net.Messages.S2C
+{
+ public class Message04RemovePlayerS2C
+ {
+ public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId, DisconnectReason reason)
+ {
+ // Only a subset of DisconnectReason shows an unique message.
+ // ExitGame, Banned and Kicked.
+ if (clear)
+ {
+ writer.Clear(MessageType.Reliable);
+ }
+
+ writer.StartMessage(MessageFlags.RemovePlayer);
+ writer.Write(gameCode);
+ writer.Write(playerId);
+ writer.Write(hostId);
+ writer.Write((byte)reason);
+ writer.EndMessage();
+ }
+
+ public static void Deserialize(IMessageReader reader)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message07JoinedGameS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message07JoinedGameS2C.cs
new file mode 100644
index 0000000..da6eb40
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message07JoinedGameS2C.cs
@@ -0,0 +1,31 @@
+namespace Impostor.Api.Net.Messages.S2C
+{
+ public static class Message07JoinedGameS2C
+ {
+ public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId, int[] otherPlayerIds)
+ {
+ if (clear)
+ {
+ writer.Clear(MessageType.Reliable);
+ }
+
+ writer.StartMessage(MessageFlags.JoinedGame);
+ writer.Write(gameCode);
+ writer.Write(playerId);
+ writer.Write(hostId);
+ writer.WritePacked(otherPlayerIds.Length);
+
+ foreach (var id in otherPlayerIds)
+ {
+ writer.WritePacked(id);
+ }
+
+ writer.EndMessage();
+ }
+
+ public static void Deserialize(IMessageReader reader)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message10AlterGameS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message10AlterGameS2C.cs
new file mode 100644
index 0000000..fa155df
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message10AlterGameS2C.cs
@@ -0,0 +1,26 @@
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Api.Net.Messages.S2C
+{
+ public static class Message10AlterGameS2C
+ {
+ public static void Serialize(IMessageWriter writer, bool clear, int gameCode, bool isPublic)
+ {
+ if (clear)
+ {
+ writer.Clear(MessageType.Reliable);
+ }
+
+ writer.StartMessage(MessageFlags.AlterGame);
+ writer.Write(gameCode);
+ writer.Write((byte)AlterGameTags.ChangePrivacy);
+ writer.Write(isPublic);
+ writer.EndMessage();
+ }
+
+ public static void Deserialize(IMessageReader reader)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message11KickPlayerS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message11KickPlayerS2C.cs
new file mode 100644
index 0000000..1e2b6ef
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message11KickPlayerS2C.cs
@@ -0,0 +1,24 @@
+namespace Impostor.Api.Net.Messages.S2C
+{
+ public class Message11KickPlayerS2C
+ {
+ public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, bool isBan)
+ {
+ if (clear)
+ {
+ writer.Clear(MessageType.Reliable);
+ }
+
+ writer.StartMessage(MessageFlags.KickPlayer);
+ writer.Write(gameCode);
+ writer.WritePacked(playerId);
+ writer.Write(isBan);
+ writer.EndMessage();
+ }
+
+ public static void Deserialize(IMessageReader reader)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message12WaitForHostS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message12WaitForHostS2C.cs
new file mode 100644
index 0000000..5964b1c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message12WaitForHostS2C.cs
@@ -0,0 +1,23 @@
+namespace Impostor.Api.Net.Messages.S2C
+{
+ public class Message12WaitForHostS2C
+ {
+ public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId)
+ {
+ if (clear)
+ {
+ writer.Clear(MessageType.Reliable);
+ }
+
+ writer.StartMessage(MessageFlags.WaitForHost);
+ writer.Write(gameCode);
+ writer.Write(playerId);
+ writer.EndMessage();
+ }
+
+ public static void Deserialize(IMessageReader reader)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message13RedirectS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message13RedirectS2C.cs
new file mode 100644
index 0000000..4b93b0e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message13RedirectS2C.cs
@@ -0,0 +1,25 @@
+using System.Net;
+
+namespace Impostor.Api.Net.Messages.S2C
+{
+ public class Message13RedirectS2C
+ {
+ public static void Serialize(IMessageWriter writer, bool clear, IPEndPoint ipEndPoint)
+ {
+ if (clear)
+ {
+ writer.Clear(MessageType.Reliable);
+ }
+
+ writer.StartMessage(MessageFlags.Redirect);
+ writer.Write(ipEndPoint.Address);
+ writer.Write((ushort)ipEndPoint.Port);
+ writer.EndMessage();
+ }
+
+ public static void Deserialize(IMessageReader reader)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message16GetGameListS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message16GetGameListS2C.cs
new file mode 100644
index 0000000..93386d7
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message16GetGameListS2C.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using Impostor.Api.Games;
+
+namespace Impostor.Api.Net.Messages.S2C
+{
+ public class Message16GetGameListS2C
+ {
+ public static void Serialize(IMessageWriter writer, int skeldGameCount, int miraHqGameCount, int polusGameCount, IEnumerable<IGame> games)
+ {
+ writer.StartMessage(MessageFlags.GetGameListV2);
+
+ // Count
+ writer.StartMessage(1);
+ writer.Write(skeldGameCount); // The Skeld
+ writer.Write(miraHqGameCount); // Mira HQ
+ writer.Write(polusGameCount); // Polus
+ writer.EndMessage();
+
+ // Listing
+ writer.StartMessage(0);
+
+ foreach (var game in games)
+ {
+ writer.StartMessage(0);
+ writer.Write(game.PublicIp.Address);
+ writer.Write((ushort)game.PublicIp.Port);
+ writer.Write(game.Code);
+ writer.Write(game.Host.Client.Name);
+ writer.Write((byte)game.PlayerCount);
+ writer.WritePacked(1); // TODO: What does Age do?
+ writer.Write((byte)game.Options.MapId);
+ writer.Write((byte)game.Options.NumImpostors);
+ writer.Write((byte)game.Options.MaxPlayers);
+ writer.EndMessage();
+ }
+
+ writer.EndMessage();
+ writer.EndMessage();
+ }
+
+ public static void Deserialize(IMessageReader reader)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Plugins/IPlugin.cs b/Impostor-dev/src/Impostor.Api/Plugins/IPlugin.cs
new file mode 100644
index 0000000..fd0f38f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Plugins/IPlugin.cs
@@ -0,0 +1,14 @@
+using System.Threading.Tasks;
+using Impostor.Api.Events;
+
+namespace Impostor.Api.Plugins
+{
+ public interface IPlugin : IEventListener
+ {
+ ValueTask EnableAsync();
+
+ ValueTask DisableAsync();
+
+ ValueTask ReloadAsync();
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Plugins/IPluginStartup.cs b/Impostor-dev/src/Impostor.Api/Plugins/IPluginStartup.cs
new file mode 100644
index 0000000..aa6a35f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Plugins/IPluginStartup.cs
@@ -0,0 +1,12 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Impostor.Api.Plugins
+{
+ public interface IPluginStartup
+ {
+ void ConfigureHost(IHostBuilder host);
+
+ void ConfigureServices(IServiceCollection services);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs b/Impostor-dev/src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs
new file mode 100644
index 0000000..b31bd47
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Impostor.Api.Plugins
+{
+ [AttributeUsage(AttributeTargets.Class)]
+ public class ImpostorPluginAttribute : Attribute
+ {
+ public ImpostorPluginAttribute(string package, string name, string author, string version)
+ {
+ Package = package;
+ Name = name;
+ Author = author;
+ Version = version;
+ }
+
+ public string Package { get; }
+
+ public string Name { get; }
+
+ public string Author { get; }
+
+ public string Version { get; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Plugins/PluginBase.cs b/Impostor-dev/src/Impostor.Api/Plugins/PluginBase.cs
new file mode 100644
index 0000000..0384363
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Plugins/PluginBase.cs
@@ -0,0 +1,22 @@
+using System.Threading.Tasks;
+
+namespace Impostor.Api.Plugins
+{
+ public class PluginBase : IPlugin
+ {
+ public virtual ValueTask EnableAsync()
+ {
+ return default;
+ }
+
+ public virtual ValueTask DisableAsync()
+ {
+ return default;
+ }
+
+ public virtual ValueTask ReloadAsync()
+ {
+ return default;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/ProjectRules.ruleset b/Impostor-dev/src/Impostor.Api/ProjectRules.ruleset
new file mode 100644
index 0000000..4ba23c2
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/ProjectRules.ruleset
@@ -0,0 +1,17 @@
+<RuleSet Name="Rules for Hello World project" Description="These rules focus on critical issues for the Hello World app." ToolsVersion="10.0">
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules">
+ <Rule Id="SA1200" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules">
+ <Rule Id="SA1600" Action="None" />
+ <Rule Id="SA1601" Action="None" />
+ <Rule Id="SA1602" Action="None" />
+ <Rule Id="SA1633" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.ReadabilityRules">
+ <Rule Id="SA1101" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules">
+ <Rule Id="SA1309" Action="None" />
+ </Rules>
+</RuleSet> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Properties/AssemblyInfo.cs b/Impostor-dev/src/Impostor.Api/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..c02e44a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly:InternalsVisibleTo("Impostor.Server")] \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Api/Unity/Mathf.cs b/Impostor-dev/src/Impostor.Api/Unity/Mathf.cs
new file mode 100644
index 0000000..4b03417
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Api/Unity/Mathf.cs
@@ -0,0 +1,54 @@
+namespace Impostor.Api.Unity
+{
+ public static class Mathf
+ {
+ /// <summary>
+ /// <para>Clamps the given value between the given minimum float and maximum float values. Returns the given value if it is within the min and max range.</para>
+ /// </summary>
+ /// <param name="value">The floating point value to restrict inside the range defined by the min and max values.</param>
+ /// <param name="min">The minimum floating point value to compare against.</param>
+ /// <param name="max">The maximum floating point value to compare against.</param>
+ /// <returns>
+ /// <para>The float result between the min and max values.</para>
+ /// </returns>
+ public static float Clamp(float value, float min, float max)
+ {
+ if (value < (double)min)
+ {
+ value = min;
+ }
+ else if (value > (double)max)
+ {
+ value = max;
+ }
+
+ return value;
+ }
+
+ /// <summary>
+ /// <para>Clamps value between 0 and 1 and returns value.</para>
+ /// </summary>
+ /// <param name="value">Value.</param>
+ /// <returns>Clamped value.</returns>
+ public static float Clamp01(float value)
+ {
+ if (value < 0.0)
+ {
+ return 0.0f;
+ }
+
+ return (double)value > 1.0 ? 1f : value;
+ }
+
+ /// <summary>
+ /// <para>Linearly interpolates between a and b by t.</para>
+ /// </summary>
+ /// <param name="a">The start value.</param>
+ /// <param name="b">The end value.</param>
+ /// <param name="t">The interpolation value between the two floats.</param>
+ /// <returns>
+ /// <para>The interpolated float result between the two float values.</para>
+ /// </returns>
+ public static float Lerp(float a, float b, float t) => a + ((b - a) * Clamp01(t));
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Benchmarks/.gitignore b/Impostor-dev/src/Impostor.Benchmarks/.gitignore
new file mode 100644
index 0000000..1c2dac6
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/.gitignore
@@ -0,0 +1 @@
+BenchmarkDotNet.Artifacts \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes.cs
new file mode 100644
index 0000000..2aba724
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes.cs
@@ -0,0 +1,172 @@
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Impostor.Benchmarks.Data
+{
+ public class MessageReader_Bytes
+ {
+ public byte Tag { get; }
+ public byte[] Buffer { get; }
+ public int Position { get; set; }
+ public int Length { get; set; }
+ public int BytesRemaining => this.Length - this.Position;
+
+ public MessageReader_Bytes(byte[] buffer, int position = 0, int length = 0)
+ {
+ Tag = byte.MaxValue;
+ Buffer = buffer;
+ Position = position;
+ Length = length;
+ }
+
+ public MessageReader_Bytes(byte tag, byte[] buffer, int position = 0, int length = 0)
+ {
+ Tag = tag;
+ Buffer = buffer;
+ Position = position;
+ Length = length;
+ }
+
+ public MessageReader_Bytes ReadMessage()
+ {
+ var length = ReadUInt16();
+ var tag = FastByte();
+ var pos = Position;
+
+ Position += length;
+ return new MessageReader_Bytes(tag, Buffer, pos, length);
+ }
+
+ public bool ReadBoolean()
+ {
+ byte val = FastByte();
+ return val != 0;
+ }
+
+ public sbyte ReadSByte()
+ {
+ return (sbyte)FastByte();
+ }
+
+ public byte ReadByte()
+ {
+ return FastByte();
+ }
+
+ public ushort ReadUInt16()
+ {
+ return (ushort)(this.FastByte() |
+ this.FastByte() << 8);
+ }
+
+ public short ReadInt16()
+ {
+ return (short)(this.FastByte() |
+ this.FastByte() << 8);
+ }
+
+ public uint ReadUInt32()
+ {
+ return this.FastByte()
+ | (uint)this.FastByte() << 8
+ | (uint)this.FastByte() << 16
+ | (uint)this.FastByte() << 24;
+ }
+
+ public int ReadInt32()
+ {
+ return this.FastByte()
+ | this.FastByte() << 8
+ | this.FastByte() << 16
+ | this.FastByte() << 24;
+ }
+
+ public unsafe float ReadSingle()
+ {
+ float output = 0;
+ fixed (byte* bufPtr = &this.Buffer[Position])
+ {
+ byte* outPtr = (byte*)&output;
+
+ *outPtr = *bufPtr;
+ *(outPtr + 1) = *(bufPtr + 1);
+ *(outPtr + 2) = *(bufPtr + 2);
+ *(outPtr + 3) = *(bufPtr + 3);
+ }
+
+ this.Position += 4;
+ return output;
+ }
+
+ public string ReadString()
+ {
+ var len = this.ReadPackedInt32();
+
+ if (this.BytesRemaining < len)
+ {
+ throw new InvalidDataException($"Read length is longer than message length: {len} of {this.BytesRemaining}");
+ }
+
+ var output = Encoding.UTF8.GetString(this.Buffer, Position, len);
+ this.Position += len;
+ return output;
+ }
+
+ public Span<byte> ReadBytesAndSize()
+ {
+ var len = ReadPackedInt32();
+ return ReadBytes(len);
+ }
+
+ public Span<byte> ReadBytes(int length)
+ {
+ var output = Buffer.AsSpan(Position, length);
+ Position += length;
+ return output;
+ }
+
+ public int ReadPackedInt32()
+ {
+ return (int)ReadPackedUInt32();
+ }
+
+ public uint ReadPackedUInt32()
+ {
+ bool readMore = true;
+ int shift = 0;
+ uint output = 0;
+
+ while (readMore)
+ {
+ byte b = FastByte();
+ if (b >= 0x80)
+ {
+ readMore = true;
+ b ^= 0x80;
+ }
+ else
+ {
+ readMore = false;
+ }
+
+ output |= (uint)(b << shift);
+ shift += 7;
+ }
+
+ return output;
+ }
+
+ public MessageReader_Bytes Slice(int start, int length)
+ {
+ return new MessageReader_Bytes(Tag, Buffer, start, length);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private byte FastByte()
+ {
+ return Buffer[Position++];
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled.cs
new file mode 100644
index 0000000..1103336
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Impostor.Benchmarks.Data
+{
+ public class MessageReader_Bytes_Pooled
+ {
+ private static ConcurrentQueue<MessageReader_Bytes_Pooled> _readers;
+
+ static MessageReader_Bytes_Pooled()
+ {
+ var instances = new List<MessageReader_Bytes_Pooled>();
+
+ for (var i = 0; i < 10000; i++)
+ {
+ instances.Add(new MessageReader_Bytes_Pooled());
+ }
+
+ _readers = new ConcurrentQueue<MessageReader_Bytes_Pooled>(instances);
+ }
+
+ public byte Tag { get; set; }
+ public byte[] Buffer { get; set; }
+ public int Position { get; set; }
+ public int Length { get; set; }
+ public int BytesRemaining => this.Length - this.Position;
+
+ public void Update(byte[] buffer, int position = 0, int length = 0)
+ {
+ Tag = byte.MaxValue;
+ Buffer = buffer;
+ Position = position;
+ Length = length;
+ }
+
+ public void Update(byte tag, byte[] buffer, int position = 0, int length = 0)
+ {
+ Tag = tag;
+ Buffer = buffer;
+ Position = position;
+ Length = length;
+ }
+
+ public MessageReader_Bytes_Pooled ReadMessage()
+ {
+ var length = ReadUInt16();
+ var tag = FastByte();
+ var pos = Position;
+
+ Position += length;
+
+ if (!_readers.TryDequeue(out var result))
+ {
+ throw new Exception("Failed to get pooled instance");
+ }
+
+ result.Update(tag, Buffer, pos, length);
+
+ return result;
+ }
+
+ public bool ReadBoolean()
+ {
+ byte val = FastByte();
+ return val != 0;
+ }
+
+ public sbyte ReadSByte()
+ {
+ return (sbyte)FastByte();
+ }
+
+ public byte ReadByte()
+ {
+ return FastByte();
+ }
+
+ public ushort ReadUInt16()
+ {
+ return (ushort)(this.FastByte() |
+ this.FastByte() << 8);
+ }
+
+ public short ReadInt16()
+ {
+ return (short)(this.FastByte() |
+ this.FastByte() << 8);
+ }
+
+ public uint ReadUInt32()
+ {
+ return this.FastByte()
+ | (uint)this.FastByte() << 8
+ | (uint)this.FastByte() << 16
+ | (uint)this.FastByte() << 24;
+ }
+
+ public int ReadInt32()
+ {
+ return this.FastByte()
+ | this.FastByte() << 8
+ | this.FastByte() << 16
+ | this.FastByte() << 24;
+ }
+
+ public unsafe float ReadSingle()
+ {
+ float output = 0;
+ fixed (byte* bufPtr = &this.Buffer[Position])
+ {
+ byte* outPtr = (byte*)&output;
+
+ *outPtr = *bufPtr;
+ *(outPtr + 1) = *(bufPtr + 1);
+ *(outPtr + 2) = *(bufPtr + 2);
+ *(outPtr + 3) = *(bufPtr + 3);
+ }
+
+ this.Position += 4;
+ return output;
+ }
+
+ public string ReadString()
+ {
+ var len = this.ReadPackedInt32();
+
+ if (this.BytesRemaining < len)
+ {
+ throw new InvalidDataException($"Read length is longer than message length: {len} of {this.BytesRemaining}");
+ }
+
+ var output = Encoding.UTF8.GetString(this.Buffer, Position, len);
+ this.Position += len;
+ return output;
+ }
+
+ public Span<byte> ReadBytesAndSize()
+ {
+ var len = ReadPackedInt32();
+ return ReadBytes(len);
+ }
+
+ public Span<byte> ReadBytes(int length)
+ {
+ var output = Buffer.AsSpan(Position, length);
+ Position += length;
+ return output;
+ }
+
+ public int ReadPackedInt32()
+ {
+ return (int)ReadPackedUInt32();
+ }
+
+ public uint ReadPackedUInt32()
+ {
+ bool readMore = true;
+ int shift = 0;
+ uint output = 0;
+
+ while (readMore)
+ {
+ byte b = FastByte();
+ if (b >= 0x80)
+ {
+ readMore = true;
+ b ^= 0x80;
+ }
+ else
+ {
+ readMore = false;
+ }
+
+ output |= (uint)(b << shift);
+ shift += 7;
+ }
+
+ return output;
+ }
+
+ public MessageReader_Bytes_Pooled Slice(int start, int length)
+ {
+ if (!_readers.TryDequeue(out var result))
+ {
+ throw new Exception("Failed to get pooled instance");
+ }
+
+ result.Update(Tag, Buffer, start, length);
+
+ return result;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private byte FastByte()
+ {
+ return Buffer[Position++];
+ }
+
+ public static MessageReader_Bytes_Pooled Get(byte[] data)
+ {
+ if (!_readers.TryDequeue(out var result))
+ {
+ throw new Exception("Failed to get pooled instance");
+ }
+
+ result.Update(data);
+
+ return result;
+ }
+
+ public static void Return(MessageReader_Bytes_Pooled instance)
+ {
+ _readers.Enqueue(instance);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled_Improved.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled_Improved.cs
new file mode 100644
index 0000000..0066847
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled_Improved.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Buffers.Binary;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Benchmarks.Data
+{
+ public class MessageReader_Bytes_Pooled_Improved : IDisposable
+ {
+ private readonly ObjectPool<MessageReader_Bytes_Pooled_Improved> _pool;
+
+ public MessageReader_Bytes_Pooled_Improved(ObjectPool<MessageReader_Bytes_Pooled_Improved> pool)
+ {
+ _pool = pool;
+ }
+
+ public byte Tag { get; set; }
+ public byte[] Buffer { get; set; }
+ public int Position { get; set; }
+ public int Length { get; set; }
+ public int BytesRemaining => this.Length - this.Position;
+
+ public void Update(byte[] buffer, int position = 0, int length = 0)
+ {
+ Tag = byte.MaxValue;
+ Buffer = buffer;
+ Position = position;
+ Length = length;
+ }
+
+ public void Update(byte tag, byte[] buffer, int position = 0, int length = 0)
+ {
+ Tag = tag;
+ Buffer = buffer;
+ Position = position;
+ Length = length;
+ }
+
+ public MessageReader_Bytes_Pooled_Improved ReadMessage()
+ {
+ var length = ReadUInt16();
+ var tag = ReadByte();
+ var pos = Position;
+
+ Position += length;
+
+ var result = _pool.Get();
+ result.Update(tag, Buffer, pos, length);
+ return result;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public byte ReadByte()
+ {
+ return Buffer[Position++];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ushort ReadUInt16()
+ {
+ var res = BinaryPrimitives.ReadUInt16LittleEndian(Buffer.AsSpan(Position));
+ Position += sizeof(ushort);
+ return res;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int ReadInt32()
+ {
+ var res = BinaryPrimitives.ReadInt32LittleEndian(Buffer.AsSpan(Position));
+ Position += sizeof(int);
+ return res;
+ }
+
+ public MessageReader_Bytes_Pooled_Improved Slice(int start, int length)
+ {
+ var result = _pool.Get();
+ result.Update(Tag, Buffer, start, length);
+ return result;
+ }
+
+ public void Dispose()
+ {
+ _pool.Return(this);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/MessageWriter.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageWriter.cs
new file mode 100644
index 0000000..94ff441
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageWriter.cs
@@ -0,0 +1,311 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text;
+using Impostor.Api.Games;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Benchmarks.Data
+{
+ public class MessageWriter
+ {
+ private static int BufferSize = 64000;
+
+ public MessageType SendOption { get; private set; }
+
+ private Stack<int> messageStarts = new Stack<int>();
+
+ public MessageWriter(byte[] buffer)
+ {
+ this.Buffer = buffer;
+ this.Length = this.Buffer.Length;
+ }
+
+ public MessageWriter(int bufferSize)
+ {
+ this.Buffer = new byte[bufferSize];
+ }
+
+ public byte[] Buffer { get; }
+ public int Length { get; set; }
+ public int Position { get; set; }
+
+ public byte[] ToByteArray(bool includeHeader)
+ {
+ if (includeHeader)
+ {
+ byte[] output = new byte[this.Length];
+ System.Buffer.BlockCopy(this.Buffer, 0, output, 0, this.Length);
+ return output;
+ }
+ else
+ {
+ switch (this.SendOption)
+ {
+ case MessageType.Reliable:
+ {
+ byte[] output = new byte[this.Length - 3];
+ System.Buffer.BlockCopy(this.Buffer, 3, output, 0, this.Length - 3);
+ return output;
+ }
+ case MessageType.Unreliable:
+ {
+ byte[] output = new byte[this.Length - 1];
+ System.Buffer.BlockCopy(this.Buffer, 1, output, 0, this.Length - 1);
+ return output;
+ }
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ throw new NotImplementedException();
+ }
+
+ public bool HasBytes(int expected)
+ {
+ if (this.SendOption == MessageType.Unreliable)
+ {
+ return this.Length > 1 + expected;
+ }
+
+ return this.Length > 3 + expected;
+ }
+
+ public void Write(GameCode value)
+ {
+ this.Write(value.Value);
+ }
+
+ ///
+ public void StartMessage(byte typeFlag)
+ {
+ messageStarts.Push(this.Position);
+ this.Position += 2; // Skip for size
+ this.Write(typeFlag);
+ }
+
+ ///
+ public void EndMessage()
+ {
+ var lastMessageStart = messageStarts.Pop();
+ ushort length = (ushort)(this.Position - lastMessageStart - 3); // Minus length and type byte
+ this.Buffer[lastMessageStart] = (byte)length;
+ this.Buffer[lastMessageStart + 1] = (byte)(length >> 8);
+ }
+
+ ///
+ public void CancelMessage()
+ {
+ this.Position = this.messageStarts.Pop();
+ this.Length = this.Position;
+ }
+
+ public void Clear(MessageType sendOption)
+ {
+ this.messageStarts.Clear();
+ this.SendOption = sendOption;
+ this.Buffer[0] = (byte)sendOption;
+ switch (sendOption)
+ {
+ default:
+ case MessageType.Unreliable:
+ this.Length = this.Position = 1;
+ break;
+
+ case MessageType.Reliable:
+ this.Length = this.Position = 3;
+ break;
+ }
+ }
+
+ #region WriteMethods
+
+ public void Write(bool value)
+ {
+ this.Buffer[this.Position++] = (byte)(value ? 1 : 0);
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(sbyte value)
+ {
+ this.Buffer[this.Position++] = (byte)value;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(byte value)
+ {
+ this.Buffer[this.Position++] = value;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(short value)
+ {
+ this.Buffer[this.Position++] = (byte)value;
+ this.Buffer[this.Position++] = (byte)(value >> 8);
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(ushort value)
+ {
+ this.Buffer[this.Position++] = (byte)value;
+ this.Buffer[this.Position++] = (byte)(value >> 8);
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(uint value)
+ {
+ this.Buffer[this.Position++] = (byte)value;
+ this.Buffer[this.Position++] = (byte)(value >> 8);
+ this.Buffer[this.Position++] = (byte)(value >> 16);
+ this.Buffer[this.Position++] = (byte)(value >> 24);
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(int value)
+ {
+ this.Buffer[this.Position++] = (byte)value;
+ this.Buffer[this.Position++] = (byte)(value >> 8);
+ this.Buffer[this.Position++] = (byte)(value >> 16);
+ this.Buffer[this.Position++] = (byte)(value >> 24);
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public unsafe void Write(float value)
+ {
+ fixed (byte* ptr = &this.Buffer[this.Position])
+ {
+ byte* valuePtr = (byte*)&value;
+
+ *ptr = *valuePtr;
+ *(ptr + 1) = *(valuePtr + 1);
+ *(ptr + 2) = *(valuePtr + 2);
+ *(ptr + 3) = *(valuePtr + 3);
+ }
+
+ this.Position += 4;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(string value)
+ {
+ var bytes = UTF8Encoding.UTF8.GetBytes(value);
+ this.WritePacked(bytes.Length);
+ this.Write(bytes);
+ }
+
+ public void Write(IPAddress value)
+ {
+ this.Write(value.GetAddressBytes());
+ }
+
+ public void WriteBytesAndSize(byte[] bytes)
+ {
+ this.WritePacked((uint)bytes.Length);
+ this.Write(bytes);
+ }
+
+ public void WriteBytesAndSize(byte[] bytes, int length)
+ {
+ this.WritePacked((uint)length);
+ this.Write(bytes, length);
+ }
+
+ public void WriteBytesAndSize(byte[] bytes, int offset, int length)
+ {
+ this.WritePacked((uint)length);
+ this.Write(bytes, offset, length);
+ }
+
+ public void Write(ReadOnlyMemory<byte> data)
+ {
+ Write(data.Span);
+ }
+
+ public void Write(ReadOnlySpan<byte> 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)
+ {
+ Array.Copy(bytes, 0, this.Buffer, this.Position, bytes.Length);
+ this.Position += bytes.Length;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(byte[] bytes, int offset, int length)
+ {
+ Array.Copy(bytes, offset, this.Buffer, this.Position, length);
+ this.Position += length;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(byte[] bytes, int length)
+ {
+ Array.Copy(bytes, 0, this.Buffer, this.Position, length);
+ this.Position += length;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ ///
+ public void WritePacked(int value)
+ {
+ this.WritePacked((uint)value);
+ }
+
+ ///
+ public void WritePacked(uint value)
+ {
+ do
+ {
+ byte b = (byte)(value & 0xFF);
+ if (value >= 0x80)
+ {
+ b |= 0x80;
+ }
+
+ this.Write(b);
+ value >>= 7;
+ } while (value > 0);
+ }
+
+ #endregion WriteMethods
+
+ public void Write(MessageWriter msg, bool includeHeader)
+ {
+ int offset = 0;
+ if (!includeHeader)
+ {
+ switch (msg.SendOption)
+ {
+ case MessageType.Unreliable:
+ offset = 1;
+ break;
+
+ case MessageType.Reliable:
+ offset = 3;
+ break;
+ }
+ }
+
+ this.Write(msg.Buffer, offset, msg.Length - offset);
+ }
+
+ public unsafe static bool IsLittleEndian()
+ {
+ byte b;
+ unsafe
+ {
+ int i = 1;
+ byte* bp = (byte*)&i;
+ b = *bp;
+ }
+
+ return b == 1;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/Pool/MessageReader_Bytes_Pooled_ImprovedPolicy.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/Pool/MessageReader_Bytes_Pooled_ImprovedPolicy.cs
new file mode 100644
index 0000000..802fcaa
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Data/Pool/MessageReader_Bytes_Pooled_ImprovedPolicy.cs
@@ -0,0 +1,26 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Benchmarks.Data.Pool
+{
+ public class MessageReader_Bytes_Pooled_ImprovedPolicy : IPooledObjectPolicy<MessageReader_Bytes_Pooled_Improved>
+ {
+ private readonly IServiceProvider _serviceProvider;
+
+ public MessageReader_Bytes_Pooled_ImprovedPolicy(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public MessageReader_Bytes_Pooled_Improved Create()
+ {
+ return new MessageReader_Bytes_Pooled_Improved(_serviceProvider.GetRequiredService<ObjectPool<MessageReader_Bytes_Pooled_Improved>>());
+ }
+
+ public bool Return(MessageReader_Bytes_Pooled_Improved obj)
+ {
+ return true;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReaderOwner.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReaderOwner.cs
new file mode 100644
index 0000000..402fbaf
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReaderOwner.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace Impostor.Benchmarks.Data.Span
+{
+ public class MessageReaderOwner
+ {
+ private readonly Memory<byte> _data;
+
+ public MessageReaderOwner(Memory<byte> data)
+ {
+ _data = data;
+ }
+
+ public int Position { get; internal set; }
+ public int Length => _data.Length;
+
+ public MessageReader_Span CreateReader()
+ {
+ return new MessageReader_Span(this, byte.MaxValue, _data.Span.Slice(Position));
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReader_Span.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReader_Span.cs
new file mode 100644
index 0000000..5636dbe
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReader_Span.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Buffers.Binary;
+using Impostor.Hazel;
+
+namespace Impostor.Benchmarks.Data.Span
+{
+ public ref struct MessageReader_Span
+ {
+ private readonly MessageReaderOwner _owner;
+ private readonly byte _tag;
+ private readonly Span<byte> _data;
+
+ public MessageReader_Span(MessageReaderOwner owner, byte tag, Span<byte> data)
+ {
+ _owner = owner;
+ _tag = tag;
+ _data = data;
+ }
+
+ public MessageReader_Span ReadMessage()
+ {
+ var length = ReadUInt16();
+ var tag = ReadByte();
+ var pos = _owner.Position;
+
+ _owner.Position += length;
+
+ return new MessageReader_Span(_owner, tag, _data.Slice(3, length));
+ }
+
+ public byte ReadByte()
+ {
+ return _data[_owner.Position++];
+ }
+
+ public ushort ReadUInt16()
+ {
+ var output = BinaryPrimitives.ReadUInt16LittleEndian(_data.Slice(_owner.Position));
+ _owner.Position += sizeof(ushort);
+ return output;
+ }
+
+ public int ReadInt32()
+ {
+ var output = BinaryPrimitives.ReadInt32LittleEndian(_data.Slice(_owner.Position));
+ _owner.Position += sizeof(int);
+ return output;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Extensions/SpanExtensions.cs b/Impostor-dev/src/Impostor.Benchmarks/Extensions/SpanExtensions.cs
new file mode 100644
index 0000000..8fbceeb
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Extensions/SpanExtensions.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
+
+namespace Impostor.Benchmarks.Extensions
+{
+ public static class SpanExtensions
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span<byte> ReadMessage(this Span<byte> input)
+ {
+ var length = BinaryPrimitives.ReadUInt16LittleEndian(input);
+ var tag = input[2];
+
+ return input.Slice(3, length);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int ReadUInt16(this ref ReadOnlySpan<byte> input)
+ {
+ return BinaryPrimitives.ReadUInt16LittleEndian(input);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Impostor.Benchmarks.csproj b/Impostor-dev/src/Impostor.Benchmarks/Impostor.Benchmarks.csproj
new file mode 100644
index 0000000..6f19bda
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Impostor.Benchmarks.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Server\Impostor.Server.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Program.cs b/Impostor-dev/src/Impostor.Benchmarks/Program.cs
new file mode 100644
index 0000000..3ce7383
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Program.cs
@@ -0,0 +1,23 @@
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Running;
+using Impostor.Benchmarks.Tests;
+
+namespace Impostor.Benchmarks
+{
+ internal static class Program
+ {
+ private static void Main(string[] args)
+ {
+ // BenchmarkRunner.Run<EventManagerBenchmark>(
+ // DefaultConfig.Instance
+ // .AddDiagnoser(MemoryDiagnoser.Default)
+ // );
+
+ BenchmarkRunner.Run<MessageReaderBenchmark>(
+ DefaultConfig.Instance
+ .AddDiagnoser(MemoryDiagnoser.Default)
+ );
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Tests/EventManagerBenchmark.cs b/Impostor-dev/src/Impostor.Benchmarks/Tests/EventManagerBenchmark.cs
new file mode 100644
index 0000000..0675080
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Tests/EventManagerBenchmark.cs
@@ -0,0 +1,77 @@
+// using System.Threading.Tasks;
+// using BenchmarkDotNet.Attributes;
+// using Impostor.Api.Events;
+// using Impostor.Api.Events.Managers;
+// using Impostor.Server.Events;
+// using Microsoft.Extensions.DependencyInjection;
+//
+// namespace Impostor.Benchmarks.Tests
+// {
+// public class EventManagerBenchmark
+// {
+// private IEventManager _eventManager;
+// private IGameEvent _event;
+//
+// [GlobalSetup]
+// public void Setup()
+// {
+// var services = new ServiceCollection();
+//
+// services.AddLogging();
+// services.AddSingleton<IEventManager, EventManager>();
+//
+// _event = new GameStartedEvent(null);
+// _eventManager = services.BuildServiceProvider().GetRequiredService<IEventManager>();
+// _eventManager.RegisterListener(new EventListener());
+// _eventManager.RegisterListener(new EventListener());
+// _eventManager.RegisterListener(new EventListener());
+// _eventManager.RegisterListener(new EventListener());
+// _eventManager.RegisterListener(new EventListener());
+// }
+//
+// [Benchmark]
+// public async Task Run_1()
+// {
+// for (var i = 0; i < 1; i++)
+// {
+// await _eventManager.CallAsync(_event);
+// }
+// }
+//
+// [Benchmark]
+// public async Task Run_1000()
+// {
+// for (var i = 0; i < 1000; i++)
+// {
+// await _eventManager.CallAsync(_event);
+// }
+// }
+//
+// [Benchmark]
+// public async Task Run_10000()
+// {
+// for (var i = 0; i < 10000; i++)
+// {
+// await _eventManager.CallAsync(_event);
+// }
+// }
+//
+// [Benchmark]
+// public async Task Run_100000()
+// {
+// for (var i = 0; i < 100000; i++)
+// {
+// await _eventManager.CallAsync(_event);
+// }
+// }
+//
+// private class EventListener : IEventListener
+// {
+// [EventListener]
+// public void OnGameStarted(IGameStartedEvent e)
+// {
+//
+// }
+// }
+// }
+// }
diff --git a/Impostor-dev/src/Impostor.Benchmarks/Tests/MessageReaderBenchmark.cs b/Impostor-dev/src/Impostor.Benchmarks/Tests/MessageReaderBenchmark.cs
new file mode 100644
index 0000000..abf1642
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Benchmarks/Tests/MessageReaderBenchmark.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Buffers.Binary;
+using BenchmarkDotNet.Attributes;
+using Impostor.Benchmarks.Data;
+using Impostor.Benchmarks.Data.Pool;
+using Impostor.Benchmarks.Extensions;
+using Impostor.Hazel;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ObjectPool;
+using MessageWriter = Impostor.Benchmarks.Data.MessageWriter;
+
+namespace Impostor.Benchmarks.Tests
+{
+ public class MessageReaderBenchmark
+ {
+ private byte[] _data;
+ private ObjectPool<MessageReader_Bytes_Pooled_Improved> _pool;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ var message = new MessageWriter(1024);
+
+ message.StartMessage(1);
+ message.Write((ushort)3100);
+ message.Write((byte)100);
+ message.Write((int) int.MaxValue);
+ message.WritePacked(int.MaxValue);
+ message.EndMessage();
+
+ _data = message.ToByteArray(true);
+
+ MessageReader_Bytes_Pooled.Return(MessageReader_Bytes_Pooled.Get(_data));
+
+ // Services
+ var services = new ServiceCollection();
+
+ services.AddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider());
+ services.AddSingleton(serviceProvider =>
+ {
+ var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
+ var policy = new MessageReader_Bytes_Pooled_ImprovedPolicy(serviceProvider);
+ return provider.Create(policy);
+ });
+
+ _pool = services
+ .BuildServiceProvider()
+ .GetRequiredService<ObjectPool<MessageReader_Bytes_Pooled_Improved>>();
+ }
+
+ [Benchmark]
+ public void Span_Run_1_000_000()
+ {
+ for (var i = 0; i < 1_000_000; i++)
+ {
+ var span = _data.AsSpan();
+ var inner = span.ReadMessage();
+
+ _ = BinaryPrimitives.ReadUInt16LittleEndian(inner);
+ _ = inner[2];
+ _ = BinaryPrimitives.ReadInt32LittleEndian(inner.Slice(3));
+ }
+ }
+
+ // [Benchmark]
+ // public void Normal_Run_1_000_000()
+ // {
+ // for (var i = 0; i < 1_000_000; i++)
+ // {
+ // var reader = new MessageReader(_data);
+ // var inner = reader.ReadMessage();
+ //
+ // _ = inner.ReadUInt16();
+ // _ = inner.ReadByte();
+ // _ = inner.ReadInt32();
+ // // inner.ReadPackedInt32();
+ // }
+ // }
+
+ [Benchmark]
+ public void Bytes_Run_1_000_000()
+ {
+ for (var i = 0; i < 1_000_000; i++)
+ {
+ var reader = new MessageReader_Bytes(_data);
+ var inner = reader.ReadMessage();
+
+ _ = inner.ReadUInt16();
+ _ = inner.ReadByte();
+ _ = inner.ReadInt32();
+ // inner.ReadPackedInt32();
+ }
+ }
+
+ [Benchmark]
+ public void Pooled_Bytes_Run_1_000_000()
+ {
+ for (var i = 0; i < 1_000_000; i++)
+ {
+ var reader = MessageReader_Bytes_Pooled.Get(_data);
+ var inner = reader.ReadMessage();
+
+ _ = inner.ReadUInt16();
+ _ = inner.ReadByte();
+ _ = inner.ReadInt32();
+ // inner.ReadPackedInt32();
+
+ MessageReader_Bytes_Pooled.Return(inner);
+ MessageReader_Bytes_Pooled.Return(reader);
+ }
+ }
+
+ [Benchmark]
+ public void Improved_Pooled_Bytes_Run_1_000_000()
+ {
+ using (var reader = _pool.Get())
+ {
+ for (var i = 0; i < 1_000_000; i++)
+ {
+ reader.Update(_data);
+
+ using (var inner = reader.ReadMessage())
+ {
+ _ = inner.ReadUInt16();
+ _ = inner.ReadByte();
+ _ = inner.ReadInt32();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Client.App/Impostor.Client.App.csproj b/Impostor-dev/src/Impostor.Client.App/Impostor.Client.App.csproj
new file mode 100644
index 0000000..886e26e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Client.App/Impostor.Client.App.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Client\Impostor.Client.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
+ </ItemGroup>
+
+</Project>
diff --git a/Impostor-dev/src/Impostor.Client.App/Program.cs b/Impostor-dev/src/Impostor.Client.App/Program.cs
new file mode 100644
index 0000000..aa8866a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Client.App/Program.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Messages;
+using Impostor.Api.Net.Messages.C2S;
+using Impostor.Hazel;
+using Impostor.Hazel.Udp;
+using Serilog;
+
+namespace Impostor.Client.App
+{
+ internal static class Program
+ {
+ private static readonly ManualResetEvent QuitEvent = new ManualResetEvent(false);
+
+ private static async Task Main(string[] args)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console()
+ .CreateLogger();
+
+ var writeHandshake = MessageWriter.Get(MessageType.Reliable);
+
+ writeHandshake.Write(50516550);
+ writeHandshake.Write("AeonLucid");
+
+ var writeGameCreate = MessageWriter.Get(MessageType.Reliable);
+
+ Message00HostGameC2S.Serialize(writeGameCreate, new GameOptionsData
+ {
+ MaxPlayers = 4,
+ NumImpostors = 2
+ });
+
+ // TODO: ObjectPool for MessageReaders
+ using (var connection = new UdpClientConnection(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 22023), null))
+ {
+ var e = new ManualResetEvent(false);
+
+ // Register events.
+ connection.DataReceived = DataReceived;
+ connection.Disconnected = Disconnected;
+
+ // Connect and send handshake.
+ await connection.ConnectAsync(writeHandshake.ToByteArray(false));
+ Log.Information("Connected.");
+
+ // Create a game.
+ await connection.SendAsync(writeGameCreate);
+ Log.Information("Requested game creation.");
+
+ // Recycle.
+ writeHandshake.Recycle();
+ writeGameCreate.Recycle();
+
+ e.WaitOne();
+ }
+ }
+
+ private static ValueTask DataReceived(DataReceivedEventArgs e)
+ {
+ Log.Information("Received data.");
+ return default;
+ }
+
+ private static ValueTask Disconnected(DisconnectedEventArgs e)
+ {
+ Log.Information("Disconnected: " + e.Reason);
+ QuitEvent.Set();
+ return default;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Client/Impostor.Client.csproj b/Impostor-dev/src/Impostor.Client/Impostor.Client.csproj
new file mode 100644
index 0000000..28b6ed3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Client/Impostor.Client.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
+ <ProjectReference Include="..\Impostor.Hazel\Impostor.Hazel.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Impostor-dev/src/Impostor.Hazel/Connection.cs b/Impostor-dev/src/Impostor.Hazel/Connection.cs
new file mode 100644
index 0000000..dec8cfe
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Connection.cs
@@ -0,0 +1,249 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+using Serilog;
+
+namespace Impostor.Hazel
+{
+ /// <summary>
+ /// Base class for all connections.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Connection is the base class for all connections that Hazel can make. It provides common functionality and a
+ /// standard interface to allow connections to be swapped easily.
+ /// </para>
+ /// <para>
+ /// Any class inheriting from Connection should provide the 3 standard guarantees that Hazel provides:
+ /// <list type="bullet">
+ /// <item>
+ /// <description>Thread Safe</description>
+ /// </item>
+ /// <item>
+ /// <description>Connection Orientated</description>
+ /// </item>
+ /// <item>
+ /// <description>Packet/Message Based</description>
+ /// </item>
+ /// </list>
+ /// </para>
+ /// </remarks>
+ /// <threadsafety static="true" instance="true"/>
+ public abstract class Connection : IDisposable
+ {
+ private static readonly ILogger Logger = Log.ForContext<Connection>();
+
+ /// <summary>
+ /// Called when a message has been received.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// DataReceived is invoked everytime a message is received from the end point of this connection, the message
+ /// that was received can be found in the <see cref="DataReceivedEventArgs"/> alongside other information from the
+ /// event.
+ /// </para>
+ /// <include file="DocInclude/common.xml" path="docs/item[@name='Event_Thread_Safety_Warning']/*" />
+ /// </remarks>
+ /// <example>
+ /// <code language="C#" source="DocInclude/TcpClientExample.cs"/>
+ /// </example>
+ public Func<DataReceivedEventArgs, ValueTask> DataReceived;
+
+ public int TestLagMs = -1;
+ public int TestDropRate = 0;
+ protected int testDropCount = 0;
+
+ /// <summary>
+ /// Called when the end point disconnects or an error occurs.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Disconnected is invoked when the connection is closed due to an exception occuring or because the remote
+ /// end point disconnected. If it was invoked due to an exception occuring then the exception is available
+ /// in the <see cref="DisconnectedEventArgs"/> passed with the event.
+ /// </para>
+ /// <include file="DocInclude/common.xml" path="docs/item[@name='Event_Thread_Safety_Warning']/*" />
+ /// </remarks>
+ /// <example>
+ /// <code language="C#" source="DocInclude/TcpClientExample.cs"/>
+ /// </example>
+ public Func<DisconnectedEventArgs, ValueTask> Disconnected;
+
+ /// <summary>
+ /// The remote end point of this Connection.
+ /// </summary>
+ /// <remarks>
+ /// This is the end point that this connection is connected to (i.e. the other device). This returns an abstract
+ /// <see cref="ConnectionEndPoint"/> which can then be cast to an appropriate end point depending on the
+ /// connection type.
+ /// </remarks>
+ public IPEndPoint EndPoint { get; protected set; }
+
+ public IPMode IPMode { get; protected set; }
+
+ /// <summary>
+ /// The traffic statistics about this Connection.
+ /// </summary>
+ /// <remarks>
+ /// Contains statistics about the number of messages and bytes sent and received by this connection.
+ /// </remarks>
+ public ConnectionStatistics Statistics { get; protected set; }
+
+ /// <summary>
+ /// The state of this connection.
+ /// </summary>
+ /// <remarks>
+ /// All implementers should be aware that when this is set to ConnectionState.Connected it will
+ /// release all threads that are blocked on <see cref="WaitOnConnect"/>.
+ /// </remarks>
+ public ConnectionState State
+ {
+ get
+ {
+ return this._state;
+ }
+
+ protected set
+ {
+ this._state = value;
+ this.SetState(value);
+ }
+ }
+
+ protected ConnectionState _state;
+ protected virtual void SetState(ConnectionState state) { }
+
+ /// <summary>
+ /// Constructor that initializes the ConnecitonStatistics object.
+ /// </summary>
+ /// <remarks>
+ /// This constructor initialises <see cref="Statistics"/> with empty statistics and sets <see cref="State"/> to
+ /// <see cref="ConnectionState.NotConnected"/>.
+ /// </remarks>
+ protected Connection()
+ {
+ this.Statistics = new ConnectionStatistics();
+ this.State = ConnectionState.NotConnected;
+ }
+
+ /// <summary>
+ /// Sends a number of bytes to the end point of the connection using the specified <see cref="MessageType"/>.
+ /// </summary>
+ /// <param name="msg">The message to send.</param>
+ /// <remarks>
+ /// <include file="DocInclude/common.xml" path="docs/item[@name='Connection_SendBytes_General']/*" />
+ /// <para>
+ /// The messageType parameter is only a request to use those options and the actual method used to send the
+ /// data is up to the implementation. There are circumstances where this parameter may be ignored but in
+ /// general any implementer should aim to always follow the user's request.
+ /// </para>
+ /// </remarks>
+ public abstract ValueTask SendAsync(IMessageWriter msg);
+
+ /// <summary>
+ /// Sends a number of bytes to the end point of the connection using the specified <see cref="MessageType"/>.
+ /// </summary>
+ /// <param name="bytes">The bytes of the message to send.</param>
+ /// <param name="messageType">The option specifying how the message should be sent.</param>
+ /// <remarks>
+ /// <include file="DocInclude/common.xml" path="docs/item[@name='Connection_SendBytes_General']/*" />
+ /// <para>
+ /// The messageType parameter is only a request to use those options and the actual method used to send the
+ /// data is up to the implementation. There are circumstances where this parameter may be ignored but in
+ /// general any implementer should aim to always follow the user's request.
+ /// </para>
+ /// </remarks>
+ public abstract ValueTask SendBytes(byte[] bytes, MessageType messageType = MessageType.Unreliable);
+
+ /// <summary>
+ /// Connects the connection to a server and begins listening.
+ /// This method does not block.
+ /// </summary>
+ /// <param name="bytes">The bytes of data to send in the handshake.</param>
+ public abstract ValueTask ConnectAsync(byte[] bytes = null);
+
+ /// <summary>
+ /// Invokes the DataReceived event.
+ /// </summary>
+ /// <param name="msg">The bytes received.</param>
+ /// <param name="messageType">The <see cref="MessageType"/> the message was received with.</param>
+ /// <remarks>
+ /// Invokes the <see cref="DataReceived"/> event on this connection to alert subscribers a new message has been
+ /// received. The bytes and the send option that the message was sent with should be passed in to give to the
+ /// subscribers.
+ /// </remarks>
+ protected async ValueTask InvokeDataReceived(IMessageReader msg, MessageType messageType)
+ {
+ // Make a copy to avoid race condition between null check and invocation
+ var handler = DataReceived;
+ if (handler != null)
+ {
+ try
+ {
+ await handler(new DataReceivedEventArgs(this, msg, messageType));
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Invoking data received failed");
+ await Disconnect("Invoking data received failed");
+ }
+ }
+ }
+
+ /// <summary>
+ /// Invokes the Disconnected event.
+ /// </summary>
+ /// <param name="e">The exception, if any, that occurred to cause this.</param>
+ /// <param name="reader">Extra disconnect data</param>
+ /// <remarks>
+ /// Invokes the <see cref="Disconnected"/> event to alert subscribres this connection has been disconnected either
+ /// by the end point or because an error occurred. If an error occurred the error should be passed in in order to
+ /// pass to the subscribers, otherwise null can be passed in.
+ /// </remarks>
+ protected async ValueTask InvokeDisconnected(string e, IMessageReader reader)
+ {
+ // Make a copy to avoid race condition between null check and invocation
+ var handler = Disconnected;
+ if (handler != null)
+ {
+ try
+ {
+ await handler(new DisconnectedEventArgs(e, reader));
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, "Error in InvokeDisconnected");
+ }
+ }
+ }
+
+ /// <summary>
+ /// For times when you want to force the disconnect handler to fire as well as close it.
+ /// If you only want to close it, just use Dispose.
+ /// </summary>
+ public abstract ValueTask Disconnect(string reason, MessageWriter writer = null);
+
+ /// <summary>
+ /// Disposes of this NetworkConnection.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Disposes of this NetworkConnection.
+ /// </summary>
+ /// <param name="disposing">Are we currently disposing?</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ this.DataReceived = null;
+ this.Disconnected = null;
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/ConnectionListener.cs b/Impostor-dev/src/Impostor.Hazel/ConnectionListener.cs
new file mode 100644
index 0000000..116f657
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/ConnectionListener.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+using Serilog;
+
+namespace Impostor.Hazel
+{
+ /// <summary>
+ /// Base class for all connection listeners.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// ConnectionListeners are server side objects that listen for clients and create matching server side connections
+ /// for each client in a similar way to TCP does. These connections should be ready for communication immediately.
+ /// </para>
+ /// <para>
+ /// Each time a client connects the <see cref="NewConnection"/> event will be invoked to alert all subscribers to
+ /// the new connection. A disconnected event is then present on the <see cref="Connection"/> that is passed to the
+ /// subscribers.
+ /// </para>
+ /// </remarks>
+ /// <threadsafety static="true" instance="true"/>
+ public abstract class ConnectionListener : IAsyncDisposable
+ {
+ private static readonly ILogger Logger = Log.ForContext<ConnectionListener>();
+
+ /// <summary>
+ /// Invoked when a new client connects.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// NewConnection is invoked each time a client connects to the listener. The
+ /// <see cref="NewConnectionEventArgs"/> contains the new <see cref="Connection"/> for communication with this
+ /// client.
+ /// </para>
+ /// <para>
+ /// Hazel may or may not store connections so it is your responsibility to keep track and properly Dispose of
+ /// connections to your server.
+ /// </para>
+ /// <include file="DocInclude/common.xml" path="docs/item[@name='Event_Thread_Safety_Warning']/*" />
+ /// </remarks>
+ /// <example>
+ /// <code language="C#" source="DocInclude/TcpListenerExample.cs"/>
+ /// </example>
+ public Func<NewConnectionEventArgs, ValueTask> NewConnection;
+
+ /// <summary>
+ /// Makes this connection listener begin listening for connections.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This instructs the listener to begin listening for new clients connecting to the server. When a new client
+ /// connects the <see cref="NewConnection"/> event will be invoked containing the connection to the new client.
+ /// </para>
+ /// <para>
+ /// To stop listening you should call <see cref="DisposeAsync()"/>.
+ /// </para>
+ /// </remarks>
+ /// <example>
+ /// <code language="C#" source="DocInclude/TcpListenerExample.cs"/>
+ /// </example>
+ public abstract Task StartAsync();
+
+ /// <summary>
+ /// Invokes the NewConnection event with the supplied connection.
+ /// </summary>
+ /// <param name="msg">The user sent bytes that were received as part of the handshake.</param>
+ /// <param name="connection">The connection to pass in the arguments.</param>
+ /// <remarks>
+ /// Implementers should call this to invoke the <see cref="NewConnection"/> event before data is received so that
+ /// subscribers do not miss any data that may have been sent immediately after connecting.
+ /// </remarks>
+ internal async Task InvokeNewConnection(IMessageReader msg, Connection connection)
+ {
+ // Make a copy to avoid race condition between null check and invocation
+ var handler = NewConnection;
+ if (handler != null)
+ {
+ try
+ {
+ await handler(new NewConnectionEventArgs(msg, connection));
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Accepting connection failed");
+ await connection.Disconnect("Accepting connection failed");
+ }
+ }
+ }
+
+ /// <summary>
+ /// Call to dispose of the connection listener.
+ /// </summary>
+ public virtual ValueTask DisposeAsync()
+ {
+ this.NewConnection = null;
+ return ValueTask.CompletedTask;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/ConnectionState.cs b/Impostor-dev/src/Impostor.Hazel/ConnectionState.cs
new file mode 100644
index 0000000..5dd7c6a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/ConnectionState.cs
@@ -0,0 +1,23 @@
+namespace Impostor.Hazel
+{
+ /// <summary>
+ /// Represents the state a <see cref="Connection"/> is currently in.
+ /// </summary>
+ public enum ConnectionState
+ {
+ /// <summary>
+ /// The Connection has either not been established yet or has been disconnected.
+ /// </summary>
+ NotConnected,
+
+ /// <summary>
+ /// The Connection is currently connecting to an endpoint.
+ /// </summary>
+ Connecting,
+
+ /// <summary>
+ /// The Connection is connected and data can be transfered.
+ /// </summary>
+ Connected,
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/ConnectionStatistics.cs b/Impostor-dev/src/Impostor.Hazel/ConnectionStatistics.cs
new file mode 100644
index 0000000..4802620
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/ConnectionStatistics.cs
@@ -0,0 +1,566 @@
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+[assembly: InternalsVisibleTo("Hazel.Tests")]
+namespace Impostor.Hazel
+{
+ /// <summary>
+ /// Holds statistics about the traffic through a <see cref="Connection"/>.
+ /// </summary>
+ /// <threadsafety static="true" instance="true"/>
+ public class ConnectionStatistics
+ {
+ private const int ExpectedMTU = 1200;
+
+ /// <summary>
+ /// The total number of messages sent.
+ /// </summary>
+ public int MessagesSent
+ {
+ get
+ {
+ return UnreliableMessagesSent + ReliableMessagesSent + FragmentedMessagesSent + AcknowledgementMessagesSent + HelloMessagesSent;
+ }
+ }
+
+ /// <summary>
+ /// The number of messages sent larger than 576 bytes. This is smaller than most default MTUs.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of unreliable messages that were sent from the <see cref="Connection"/>, incremented
+ /// each time that LogUnreliableSend is called by the Connection. Messages that caused an error are not
+ /// counted and messages are only counted once all other operations in the send are complete.
+ /// </remarks>
+ public int FragmentableMessagesSent
+ {
+ get
+ {
+ return fragmentableMessagesSent;
+ }
+ }
+
+ /// <summary>
+ /// The number of messages sent larger than 576 bytes.
+ /// </summary>
+ int fragmentableMessagesSent;
+
+ /// <summary>
+ /// The number of unreliable messages sent.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of unreliable messages that were sent from the <see cref="Connection"/>, incremented
+ /// each time that LogUnreliableSend is called by the Connection. Messages that caused an error are not
+ /// counted and messages are only counted once all other operations in the send are complete.
+ /// </remarks>
+ public int UnreliableMessagesSent
+ {
+ get
+ {
+ return unreliableMessagesSent;
+ }
+ }
+
+ /// <summary>
+ /// The number of unreliable messages sent.
+ /// </summary>
+ int unreliableMessagesSent;
+
+ /// <summary>
+ /// The number of reliable messages sent.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of reliable messages that were sent from the <see cref="Connection"/>, incremented
+ /// each time that LogReliableSend is called by the Connection. Messages that caused an error are not
+ /// counted and messages are only counted once all other operations in the send are complete.
+ /// </remarks>
+ public int ReliableMessagesSent
+ {
+ get
+ {
+ return reliableMessagesSent;
+ }
+ }
+
+ /// <summary>
+ /// The number of unreliable messages sent.
+ /// </summary>
+ int reliableMessagesSent;
+
+ /// <summary>
+ /// The number of fragmented messages sent.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of fragmented messages that were sent from the <see cref="Connection"/>, incremented
+ /// each time that LogFragmentedSend is called by the Connection. Messages that caused an error are not
+ /// counted and messages are only counted once all other operations in the send are complete.
+ /// </remarks>
+ public int FragmentedMessagesSent
+ {
+ get
+ {
+ return fragmentedMessagesSent;
+ }
+ }
+
+ /// <summary>
+ /// The number of fragmented messages sent.
+ /// </summary>
+ int fragmentedMessagesSent;
+
+ /// <summary>
+ /// The number of acknowledgement messages sent.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of acknowledgements that were sent from the <see cref="Connection"/>, incremented
+ /// each time that LogAcknowledgementSend is called by the Connection. Messages that caused an error are not
+ /// counted and messages are only counted once all other operations in the send are complete.
+ /// </remarks>
+ public int AcknowledgementMessagesSent
+ {
+ get
+ {
+ return acknowledgementMessagesSent;
+ }
+ }
+
+ /// <summary>
+ /// The number of acknowledgement messages sent.
+ /// </summary>
+ int acknowledgementMessagesSent;
+
+ /// <summary>
+ /// The number of hello messages sent.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of hello messages that were sent from the <see cref="Connection"/>, incremented
+ /// each time that LogHelloSend is called by the Connection. Messages that caused an error are not
+ /// counted and messages are only counted once all other operations in the send are complete.
+ /// </remarks>
+ public int HelloMessagesSent
+ {
+ get
+ {
+ return helloMessagesSent;
+ }
+ }
+
+ /// <summary>
+ /// The number of hello messages sent.
+ /// </summary>
+ int helloMessagesSent;
+
+ /// <summary>
+ /// The number of bytes of data sent.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This is the number of bytes of data (i.e. user bytes) that were sent from the <see cref="Connection"/>,
+ /// accumulated each time that LogSend is called by the Connection. Messages that caused an error are not
+ /// counted and messages are only counted once all other operations in the send are complete.
+ /// </para>
+ /// <para>
+ /// For the number of bytes including protocol bytes see <see cref="TotalBytesSent"/>.
+ /// </para>
+ /// </remarks>
+ public long DataBytesSent
+ {
+ get
+ {
+ return Interlocked.Read(ref dataBytesSent);
+ }
+ }
+
+ /// <summary>
+ /// The number of bytes of data sent.
+ /// </summary>
+ long dataBytesSent;
+
+ /// <summary>
+ /// The number of bytes sent in total.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This is the total number of bytes (the data bytes plus protocol bytes) that were sent from the
+ /// <see cref="Connection"/>, accumulated each time that LogSend is called by the Connection. Messages that
+ /// caused an error are not counted and messages are only counted once all other operations in the send are
+ /// complete.
+ /// </para>
+ /// <para>
+ /// For the number of data bytes excluding protocol bytes see <see cref="DataBytesSent"/>.
+ /// </para>
+ /// </remarks>
+ public long TotalBytesSent
+ {
+ get
+ {
+ return Interlocked.Read(ref totalBytesSent);
+ }
+ }
+
+ /// <summary>
+ /// The number of bytes sent in total.
+ /// </summary>
+ long totalBytesSent;
+
+ /// <summary>
+ /// The total number of messages received.
+ /// </summary>
+ public int MessagesReceived
+ {
+ get
+ {
+ return UnreliableMessagesReceived + ReliableMessagesReceived + FragmentedMessagesReceived + AcknowledgementMessagesReceived + helloMessagesReceived;
+ }
+ }
+
+ /// <summary>
+ /// The number of unreliable messages received.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of unreliable messages that were received by the <see cref="Connection"/>, incremented
+ /// each time that LogUnreliableReceive is called by the Connection. Messages are counted before the receive event is invoked.
+ /// </remarks>
+ public int UnreliableMessagesReceived
+ {
+ get
+ {
+ return unreliableMessagesReceived;
+ }
+ }
+
+ /// <summary>
+ /// The number of unreliable messages received.
+ /// </summary>
+ int unreliableMessagesReceived;
+
+ /// <summary>
+ /// The number of reliable messages received.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of reliable messages that were received by the <see cref="Connection"/>, incremented
+ /// each time that LogReliableReceive is called by the Connection. Messages are counted before the receive event is invoked.
+ /// </remarks>
+ public int ReliableMessagesReceived
+ {
+ get
+ {
+ return reliableMessagesReceived;
+ }
+ }
+
+ /// <summary>
+ /// The number of reliable messages received.
+ /// </summary>
+ int reliableMessagesReceived;
+
+ /// <summary>
+ /// The number of fragmented messages received.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of fragmented messages that were received by the <see cref="Connection"/>, incremented
+ /// each time that LogFragmentedReceive is called by the Connection. Messages are counted before the receive event is invoked.
+ /// </remarks>
+ public int FragmentedMessagesReceived
+ {
+ get
+ {
+ return fragmentedMessagesReceived;
+ }
+ }
+
+ /// <summary>
+ /// The number of fragmented messages received.
+ /// </summary>
+ int fragmentedMessagesReceived;
+
+ /// <summary>
+ /// The number of acknowledgement messages received.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of acknowledgement messages that were received by the <see cref="Connection"/>, incremented
+ /// each time that LogAcknowledgemntReceive is called by the Connection. Messages are counted before the receive event is invoked.
+ /// </remarks>
+ public int AcknowledgementMessagesReceived
+ {
+ get
+ {
+ return acknowledgementMessagesReceived;
+ }
+ }
+
+ /// <summary>
+ /// The number of acknowledgement messages received.
+ /// </summary>
+ int acknowledgementMessagesReceived;
+
+ /// <summary>
+ /// The number of ping messages received.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of hello messages that were received by the <see cref="Connection"/>, incremented
+ /// each time that LogHelloReceive is called by the Connection. Messages are counted before the receive event is invoked.
+ /// </remarks>
+ public int PingMessagesReceived
+ {
+ get
+ {
+ return pingMessagesReceived;
+ }
+ }
+
+ /// <summary>
+ /// The number of hello messages received.
+ /// </summary>
+ int pingMessagesReceived;
+
+ /// <summary>
+ /// The number of hello messages received.
+ /// </summary>
+ /// <remarks>
+ /// This is the number of hello messages that were received by the <see cref="Connection"/>, incremented
+ /// each time that LogHelloReceive is called by the Connection. Messages are counted before the receive event is invoked.
+ /// </remarks>
+ public int HelloMessagesReceived
+ {
+ get
+ {
+ return helloMessagesReceived;
+ }
+ }
+
+ /// <summary>
+ /// The number of hello messages received.
+ /// </summary>
+ int helloMessagesReceived;
+
+ /// <summary>
+ /// The number of bytes of data received.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This is the number of bytes of data (i.e. user bytes) that were received by the <see cref="Connection"/>,
+ /// accumulated each time that LogReceive is called by the Connection. Messages are counted before the receive
+ /// event is invoked.
+ /// </para>
+ /// <para>
+ /// For the number of bytes including protocol bytes see <see cref="TotalBytesReceived"/>.
+ /// </para>
+ /// </remarks>
+ public long DataBytesReceived
+ {
+ get
+ {
+ return Interlocked.Read(ref dataBytesReceived);
+ }
+ }
+
+ /// <summary>
+ /// The number of bytes of data received.
+ /// </summary>
+ long dataBytesReceived;
+
+ /// <summary>
+ /// The number of bytes received in total.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This is the total number of bytes (the data bytes plus protocol bytes) that were received by the
+ /// <see cref="Connection"/>, accumulated each time that LogReceive is called by the Connection. Messages are
+ /// counted before the receive event is invoked.
+ /// </para>
+ /// <para>
+ /// For the number of data bytes excluding protocol bytes see <see cref="DataBytesReceived"/>.
+ /// </para>
+ /// </remarks>
+ public long TotalBytesReceived
+ {
+ get
+ {
+ return Interlocked.Read(ref totalBytesReceived);
+ }
+ }
+
+ /// <summary>
+ /// The number of bytes received in total.
+ /// </summary>
+ long totalBytesReceived;
+
+ public int MessagesResent { get { return messagesResent; } }
+ int messagesResent;
+
+ /// <summary>
+ /// Logs the sending of an unreliable data packet in the statistics.
+ /// </summary>
+ /// <param name="dataLength">The number of bytes of data sent.</param>
+ /// <param name="totalLength">The total number of bytes sent.</param>
+ /// <remarks>
+ /// This should be called after the data has been sent and should only be called for data that is sent sucessfully.
+ /// </remarks>
+ internal void LogUnreliableSend(int dataLength, int totalLength)
+ {
+ Interlocked.Increment(ref unreliableMessagesSent);
+ Interlocked.Add(ref dataBytesSent, dataLength);
+ Interlocked.Add(ref totalBytesSent, totalLength);
+
+ if (totalLength > ExpectedMTU)
+ {
+ Interlocked.Increment(ref fragmentableMessagesSent);
+ }
+ }
+
+ /// <summary>
+ /// Logs the sending of a reliable data packet in the statistics.
+ /// </summary>
+ /// <param name="dataLength">The number of bytes of data sent.</param>
+ /// <param name="totalLength">The total number of bytes sent.</param>
+ /// <remarks>
+ /// This should be called after the data has been sent and should only be called for data that is sent sucessfully.
+ /// </remarks>
+ internal void LogReliableSend(int dataLength, int totalLength)
+ {
+ Interlocked.Increment(ref reliableMessagesSent);
+ Interlocked.Add(ref dataBytesSent, dataLength);
+ Interlocked.Add(ref totalBytesSent, totalLength);
+
+ if (totalLength > ExpectedMTU)
+ {
+ Interlocked.Increment(ref fragmentableMessagesSent);
+ }
+ }
+
+ /// <summary>
+ /// Logs the sending of a fragmented data packet in the statistics.
+ /// </summary>
+ /// <param name="dataLength">The number of bytes of data sent.</param>
+ /// <param name="totalLength">The total number of bytes sent.</param>
+ /// <remarks>
+ /// This should be called after the data has been sent and should only be called for data that is sent sucessfully.
+ /// </remarks>
+ internal void LogFragmentedSend(int dataLength, int totalLength)
+ {
+ Interlocked.Increment(ref fragmentedMessagesSent);
+ Interlocked.Add(ref dataBytesSent, dataLength);
+ Interlocked.Add(ref totalBytesSent, totalLength);
+
+ if (totalLength > ExpectedMTU)
+ {
+ Interlocked.Increment(ref fragmentableMessagesSent);
+ }
+ }
+
+ /// <summary>
+ /// Logs the sending of a acknowledgement data packet in the statistics.
+ /// </summary>
+ /// <param name="totalLength">The total number of bytes sent.</param>
+ /// <remarks>
+ /// This should be called after the data has been sent and should only be called for data that is sent sucessfully.
+ /// </remarks>
+ internal void LogAcknowledgementSend(int totalLength)
+ {
+ Interlocked.Increment(ref acknowledgementMessagesSent);
+ Interlocked.Add(ref totalBytesSent, totalLength);
+ }
+
+ /// <summary>
+ /// Logs the sending of a hellp data packet in the statistics.
+ /// </summary>
+ /// <param name="totalLength">The total number of bytes sent.</param>
+ /// <remarks>
+ /// This should be called after the data has been sent and should only be called for data that is sent sucessfully.
+ /// </remarks>
+ internal void LogHelloSend(int totalLength)
+ {
+ Interlocked.Increment(ref helloMessagesSent);
+ Interlocked.Add(ref totalBytesSent, totalLength);
+ }
+
+ /// <summary>
+ /// Logs the receiving of an unreliable data packet in the statistics.
+ /// </summary>
+ /// <param name="dataLength">The number of bytes of data received.</param>
+ /// <param name="totalLength">The total number of bytes received.</param>
+ /// <remarks>
+ /// This should be called before the received event is invoked so it is up to date for subscribers to that event.
+ /// </remarks>
+ internal void LogUnreliableReceive(int dataLength, int totalLength)
+ {
+ Interlocked.Increment(ref unreliableMessagesReceived);
+ Interlocked.Add(ref dataBytesReceived, dataLength);
+ Interlocked.Add(ref totalBytesReceived, totalLength);
+ }
+
+ /// <summary>
+ /// Logs the receiving of a reliable data packet in the statistics.
+ /// </summary>
+ /// <param name="dataLength">The number of bytes of data received.</param>
+ /// <param name="totalLength">The total number of bytes received.</param>
+ /// <remarks>
+ /// This should be called before the received event is invoked so it is up to date for subscribers to that event.
+ /// </remarks>
+ internal void LogReliableReceive(int dataLength, int totalLength)
+ {
+ Interlocked.Increment(ref reliableMessagesReceived);
+ Interlocked.Add(ref dataBytesReceived, dataLength);
+ Interlocked.Add(ref totalBytesReceived, totalLength);
+ }
+
+ /// <summary>
+ /// Logs the receiving of a fragmented data packet in the statistics.
+ /// </summary>
+ /// <param name="dataLength">The number of bytes of data received.</param>
+ /// <param name="totalLength">The total number of bytes received.</param>
+ /// <remarks>
+ /// This should be called before the received event is invoked so it is up to date for subscribers to that event.
+ /// </remarks>
+ internal void LogFragmentedReceive(int dataLength, int totalLength)
+ {
+ Interlocked.Increment(ref fragmentedMessagesReceived);
+ Interlocked.Add(ref dataBytesReceived, dataLength);
+ Interlocked.Add(ref totalBytesReceived, totalLength);
+ }
+
+ /// <summary>
+ /// Logs the receiving of an acknowledgement data packet in the statistics.
+ /// </summary>
+ /// <param name="totalLength">The total number of bytes received.</param>
+ /// <remarks>
+ /// This should be called before the received event is invoked so it is up to date for subscribers to that event.
+ /// </remarks>
+ internal void LogAcknowledgementReceive(int totalLength)
+ {
+ Interlocked.Increment(ref acknowledgementMessagesReceived);
+ Interlocked.Add(ref totalBytesReceived, totalLength);
+ }
+
+ /// <summary>
+ /// Logs the receiving of a hello data packet in the statistics.
+ /// </summary>
+ /// <param name="totalLength">The total number of bytes received.</param>
+ /// <remarks>
+ /// This should be called before the received event is invoked so it is up to date for subscribers to that event.
+ /// </remarks>
+ internal void LogPingReceive(int totalLength)
+ {
+ Interlocked.Increment(ref pingMessagesReceived);
+ Interlocked.Add(ref totalBytesReceived, totalLength);
+ }
+
+ /// <summary>
+ /// Logs the receiving of a hello data packet in the statistics.
+ /// </summary>
+ /// <param name="totalLength">The total number of bytes received.</param>
+ /// <remarks>
+ /// This should be called before the received event is invoked so it is up to date for subscribers to that event.
+ /// </remarks>
+ internal void LogHelloReceive(int totalLength)
+ {
+ Interlocked.Increment(ref helloMessagesReceived);
+ Interlocked.Add(ref totalBytesReceived, totalLength);
+ }
+
+ internal void LogMessageResent()
+ {
+ Interlocked.Increment(ref messagesResent);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/DataReceivedEventArgs.cs b/Impostor-dev/src/Impostor.Hazel/DataReceivedEventArgs.cs
new file mode 100644
index 0000000..9176d8d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/DataReceivedEventArgs.cs
@@ -0,0 +1,26 @@
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Hazel
+{
+ public struct DataReceivedEventArgs
+ {
+ public readonly Connection Sender;
+
+ /// <summary>
+ /// The bytes received from the client.
+ /// </summary>
+ public readonly IMessageReader Message;
+
+ /// <summary>
+ /// The <see cref="Type"/> the data was sent with.
+ /// </summary>
+ public readonly MessageType Type;
+
+ public DataReceivedEventArgs(Connection sender, IMessageReader msg, MessageType type)
+ {
+ this.Sender = sender;
+ this.Message = msg;
+ this.Type = type;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/DisconnectedEventArgs.cs b/Impostor-dev/src/Impostor.Hazel/DisconnectedEventArgs.cs
new file mode 100644
index 0000000..d46df4b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/DisconnectedEventArgs.cs
@@ -0,0 +1,25 @@
+using System;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Hazel
+{
+ public class DisconnectedEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Optional disconnect reason. May be null.
+ /// </summary>
+ public readonly string Reason;
+
+ /// <summary>
+ /// Optional data sent with a disconnect message. May be null.
+ /// You must not recycle this. If you need the message outside of a callback, you should copy it.
+ /// </summary>
+ public readonly IMessageReader Message;
+
+ public DisconnectedEventArgs(string reason, IMessageReader message)
+ {
+ this.Reason = reason;
+ this.Message = message;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/Extensions/ServiceProviderExtensions.cs b/Impostor-dev/src/Impostor.Hazel/Extensions/ServiceProviderExtensions.cs
new file mode 100644
index 0000000..56c7380
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Extensions/ServiceProviderExtensions.cs
@@ -0,0 +1,21 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Hazel.Extensions
+{
+ public static class ServiceProviderExtensions
+ {
+ public static void AddHazel(this IServiceCollection services)
+ {
+ services.TryAddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider());
+
+ services.AddSingleton(serviceProvider =>
+ {
+ var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
+ var policy = ActivatorUtilities.CreateInstance<MessageReaderPolicy>(serviceProvider);
+ return provider.Create(policy);
+ });
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/HazelException.cs b/Impostor-dev/src/Impostor.Hazel/HazelException.cs
new file mode 100644
index 0000000..8c6fc3c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/HazelException.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Impostor.Hazel
+{
+ /// <summary>
+ /// Wrapper for exceptions thrown from Hazel.
+ /// </summary>
+ [Serializable]
+ public class HazelException : Exception
+ {
+ internal HazelException(string msg) : base (msg)
+ {
+
+ }
+
+ internal HazelException(string msg, Exception e) : base (msg, e)
+ {
+
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/IPMode.cs b/Impostor-dev/src/Impostor.Hazel/IPMode.cs
new file mode 100644
index 0000000..5eb6679
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/IPMode.cs
@@ -0,0 +1,24 @@
+namespace Impostor.Hazel
+{
+ /// <summary>
+ /// Represents the IP version that a connection or listener will use.
+ /// </summary>
+ /// <remarks>
+ /// If you wand a client to connect or be able to connect using IPv6 then you should use <see cref="IPv4AndIPv6"/>,
+ /// this sets the underlying sockets to use IPv6 but still allow IPv4 sockets to connect for backwards compatability
+ /// and hence it is the default IPMode in most cases.
+ /// </remarks>
+ public enum IPMode
+ {
+ /// <summary>
+ /// Instruction to use IPv4 only, IPv6 connections will not be able to connect.
+ /// </summary>
+ IPv4,
+
+ /// <summary>
+ /// Instruction to use IPv6 only, IPv4 connections will not be able to connect. IPv4 addresses can be connected
+ /// by converting to IPv6 addresses.
+ /// </summary>
+ IPv6
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/IRecyclable.cs b/Impostor-dev/src/Impostor.Hazel/IRecyclable.cs
new file mode 100644
index 0000000..69be122
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/IRecyclable.cs
@@ -0,0 +1,24 @@
+namespace Impostor.Hazel
+{
+ /// <summary>
+ /// Interface for all items that can be returned to an object pool.
+ /// </summary>
+ /// <threadsafety static="true" instance="true"/>
+ public interface IRecyclable
+ {
+ /// <summary>
+ /// Returns this object back to the object pool.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Calling this when you are done with the object returns the object back to a pool in order to be reused.
+ /// This can reduce the amount of work the GC has to do dramatically but it is optional to call this.
+ /// </para>
+ /// <para>
+ /// Calling this indicates to Hazel that this can be reused and thus you should only call this when you are
+ /// completely finished with the object as the contents can be overwritten at any point after.
+ /// </para>
+ /// </remarks>
+ void Recycle();
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/Impostor.Hazel.csproj b/Impostor-dev/src/Impostor.Hazel/Impostor.Hazel.csproj
new file mode 100644
index 0000000..3e035fb
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Impostor.Hazel.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <TargetFramework>net5.0</TargetFramework>
+ <DefineConstants>HAZEL_BAG</DefineConstants>
+ <Version>1.0.0</Version>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.0" />
+ <PackageReference Include="Serilog" Version="2.10.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Impostor-dev/src/Impostor.Hazel/MessageReader.cs b/Impostor-dev/src/Impostor.Hazel/MessageReader.cs
new file mode 100644
index 0000000..986d0b0
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/MessageReader.cs
@@ -0,0 +1,256 @@
+using System;
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
+using System.Text;
+using Impostor.Api;
+using Impostor.Api.Net.Messages;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Hazel
+{
+ public class MessageReader : IMessageReader
+ {
+ private static readonly ArrayPool<byte> ArrayPool = ArrayPool<byte>.Shared;
+
+ private readonly ObjectPool<MessageReader> _pool;
+ private bool _inUse;
+
+ internal MessageReader(ObjectPool<MessageReader> pool)
+ {
+ _pool = pool;
+ }
+
+ public byte[] Buffer { get; private set; }
+
+ public int Offset { get; internal set; }
+
+ public int Position { get; internal set; }
+
+ public int Length { get; internal set; }
+
+ public byte Tag { get; private set; }
+
+ public MessageReader Parent { get; private set; }
+
+ private int ReadPosition => Offset + Position;
+
+ public void Update(byte[] buffer, int offset = 0, int position = 0, int? length = null, byte tag = byte.MaxValue, MessageReader parent = null)
+ {
+ _inUse = true;
+
+ Buffer = buffer;
+ Offset = offset;
+ Position = position;
+ Length = length ?? buffer.Length;
+ Tag = tag;
+ Parent = parent;
+ }
+
+ internal void Reset()
+ {
+ _inUse = false;
+
+ Tag = byte.MaxValue;
+ Buffer = null;
+ Offset = 0;
+ Position = 0;
+ Length = 0;
+ Parent = null;
+ }
+
+ public IMessageReader ReadMessage()
+ {
+ var length = ReadUInt16();
+ var tag = FastByte();
+ var pos = ReadPosition;
+
+ Position += length;
+
+ var reader = _pool.Get();
+ reader.Update(Buffer, pos, 0, length, tag, this);
+ return reader;
+ }
+
+ public bool ReadBoolean()
+ {
+ byte val = FastByte();
+ return val != 0;
+ }
+
+ public sbyte ReadSByte()
+ {
+ return (sbyte)FastByte();
+ }
+
+ public byte ReadByte()
+ {
+ return FastByte();
+ }
+
+ public ushort ReadUInt16()
+ {
+ var output = BinaryPrimitives.ReadUInt16LittleEndian(Buffer.AsSpan(ReadPosition));
+ Position += sizeof(ushort);
+ return output;
+ }
+
+ public short ReadInt16()
+ {
+ var output = BinaryPrimitives.ReadInt16LittleEndian(Buffer.AsSpan(ReadPosition));
+ Position += sizeof(short);
+ return output;
+ }
+
+ public uint ReadUInt32()
+ {
+ var output = BinaryPrimitives.ReadUInt32LittleEndian(Buffer.AsSpan(ReadPosition));
+ Position += sizeof(uint);
+ return output;
+ }
+
+ public int ReadInt32()
+ {
+ var output = BinaryPrimitives.ReadInt32LittleEndian(Buffer.AsSpan(ReadPosition));
+ Position += sizeof(int);
+ return output;
+ }
+
+ public unsafe float ReadSingle()
+ {
+ var output = BinaryPrimitives.ReadSingleLittleEndian(Buffer.AsSpan(ReadPosition));
+ Position += sizeof(float);
+ return output;
+ }
+
+ public string ReadString()
+ {
+ var len = ReadPackedInt32();
+ var output = Encoding.UTF8.GetString(Buffer.AsSpan(ReadPosition, len));
+ Position += len;
+ return output;
+ }
+
+ public ReadOnlyMemory<byte> ReadBytesAndSize()
+ {
+ var len = ReadPackedInt32();
+ return ReadBytes(len);
+ }
+
+ public ReadOnlyMemory<byte> ReadBytes(int length)
+ {
+ var output = Buffer.AsMemory(ReadPosition, length);
+ Position += length;
+ return output;
+ }
+
+ public int ReadPackedInt32()
+ {
+ return (int)ReadPackedUInt32();
+ }
+
+ public uint ReadPackedUInt32()
+ {
+ bool readMore = true;
+ int shift = 0;
+ uint output = 0;
+
+ while (readMore)
+ {
+ byte b = FastByte();
+ if (b >= 0x80)
+ {
+ readMore = true;
+ b ^= 0x80;
+ }
+ else
+ {
+ readMore = false;
+ }
+
+ output |= (uint)(b << shift);
+ shift += 7;
+ }
+
+ return output;
+ }
+
+ public void CopyTo(IMessageWriter writer)
+ {
+ writer.Write((ushort) Length);
+ writer.Write((byte) Tag);
+ writer.Write(Buffer.AsMemory(Offset, Length));
+ }
+
+ public void Seek(int position)
+ {
+ Position = position;
+ }
+
+ public void RemoveMessage(IMessageReader message)
+ {
+ if (message.Buffer != Buffer)
+ {
+ throw new ImpostorProtocolException("Tried to remove message from a message that does not have the same buffer.");
+ }
+
+ // Offset of where to start removing.
+ var offsetStart = message.Offset - 3;
+
+ // Offset of where to end removing.
+ var offsetEnd = message.Offset + message.Length;
+
+ // The amount of bytes to copy over ourselves.
+ var lengthToCopy = message.Buffer.Length - offsetEnd;
+
+ System.Buffer.BlockCopy(Buffer, offsetEnd, Buffer, offsetStart, lengthToCopy);
+
+ ((MessageReader) message).Parent.AdjustLength(message.Offset, message.Length + 3);
+ }
+
+ private void AdjustLength(int offset, int amount)
+ {
+ this.Length -= amount;
+
+ if (this.ReadPosition > offset)
+ {
+ this.Position -= amount;
+ }
+
+ if (Parent != null)
+ {
+ var lengthOffset = this.Offset - 3;
+ var curLen = this.Buffer[lengthOffset] |
+ (this.Buffer[lengthOffset + 1] << 8);
+
+ curLen -= amount;
+
+ this.Buffer[lengthOffset] = (byte)curLen;
+ this.Buffer[lengthOffset + 1] = (byte)(this.Buffer[lengthOffset + 1] >> 8);
+
+ Parent.AdjustLength(offset, amount);
+ }
+ }
+
+ public IMessageReader Copy(int offset = 0)
+ {
+ var reader = _pool.Get();
+ reader.Update(Buffer, Offset + offset, Position, Length - offset, Tag, Parent);
+ return reader;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private byte FastByte()
+ {
+ return Buffer[Offset + Position++];
+ }
+
+ public void Dispose()
+ {
+ if (_inUse)
+ {
+ _pool.Return(this);
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/MessageReaderPolicy.cs b/Impostor-dev/src/Impostor.Hazel/MessageReaderPolicy.cs
new file mode 100644
index 0000000..ef3939a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/MessageReaderPolicy.cs
@@ -0,0 +1,27 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Hazel
+{
+ public class MessageReaderPolicy : IPooledObjectPolicy<MessageReader>
+ {
+ private readonly IServiceProvider _serviceProvider;
+
+ public MessageReaderPolicy(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public MessageReader Create()
+ {
+ return new MessageReader(_serviceProvider.GetRequiredService<ObjectPool<MessageReader>>());
+ }
+
+ public bool Return(MessageReader obj)
+ {
+ obj.Reset();
+ return true;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/MessageWriter.cs b/Impostor-dev/src/Impostor.Hazel/MessageWriter.cs
new file mode 100644
index 0000000..5b7342a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/MessageWriter.cs
@@ -0,0 +1,335 @@
+using Impostor.Api.Games;
+using Impostor.Api.Net.Messages;
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text;
+
+namespace Impostor.Hazel
+{
+ public class MessageWriter : IMessageWriter, IRecyclable, IDisposable
+ {
+ private static int BufferSize = 64000;
+ private static readonly ObjectPoolCustom<MessageWriter> WriterPool = new ObjectPoolCustom<MessageWriter>(() => new MessageWriter(BufferSize));
+
+ public MessageType SendOption { get; private set; }
+
+ private Stack<int> messageStarts = new Stack<int>();
+
+ public MessageWriter(byte[] buffer)
+ {
+ this.Buffer = buffer;
+ this.Length = this.Buffer.Length;
+ }
+
+ public MessageWriter(int bufferSize)
+ {
+ this.Buffer = new byte[bufferSize];
+ }
+
+ public byte[] Buffer { get; }
+ public int Length { get; set; }
+ public int Position { get; set; }
+
+ public byte[] ToByteArray(bool includeHeader)
+ {
+ if (includeHeader)
+ {
+ byte[] output = new byte[this.Length];
+ System.Buffer.BlockCopy(this.Buffer, 0, output, 0, this.Length);
+ return output;
+ }
+ else
+ {
+ switch (this.SendOption)
+ {
+ case MessageType.Reliable:
+ {
+ byte[] output = new byte[this.Length - 3];
+ System.Buffer.BlockCopy(this.Buffer, 3, output, 0, this.Length - 3);
+ return output;
+ }
+ case MessageType.Unreliable:
+ {
+ byte[] output = new byte[this.Length - 1];
+ System.Buffer.BlockCopy(this.Buffer, 1, output, 0, this.Length - 1);
+ return output;
+ }
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// <param name="sendOption">The option specifying how the message should be sent.</param>
+ public static MessageWriter Get(MessageType sendOption = MessageType.Unreliable)
+ {
+ var output = WriterPool.GetObject();
+ output.Clear(sendOption);
+
+ return output;
+ }
+
+ public bool HasBytes(int expected)
+ {
+ if (this.SendOption == MessageType.Unreliable)
+ {
+ return this.Length > 1 + expected;
+ }
+
+ return this.Length > 3 + expected;
+ }
+
+ public void Write(GameCode value)
+ {
+ this.Write(value.Value);
+ }
+
+ ///
+ public void StartMessage(byte typeFlag)
+ {
+ messageStarts.Push(this.Position);
+ this.Position += 2; // Skip for size
+ this.Write(typeFlag);
+ }
+
+ ///
+ public void EndMessage()
+ {
+ var lastMessageStart = messageStarts.Pop();
+ ushort length = (ushort)(this.Position - lastMessageStart - 3); // Minus length and type byte
+ this.Buffer[lastMessageStart] = (byte)length;
+ this.Buffer[lastMessageStart + 1] = (byte)(length >> 8);
+ }
+
+ ///
+ public void CancelMessage()
+ {
+ this.Position = this.messageStarts.Pop();
+ this.Length = this.Position;
+ }
+
+ public void Clear(MessageType sendOption)
+ {
+ this.messageStarts.Clear();
+ this.SendOption = sendOption;
+ this.Buffer[0] = (byte)sendOption;
+ switch (sendOption)
+ {
+ default:
+ case MessageType.Unreliable:
+ this.Length = this.Position = 1;
+ break;
+
+ case MessageType.Reliable:
+ this.Length = this.Position = 3;
+ break;
+ }
+ }
+
+ ///
+ public void Recycle()
+ {
+ this.Position = this.Length = 0;
+ WriterPool.PutObject(this);
+ }
+
+ #region WriteMethods
+
+ public void Write(bool value)
+ {
+ this.Buffer[this.Position++] = (byte)(value ? 1 : 0);
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(sbyte value)
+ {
+ this.Buffer[this.Position++] = (byte)value;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(byte value)
+ {
+ this.Buffer[this.Position++] = value;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(short value)
+ {
+ this.Buffer[this.Position++] = (byte)value;
+ this.Buffer[this.Position++] = (byte)(value >> 8);
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(ushort value)
+ {
+ this.Buffer[this.Position++] = (byte)value;
+ this.Buffer[this.Position++] = (byte)(value >> 8);
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(uint value)
+ {
+ this.Buffer[this.Position++] = (byte)value;
+ this.Buffer[this.Position++] = (byte)(value >> 8);
+ this.Buffer[this.Position++] = (byte)(value >> 16);
+ this.Buffer[this.Position++] = (byte)(value >> 24);
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(int value)
+ {
+ this.Buffer[this.Position++] = (byte)value;
+ this.Buffer[this.Position++] = (byte)(value >> 8);
+ this.Buffer[this.Position++] = (byte)(value >> 16);
+ this.Buffer[this.Position++] = (byte)(value >> 24);
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public unsafe void Write(float value)
+ {
+ fixed (byte* ptr = &this.Buffer[this.Position])
+ {
+ byte* valuePtr = (byte*)&value;
+
+ *ptr = *valuePtr;
+ *(ptr + 1) = *(valuePtr + 1);
+ *(ptr + 2) = *(valuePtr + 2);
+ *(ptr + 3) = *(valuePtr + 3);
+ }
+
+ this.Position += 4;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(string value)
+ {
+ var bytes = UTF8Encoding.UTF8.GetBytes(value);
+ this.WritePacked(bytes.Length);
+ this.Write(bytes);
+ }
+
+ public void Write(IPAddress value)
+ {
+ this.Write(value.GetAddressBytes());
+ }
+
+ public void WriteBytesAndSize(byte[] bytes)
+ {
+ this.WritePacked((uint)bytes.Length);
+ this.Write(bytes);
+ }
+
+ public void WriteBytesAndSize(byte[] bytes, int length)
+ {
+ this.WritePacked((uint)length);
+ this.Write(bytes, length);
+ }
+
+ public void WriteBytesAndSize(byte[] bytes, int offset, int length)
+ {
+ this.WritePacked((uint)length);
+ this.Write(bytes, offset, length);
+ }
+
+ public void Write(ReadOnlyMemory<byte> data)
+ {
+ Write(data.Span);
+ }
+
+ public void Write(ReadOnlySpan<byte> 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)
+ {
+ Array.Copy(bytes, 0, this.Buffer, this.Position, bytes.Length);
+ this.Position += bytes.Length;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(byte[] bytes, int offset, int length)
+ {
+ Array.Copy(bytes, offset, this.Buffer, this.Position, length);
+ this.Position += length;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ public void Write(byte[] bytes, int length)
+ {
+ Array.Copy(bytes, 0, this.Buffer, this.Position, length);
+ this.Position += length;
+ if (this.Position > this.Length) this.Length = this.Position;
+ }
+
+ ///
+ public void WritePacked(int value)
+ {
+ this.WritePacked((uint)value);
+ }
+
+ ///
+ public void WritePacked(uint value)
+ {
+ do
+ {
+ byte b = (byte)(value & 0xFF);
+ if (value >= 0x80)
+ {
+ b |= 0x80;
+ }
+
+ this.Write(b);
+ value >>= 7;
+ } while (value > 0);
+ }
+
+ #endregion WriteMethods
+
+ public void Write(MessageWriter msg, bool includeHeader)
+ {
+ int offset = 0;
+ if (!includeHeader)
+ {
+ switch (msg.SendOption)
+ {
+ case MessageType.Unreliable:
+ offset = 1;
+ break;
+
+ case MessageType.Reliable:
+ offset = 3;
+ break;
+ }
+ }
+
+ this.Write(msg.Buffer, offset, msg.Length - offset);
+ }
+
+ public unsafe static bool IsLittleEndian()
+ {
+ byte b;
+ unsafe
+ {
+ int i = 1;
+ byte* bp = (byte*)&i;
+ b = *bp;
+ }
+
+ return b == 1;
+ }
+
+ public void Dispose()
+ {
+ Recycle();
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/NetworkConnection.cs b/Impostor-dev/src/Impostor.Hazel/NetworkConnection.cs
new file mode 100644
index 0000000..282fe10
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/NetworkConnection.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Hazel
+{
+ public enum HazelInternalErrors
+ {
+ SocketExceptionSend,
+ SocketExceptionReceive,
+ ReceivedZeroBytes,
+ PingsWithoutResponse,
+ ReliablePacketWithoutResponse,
+ ConnectionDisconnected
+ }
+
+ /// <summary>
+ /// Abstract base class for a <see cref="Connection"/> to a remote end point via a network protocol like TCP or UDP.
+ /// </summary>
+ /// <threadsafety static="true" instance="true"/>
+ public abstract class NetworkConnection : Connection
+ {
+ /// <summary>
+ /// An event that gives us a chance to send well-formed disconnect messages to clients when an internal disconnect happens.
+ /// </summary>
+ public Func<HazelInternalErrors, MessageWriter> OnInternalDisconnect;
+
+ /// <summary>
+ /// The remote end point of this connection.
+ /// </summary>
+ /// <remarks>
+ /// This is the end point of the other device given as an <see cref="System.Net.EndPoint"/> rather than a generic
+ /// <see cref="ConnectionEndPoint"/> as the base <see cref="Connection"/> does.
+ /// </remarks>
+ public IPEndPoint RemoteEndPoint { get; protected set; }
+
+ public long GetIP4Address()
+ {
+ if (IPMode == IPMode.IPv4)
+ {
+ return ((IPEndPoint)this.RemoteEndPoint).Address.Address;
+ }
+ else
+ {
+ var bytes = ((IPEndPoint)this.RemoteEndPoint).Address.GetAddressBytes();
+ return BitConverter.ToInt64(bytes, bytes.Length - 8);
+ }
+ }
+
+ /// <summary>
+ /// Sends a disconnect message to the end point.
+ /// </summary>
+ protected abstract ValueTask<bool> SendDisconnect(MessageWriter writer);
+
+ /// <summary>
+ /// Called when the socket has been disconnected at the remote host.
+ /// </summary>
+ protected async ValueTask DisconnectRemote(string reason, IMessageReader reader)
+ {
+ if (await SendDisconnect(null))
+ {
+ try
+ {
+ await InvokeDisconnected(reason, reader);
+ }
+ catch { }
+ }
+
+ this.Dispose();
+ }
+
+ /// <summary>
+ /// Called when socket is disconnected internally
+ /// </summary>
+ internal async ValueTask DisconnectInternal(HazelInternalErrors error, string reason)
+ {
+ var handler = this.OnInternalDisconnect;
+ if (handler != null)
+ {
+ MessageWriter messageToRemote = handler(error);
+ if (messageToRemote != null)
+ {
+ try
+ {
+ await Disconnect(reason, messageToRemote);
+ }
+ finally
+ {
+ messageToRemote.Recycle();
+ }
+ }
+ else
+ {
+ await Disconnect(reason);
+ }
+ }
+ else
+ {
+ await Disconnect(reason);
+ }
+ }
+
+ /// <summary>
+ /// Called when the socket has been disconnected locally.
+ /// </summary>
+ public override async ValueTask Disconnect(string reason, MessageWriter writer = null)
+ {
+ if (await SendDisconnect(writer))
+ {
+ try
+ {
+ await InvokeDisconnected(reason, null);
+ }
+ catch { }
+ }
+
+ this.Dispose();
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/NetworkConnectionListener.cs b/Impostor-dev/src/Impostor.Hazel/NetworkConnectionListener.cs
new file mode 100644
index 0000000..e1d7ffa
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/NetworkConnectionListener.cs
@@ -0,0 +1,21 @@
+using System.Net;
+
+namespace Impostor.Hazel
+{
+ /// <summary>
+ /// Abstract base class for a <see cref="ConnectionListener"/> for network based connections.
+ /// </summary>
+ /// <threadsafety static="true" instance="true"/>
+ public abstract class NetworkConnectionListener : ConnectionListener
+ {
+ /// <summary>
+ /// The local end point the listener is listening for new clients on.
+ /// </summary>
+ public IPEndPoint EndPoint { get; protected set; }
+
+ /// <summary>
+ /// The <see cref="IPMode">IPMode</see> the listener is listening for new clients on.
+ /// </summary>
+ public IPMode IPMode { get; protected set; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/NewConnectionEventArgs.cs b/Impostor-dev/src/Impostor.Hazel/NewConnectionEventArgs.cs
new file mode 100644
index 0000000..be9e7a2
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/NewConnectionEventArgs.cs
@@ -0,0 +1,24 @@
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Hazel
+{
+ public struct NewConnectionEventArgs
+ {
+ /// <summary>
+ /// The data received from the client in the handshake.
+ /// This data is yours. Remember to recycle it.
+ /// </summary>
+ public readonly IMessageReader HandshakeData;
+
+ /// <summary>
+ /// The <see cref="Connection"/> to the new client.
+ /// </summary>
+ public readonly Connection Connection;
+
+ public NewConnectionEventArgs(IMessageReader handshakeData, Connection connection)
+ {
+ this.HandshakeData = handshakeData;
+ this.Connection = connection;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/ObjectPoolCustom.cs b/Impostor-dev/src/Impostor.Hazel/ObjectPoolCustom.cs
new file mode 100644
index 0000000..5c9ef9b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/ObjectPoolCustom.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+
+namespace Impostor.Hazel
+{
+ /// <summary>
+ /// A fairly simple object pool for items that will be created a lot.
+ /// </summary>
+ /// <typeparam name="T">The type that is pooled.</typeparam>
+ /// <threadsafety static="true" instance="true"/>
+ public sealed class ObjectPoolCustom<T> where T : IRecyclable
+ {
+ private int numberCreated;
+ public int NumberCreated { get { return numberCreated; } }
+
+ public int NumberInUse { get { return this.inuse.Count; } }
+ public int NumberNotInUse { get { return this.pool.Count; } }
+ public int Size { get { return this.NumberInUse + this.NumberNotInUse; } }
+
+#if HAZEL_BAG
+ private readonly ConcurrentBag<T> pool = new ConcurrentBag<T>();
+#else
+ private readonly List<T> pool = new List<T>();
+#endif
+
+ // Unavailable objects
+ private readonly ConcurrentDictionary<T, bool> inuse = new ConcurrentDictionary<T, bool>();
+
+ /// <summary>
+ /// The generator for creating new objects.
+ /// </summary>
+ /// <returns></returns>
+ private readonly Func<T> objectFactory;
+
+ /// <summary>
+ /// Internal constructor for our ObjectPool.
+ /// </summary>
+ internal ObjectPoolCustom(Func<T> objectFactory)
+ {
+ this.objectFactory = objectFactory;
+ }
+
+ /// <summary>
+ /// Returns a pooled object of type T, if none are available another is created.
+ /// </summary>
+ /// <returns>An instance of T.</returns>
+ internal T GetObject()
+ {
+#if HAZEL_BAG
+ if (!pool.TryTake(out T item))
+ {
+ Interlocked.Increment(ref numberCreated);
+ item = objectFactory.Invoke();
+ }
+#else
+ T item;
+ lock (this.pool)
+ {
+ if (this.pool.Count > 0)
+ {
+ var idx = this.pool.Count - 1;
+ item = this.pool[idx];
+ this.pool.RemoveAt(idx);
+ }
+ else
+ {
+ Interlocked.Increment(ref numberCreated);
+ item = objectFactory.Invoke();
+ }
+ }
+#endif
+
+ if (!inuse.TryAdd(item, true))
+ {
+ throw new Exception("Duplicate pull " + typeof(T).Name);
+ }
+
+ return item;
+ }
+
+ /// <summary>
+ /// Returns an object to the pool.
+ /// </summary>
+ /// <param name="item">The item to return.</param>
+ internal void PutObject(T item)
+ {
+ if (inuse.TryRemove(item, out bool b))
+ {
+#if HAZEL_BAG
+ pool.Add(item);
+#else
+ lock (this.pool)
+ {
+ pool.Add(item);
+ }
+#endif
+ }
+ else
+ {
+#if DEBUG
+ throw new Exception("Duplicate add " + typeof(T).Name);
+#endif
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/SendOptionInternal.cs b/Impostor-dev/src/Impostor.Hazel/Udp/SendOptionInternal.cs
new file mode 100644
index 0000000..c0c4e21
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Udp/SendOptionInternal.cs
@@ -0,0 +1,33 @@
+namespace Impostor.Hazel.Udp
+{
+ /// <summary>
+ /// Extra internal states for SendOption enumeration when using UDP.
+ /// </summary>
+ public enum UdpSendOption : byte
+ {
+ /// <summary>
+ /// Hello message for initiating communication.
+ /// </summary>
+ Hello = 8,
+
+ /// <summary>
+ /// A single byte of continued existence
+ /// </summary>
+ Ping = 12,
+
+ /// <summary>
+ /// Message for discontinuing communication.
+ /// </summary>
+ Disconnect = 9,
+
+ /// <summary>
+ /// Message acknowledging the receipt of a message.
+ /// </summary>
+ Acknowledgement = 10,
+
+ /// <summary>
+ /// Message that is part of a larger, fragmented message.
+ /// </summary>
+ Fragment = 11,
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcastListener.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcastListener.cs
new file mode 100644
index 0000000..ed7b68d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcastListener.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+namespace Impostor.Hazel.Udp
+{
+ public class BroadcastPacket
+ {
+ public string Data;
+ public DateTime ReceiveTime;
+ public IPEndPoint Sender;
+
+ public BroadcastPacket(string data, IPEndPoint sender)
+ {
+ this.Data = data;
+ this.Sender = sender;
+ this.ReceiveTime = DateTime.Now;
+ }
+
+ public string GetAddress()
+ {
+ return this.Sender.Address.ToString();
+ }
+ }
+
+ public class UdpBroadcastListener : IDisposable
+ {
+ private Socket socket;
+ private EndPoint endpoint;
+ private Action<string> logger;
+
+ private byte[] buffer = new byte[1024];
+
+ private List<BroadcastPacket> packets = new List<BroadcastPacket>();
+
+ public bool Running { get; private set; }
+
+ ///
+ public UdpBroadcastListener(int port, Action<string> logger = null)
+ {
+ this.logger = logger;
+ this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ this.socket.EnableBroadcast = true;
+ this.socket.MulticastLoopback = false;
+ this.endpoint = new IPEndPoint(IPAddress.Any, port);
+ this.socket.Bind(this.endpoint);
+ }
+
+ ///
+ public void StartListen()
+ {
+ if (this.Running) return;
+ this.Running = true;
+
+ try
+ {
+ EndPoint endpt = new IPEndPoint(IPAddress.Any, 0);
+ this.socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpt, this.HandleData, null);
+ }
+ catch (NullReferenceException) { }
+ catch (Exception e)
+ {
+ this.logger?.Invoke("BroadcastListener: " + e);
+ this.Dispose();
+ }
+ }
+
+ private void HandleData(IAsyncResult result)
+ {
+ this.Running = false;
+
+ int numBytes;
+ EndPoint endpt = new IPEndPoint(IPAddress.Any, 0);
+ try
+ {
+ numBytes = this.socket.EndReceiveFrom(result, ref endpt);
+ }
+ catch (NullReferenceException)
+ {
+ // Already disposed
+ return;
+ }
+ catch (Exception e)
+ {
+ this.logger?.Invoke("BroadcastListener: " + e);
+ this.Dispose();
+ return;
+ }
+
+ if (numBytes < 3
+ || buffer[0] != 4 || buffer[1] != 2)
+ {
+ this.StartListen();
+ return;
+ }
+
+ IPEndPoint ipEnd = (IPEndPoint)endpt;
+ string data = UTF8Encoding.UTF8.GetString(buffer, 2, numBytes - 2);
+ int dataHash = data.GetHashCode();
+
+ lock (packets)
+ {
+ bool found = false;
+ for (int i = 0; i < this.packets.Count; ++i)
+ {
+ var pkt = this.packets[i];
+ if (pkt == null || pkt.Data == null)
+ {
+ this.packets.RemoveAt(i);
+ i--;
+ continue;
+ }
+
+ if (pkt.Data.GetHashCode() == dataHash
+ && pkt.Sender.Equals(ipEnd))
+ {
+ this.packets[i].ReceiveTime = DateTime.Now;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ this.packets.Add(new BroadcastPacket(data, ipEnd));
+ }
+ }
+
+ this.StartListen();
+ }
+
+ ///
+ public BroadcastPacket[] GetPackets()
+ {
+ lock (this.packets)
+ {
+ var output = this.packets.ToArray();
+ this.packets.Clear();
+ return output;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (this.socket != null)
+ {
+ try { this.socket.Shutdown(SocketShutdown.Both); } catch { }
+ try { this.socket.Close(); } catch { }
+ try { this.socket.Dispose(); } catch { }
+ this.socket = null;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcaster.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcaster.cs
new file mode 100644
index 0000000..5fa1cca
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcaster.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+namespace Impostor.Hazel.Udp
+{
+ ///
+ public class UdpBroadcaster : IDisposable
+ {
+ private Socket socket;
+ private byte[] data;
+ private EndPoint endpoint;
+ private Action<string> logger;
+
+ ///
+ public UdpBroadcaster(int port, Action<string> logger = null)
+ {
+ this.logger = logger;
+ this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ this.socket.EnableBroadcast = true;
+ this.socket.MulticastLoopback = false;
+ this.endpoint = new IPEndPoint(IPAddress.Broadcast, port);
+ }
+
+ ///
+ public void SetData(string data)
+ {
+ int len = UTF8Encoding.UTF8.GetByteCount(data);
+ this.data = new byte[len + 2];
+ this.data[0] = 4;
+ this.data[1] = 2;
+
+ UTF8Encoding.UTF8.GetBytes(data, 0, data.Length, this.data, 2);
+ }
+
+ ///
+ public void Broadcast()
+ {
+ if (this.data == null)
+ {
+ return;
+ }
+
+ try
+ {
+ this.socket.BeginSendTo(data, 0, data.Length, SocketFlags.None, this.endpoint, this.FinishSendTo, null);
+ }
+ catch (Exception e)
+ {
+ this.logger?.Invoke("BroadcastListener: " + e);
+ }
+ }
+
+ private void FinishSendTo(IAsyncResult evt)
+ {
+ try
+ {
+ this.socket.EndSendTo(evt);
+ }
+ catch (Exception e)
+ {
+ this.logger?.Invoke("BroadcastListener: " + e);
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (this.socket != null)
+ {
+ try { this.socket.Shutdown(SocketShutdown.Both); } catch { }
+ try { this.socket.Close(); } catch { }
+ try { this.socket.Dispose(); } catch { }
+ this.socket = null;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpClientConnection.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpClientConnection.cs
new file mode 100644
index 0000000..5125ebe
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpClientConnection.cs
@@ -0,0 +1,225 @@
+using System;
+using System.Buffers;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+using Microsoft.Extensions.ObjectPool;
+using Serilog;
+
+namespace Impostor.Hazel.Udp
+{
+ /// <summary>
+ /// Represents a client's connection to a server that uses the UDP protocol.
+ /// </summary>
+ /// <inheritdoc/>
+ public sealed class UdpClientConnection : UdpConnection
+ {
+ private static readonly ILogger Logger = Log.ForContext<UdpClientConnection>();
+
+ /// <summary>
+ /// The socket we're connected via.
+ /// </summary>
+ private readonly UdpClient _socket;
+
+ private readonly Timer _reliablePacketTimer;
+ private readonly SemaphoreSlim _connectWaitLock;
+ private Task _listenTask;
+
+ /// <summary>
+ /// Creates a new UdpClientConnection.
+ /// </summary>
+ /// <param name="remoteEndPoint">A <see cref="NetworkEndPoint"/> to connect to.</param>
+ public UdpClientConnection(IPEndPoint remoteEndPoint, ObjectPool<MessageReader> readerPool, IPMode ipMode = IPMode.IPv4) : base(null, readerPool)
+ {
+ EndPoint = remoteEndPoint;
+ RemoteEndPoint = remoteEndPoint;
+ IPMode = ipMode;
+
+ _socket = new UdpClient
+ {
+ DontFragment = false
+ };
+
+ _reliablePacketTimer = new Timer(ManageReliablePacketsInternal, null, 100, Timeout.Infinite);
+ _connectWaitLock = new SemaphoreSlim(1, 1);
+ }
+
+ ~UdpClientConnection()
+ {
+ Dispose(false);
+ }
+
+ private async void ManageReliablePacketsInternal(object state)
+ {
+ await ManageReliablePackets();
+
+ try
+ {
+ _reliablePacketTimer.Change(100, Timeout.Infinite);
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ /// <inheritdoc />
+ protected override ValueTask WriteBytesToConnection(byte[] bytes, int length)
+ {
+ return WriteBytesToConnectionReal(bytes, length);
+ }
+
+ private async ValueTask WriteBytesToConnectionReal(byte[] bytes, int length)
+ {
+ try
+ {
+ await _socket.SendAsync(bytes, length);
+ }
+ catch (NullReferenceException) { }
+ catch (ObjectDisposedException)
+ {
+ // Already disposed and disconnected...
+ }
+ catch (SocketException ex)
+ {
+ await DisconnectInternal(HazelInternalErrors.SocketExceptionSend, "Could not send data as a SocketException occurred: " + ex.Message);
+ }
+ }
+
+ /// <inheritdoc />
+ public override async ValueTask ConnectAsync(byte[] bytes = null)
+ {
+ State = ConnectionState.Connecting;
+
+ try
+ {
+ _socket.Connect(RemoteEndPoint);
+ }
+ catch (SocketException e)
+ {
+ State = ConnectionState.NotConnected;
+ throw new HazelException("A SocketException occurred while binding to the port.", e);
+ }
+
+ try
+ {
+ _listenTask = Task.Factory.StartNew(ListenAsync, TaskCreationOptions.LongRunning);
+ }
+ catch (ObjectDisposedException)
+ {
+ // If the socket's been disposed then we can just end there but make sure we're in NotConnected state.
+ // If we end up here I'm really lost...
+ State = ConnectionState.NotConnected;
+ return;
+ }
+ catch (SocketException e)
+ {
+ Dispose();
+ throw new HazelException("A SocketException occurred while initiating a receive operation.", e);
+ }
+
+ // Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present)
+ // When acknowledged set the state to connected
+ await SendHello(bytes, () =>
+ {
+ State = ConnectionState.Connected;
+ InitializeKeepAliveTimer();
+ });
+
+ await _connectWaitLock.WaitAsync(TimeSpan.FromSeconds(10));
+ }
+
+ private async Task ListenAsync()
+ {
+ // Start packet handler.
+ await StartAsync();
+
+ // Listen.
+ while (State != ConnectionState.NotConnected)
+ {
+ UdpReceiveResult data;
+
+ try
+ {
+ data = await _socket.ReceiveAsync();
+ }
+ catch (SocketException e)
+ {
+ await DisconnectInternal(HazelInternalErrors.SocketExceptionReceive, "Socket exception while reading data: " + e.Message);
+ return;
+ }
+ catch (Exception)
+ {
+ return;
+ }
+
+ if (data.Buffer.Length == 0)
+ {
+ await DisconnectInternal(HazelInternalErrors.ReceivedZeroBytes, "Received 0 bytes");
+ return;
+ }
+
+ // Write to client.
+ await Pipeline.Writer.WriteAsync(data.Buffer);
+ }
+ }
+
+ protected override void SetState(ConnectionState state)
+ {
+ if (state == ConnectionState.Connected)
+ {
+ _connectWaitLock.Release();
+ }
+ }
+
+ /// <summary>
+ /// Sends a disconnect message to the end point.
+ /// You may include optional disconnect data. The SendOption must be unreliable.
+ /// </summary>
+ protected override async ValueTask<bool> SendDisconnect(MessageWriter data = null)
+ {
+ lock (this)
+ {
+ if (_state == ConnectionState.NotConnected) return false;
+ _state = ConnectionState.NotConnected;
+ }
+
+ var bytes = EmptyDisconnectBytes;
+ if (data != null && data.Length > 0)
+ {
+ if (data.SendOption != MessageType.Unreliable)
+ {
+ throw new ArgumentException("Disconnect messages can only be unreliable.");
+ }
+
+ bytes = data.ToByteArray(true);
+ bytes[0] = (byte)UdpSendOption.Disconnect;
+ }
+
+ try
+ {
+ await _socket.SendAsync(bytes, bytes.Length, RemoteEndPoint);
+ }
+ catch { }
+
+ return true;
+ }
+
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ State = ConnectionState.NotConnected;
+
+ try { _socket.Close(); } catch { }
+ try { _socket.Dispose(); } catch { }
+
+ _reliablePacketTimer.Dispose();
+ _connectWaitLock.Dispose();
+
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.KeepAlive.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.KeepAlive.cs
new file mode 100644
index 0000000..a73291b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.KeepAlive.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Impostor.Hazel.Udp
+{
+ partial class UdpConnection
+ {
+
+ /// <summary>
+ /// Class to hold packet data
+ /// </summary>
+ public class PingPacket : IRecyclable
+ {
+ private static readonly ObjectPoolCustom<PingPacket> PacketPool = new ObjectPoolCustom<PingPacket>(() => new PingPacket());
+
+ public readonly Stopwatch Stopwatch = new Stopwatch();
+
+ internal static PingPacket GetObject()
+ {
+ return PacketPool.GetObject();
+ }
+
+ public void Recycle()
+ {
+ Stopwatch.Stop();
+ PacketPool.PutObject(this);
+ }
+ }
+
+ internal ConcurrentDictionary<ushort, PingPacket> activePingPackets = new ConcurrentDictionary<ushort, PingPacket>();
+
+ /// <summary>
+ /// The interval from data being received or transmitted to a keepalive packet being sent in milliseconds.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Keepalive packets serve to close connections when an endpoint abruptly disconnects and to ensure than any
+ /// NAT devices do not close their translation for our argument. By ensuring there is regular contact the
+ /// connection can detect and prevent these issues.
+ /// </para>
+ /// <para>
+ /// The default value is 10 seconds, set to System.Threading.Timeout.Infinite to disable keepalive packets.
+ /// </para>
+ /// </remarks>
+ public int KeepAliveInterval
+ {
+ get
+ {
+ return keepAliveInterval;
+ }
+
+ set
+ {
+ keepAliveInterval = value;
+ ResetKeepAliveTimer();
+ }
+ }
+ private int keepAliveInterval = 1500;
+
+ public int MissingPingsUntilDisconnect { get; set; } = 6;
+ private volatile int pingsSinceAck = 0;
+
+ /// <summary>
+ /// The timer creating keepalive pulses.
+ /// </summary>
+ private Timer keepAliveTimer;
+
+ /// <summary>
+ /// Starts the keepalive timer.
+ /// </summary>
+ protected void InitializeKeepAliveTimer()
+ {
+ keepAliveTimer = new Timer(
+ HandleKeepAlive,
+ null,
+ keepAliveInterval,
+ keepAliveInterval
+ );
+ }
+
+ private async void HandleKeepAlive(object state)
+ {
+ if (this.State != ConnectionState.Connected) return;
+
+ if (this.pingsSinceAck >= this.MissingPingsUntilDisconnect)
+ {
+ this.DisposeKeepAliveTimer();
+ await this.DisconnectInternal(HazelInternalErrors.PingsWithoutResponse, $"Sent {this.pingsSinceAck} pings that remote has not responded to.");
+ return;
+ }
+
+ try
+ {
+ Interlocked.Increment(ref pingsSinceAck);
+ await SendPing();
+ }
+ catch
+ {
+ }
+ }
+
+ // Pings are special, quasi-reliable packets.
+ // We send them to trigger responses that validate our connection is alive
+ // An unacked ping should never be the sole cause of a disconnect.
+ // Rather, the responses will reset our pingsSinceAck, enough unacked
+ // pings should cause a disconnect.
+ private async ValueTask SendPing()
+ {
+ ushort id = (ushort)Interlocked.Increment(ref lastIDAllocated);
+
+ byte[] bytes = new byte[3];
+ bytes[0] = (byte)UdpSendOption.Ping;
+ bytes[1] = (byte)(id >> 8);
+ bytes[2] = (byte)id;
+
+ PingPacket pkt;
+ if (!this.activePingPackets.TryGetValue(id, out pkt))
+ {
+ pkt = PingPacket.GetObject();
+ if (!this.activePingPackets.TryAdd(id, pkt))
+ {
+ throw new Exception("This shouldn't be possible");
+ }
+ }
+
+ pkt.Stopwatch.Restart();
+
+ await WriteBytesToConnection(bytes, bytes.Length);
+
+ Statistics.LogReliableSend(0, bytes.Length);
+ }
+
+ /// <summary>
+ /// Resets the keepalive timer to zero.
+ /// </summary>
+ private void ResetKeepAliveTimer()
+ {
+ try
+ {
+ keepAliveTimer.Change(keepAliveInterval, keepAliveInterval);
+ }
+ catch { }
+ }
+
+ /// <summary>
+ /// Disposes of the keep alive timer.
+ /// </summary>
+ private void DisposeKeepAliveTimer()
+ {
+ if (this.keepAliveTimer != null)
+ {
+ this.keepAliveTimer.Dispose();
+ }
+
+ foreach (var kvp in activePingPackets)
+ {
+ if (this.activePingPackets.TryRemove(kvp.Key, out var pkt))
+ {
+ pkt.Recycle();
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.Reliable.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.Reliable.cs
new file mode 100644
index 0000000..a7a4309
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.Reliable.cs
@@ -0,0 +1,491 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Hazel.Udp
+{
+ partial class UdpConnection
+ {
+ /// <summary>
+ /// The starting timeout, in miliseconds, at which data will be resent.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// For reliable delivery data is resent at specified intervals unless an acknowledgement is received from the
+ /// receiving device. The ResendTimeout specifies the interval between the packets being resent, each time a packet
+ /// is resent the interval is increased for that packet until the duration exceeds the <see cref="DisconnectTimeout"/> value.
+ /// </para>
+ /// <para>
+ /// Setting this to its default of 0 will mean the timeout is 2 times the value of the average ping, usually
+ /// resulting in a more dynamic resend that responds to endpoints on slower or faster connections.
+ /// </para>
+ /// </remarks>
+ public volatile int ResendTimeout = 0;
+
+ /// <summary>
+ /// Max number of times to resend. 0 == no limit
+ /// </summary>
+ public volatile int ResendLimit = 0;
+
+ /// <summary>
+ /// A compounding multiplier to back off resend timeout.
+ /// Applied to ping before first timeout when ResendTimeout == 0.
+ /// </summary>
+ public volatile float ResendPingMultiplier = 2;
+
+ /// <summary>
+ /// Holds the last ID allocated.
+ /// </summary>
+ private int lastIDAllocated = 0;
+
+ /// <summary>
+ /// The packets of data that have been transmitted reliably and not acknowledged.
+ /// </summary>
+ internal ConcurrentDictionary<ushort, Packet> reliableDataPacketsSent = new ConcurrentDictionary<ushort, Packet>();
+
+ /// <summary>
+ /// Packet ids that have not been received, but are expected.
+ /// </summary>
+ private HashSet<ushort> reliableDataPacketsMissing = new HashSet<ushort>();
+
+ /// <summary>
+ /// The packet id that was received last.
+ /// </summary>
+ private volatile ushort reliableReceiveLast = ushort.MaxValue;
+
+ private object PingLock = new object();
+
+ /// <summary>
+ /// Returns the average ping to this endpoint.
+ /// </summary>
+ /// <remarks>
+ /// This returns the average ping for a one-way trip as calculated from the reliable packets that have been sent
+ /// and acknowledged by the endpoint.
+ /// </remarks>
+ public float AveragePingMs = 500;
+
+ /// <summary>
+ /// The maximum times a message should be resent before marking the endpoint as disconnected.
+ /// </summary>
+ /// <remarks>
+ /// Reliable packets will be resent at an interval defined in <see cref="ResendTimeout"/> for the number of times
+ /// specified here. Once a packet has been retransmitted this number of times and has not been acknowledged the
+ /// connection will be marked as disconnected and the <see cref="Connection.Disconnected">Disconnected</see> event
+ /// will be invoked.
+ /// </remarks>
+ public volatile int DisconnectTimeout = 5000;
+
+ /// <summary>
+ /// Class to hold packet data
+ /// </summary>
+ public class Packet : IRecyclable
+ {
+ /// <summary>
+ /// Object pool for this event.
+ /// </summary>
+ public static readonly ObjectPoolCustom<Packet> PacketPool = new ObjectPoolCustom<Packet>(() => new Packet());
+
+ /// <summary>
+ /// Returns an instance of this object from the pool.
+ /// </summary>
+ /// <returns></returns>
+ internal static Packet GetObject()
+ {
+ return PacketPool.GetObject();
+ }
+
+ public ushort Id;
+ private byte[] Data;
+ private UdpConnection Connection;
+ private int Length;
+
+ public int NextTimeout;
+ public volatile bool Acknowledged;
+
+ public Action AckCallback;
+
+ public int Retransmissions;
+ public Stopwatch Stopwatch = new Stopwatch();
+
+ Packet()
+ {
+ }
+
+ internal void Set(ushort id, UdpConnection connection, byte[] data, int length, int timeout, Action ackCallback)
+ {
+ this.Id = id;
+ this.Data = data;
+ this.Connection = connection;
+ this.Length = length;
+
+ this.Acknowledged = false;
+ this.NextTimeout = timeout;
+ this.AckCallback = ackCallback;
+ this.Retransmissions = 0;
+
+ this.Stopwatch.Restart();
+ }
+
+ // Packets resent
+ public async ValueTask<int> Resend()
+ {
+ var connection = this.Connection;
+ if (!this.Acknowledged && connection != null)
+ {
+ long lifetime = this.Stopwatch.ElapsedMilliseconds;
+ if (lifetime >= connection.DisconnectTimeout)
+ {
+ if (connection.reliableDataPacketsSent.TryRemove(this.Id, out Packet self))
+ {
+ await connection.DisconnectInternal(HazelInternalErrors.ReliablePacketWithoutResponse, $"Reliable packet {self.Id} (size={this.Length}) was not ack'd after {lifetime}ms ({self.Retransmissions} resends)");
+
+ self.Recycle();
+ }
+
+ return 0;
+ }
+
+ if (lifetime >= this.NextTimeout)
+ {
+ ++this.Retransmissions;
+ if (connection.ResendLimit != 0
+ && this.Retransmissions > connection.ResendLimit)
+ {
+ if (connection.reliableDataPacketsSent.TryRemove(this.Id, out Packet self))
+ {
+ await connection.DisconnectInternal(HazelInternalErrors.ReliablePacketWithoutResponse, $"Reliable packet {self.Id} (size={this.Length}) was not ack'd after {self.Retransmissions} resends ({lifetime}ms)");
+
+ self.Recycle();
+ }
+
+ return 0;
+ }
+
+ this.NextTimeout += (int)Math.Min(this.NextTimeout * connection.ResendPingMultiplier, 1000);
+ try
+ {
+ await connection.WriteBytesToConnection(this.Data, this.Length);
+ connection.Statistics.LogMessageResent();
+ return 1;
+ }
+ catch (InvalidOperationException)
+ {
+ await connection.DisconnectInternal(HazelInternalErrors.ConnectionDisconnected, "Could not resend data as connection is no longer connected");
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /// <summary>
+ /// Returns this object back to the object pool from whence it came.
+ /// </summary>
+ public void Recycle()
+ {
+ this.Acknowledged = true;
+ this.Connection = null;
+
+ PacketPool.PutObject(this);
+ }
+ }
+
+ internal async ValueTask<int> ManageReliablePackets()
+ {
+ int output = 0;
+ if (this.reliableDataPacketsSent.Count > 0)
+ {
+ foreach (var kvp in this.reliableDataPacketsSent)
+ {
+ Packet pkt = kvp.Value;
+
+ try
+ {
+ output += await pkt.Resend();
+ }
+ catch { }
+ }
+ }
+
+ return output;
+ }
+
+ /// <summary>
+ /// Adds a 2 byte ID to the packet at offset and stores the packet reference for retransmission.
+ /// </summary>
+ /// <param name="buffer">The buffer to attach to.</param>
+ /// <param name="offset">The offset to attach at.</param>
+ /// <param name="ackCallback">The callback to make once the packet has been acknowledged.</param>
+ protected void AttachReliableID(byte[] buffer, int offset, int sendLength, Action ackCallback = null)
+ {
+ ushort id = (ushort)Interlocked.Increment(ref lastIDAllocated);
+
+ buffer[offset] = (byte)(id >> 8);
+ buffer[offset + 1] = (byte)id;
+
+ Packet packet = Packet.GetObject();
+ packet.Set(
+ id,
+ this,
+ buffer,
+ sendLength,
+ ResendTimeout > 0 ? ResendTimeout : (int)Math.Min(AveragePingMs * this.ResendPingMultiplier, 300),
+ ackCallback);
+
+ if (!reliableDataPacketsSent.TryAdd(id, packet))
+ {
+ throw new Exception("That shouldn't be possible");
+ }
+ }
+
+ public static int ClampToInt(float value, int min, int max)
+ {
+ if (value < min) return min;
+ if (value > max) return max;
+ return (int)value;
+ }
+
+ /// <summary>
+ /// Sends the bytes reliably and stores the send.
+ /// </summary>
+ /// <param name="sendOption"></param>
+ /// <param name="data">The byte array to write to.</param>
+ /// <param name="ackCallback">The callback to make once the packet has been acknowledged.</param>
+ private async ValueTask ReliableSend(byte sendOption, byte[] data, Action ackCallback = null)
+ {
+ //Inform keepalive not to send for a while
+ ResetKeepAliveTimer();
+
+ byte[] bytes = new byte[data.Length + 3];
+
+ //Add message type
+ bytes[0] = sendOption;
+
+ //Add reliable ID
+ AttachReliableID(bytes, 1, bytes.Length, ackCallback);
+
+ //Copy data into new array
+ Buffer.BlockCopy(data, 0, bytes, bytes.Length - data.Length, data.Length);
+
+ //Write to connection
+ await WriteBytesToConnection(bytes, bytes.Length);
+
+ Statistics.LogReliableSend(data.Length, bytes.Length);
+ }
+
+ /// <summary>
+ /// Handles a reliable message being received and invokes the data event.
+ /// </summary>
+ /// <param name="message">The buffer received.</param>
+ private async ValueTask ReliableMessageReceive(MessageReader message)
+ {
+ if (await ProcessReliableReceive(message.Buffer, 1))
+ {
+ message.Offset += 3;
+ message.Length -= 3;
+ message.Position = 0;
+
+ await InvokeDataReceived(message, MessageType.Reliable);
+ }
+
+ Statistics.LogReliableReceive(message.Length - 3, message.Length);
+ }
+
+ /// <summary>
+ /// Handles receives from reliable packets.
+ /// </summary>
+ /// <param name="bytes">The buffer containing the data.</param>
+ /// <param name="offset">The offset of the reliable header.</param>
+ /// <returns>Whether the packet was a new packet or not.</returns>
+ private async ValueTask<bool> ProcessReliableReceive(ReadOnlyMemory<byte> bytes, int offset)
+ {
+ var b1 = bytes.Span[offset];
+ var b2 = bytes.Span[offset + 1];
+
+ //Get the ID form the packet
+ var id = (ushort)((b1 << 8) + b2);
+
+ //Send an acknowledgement
+ await SendAck(id);
+
+ /*
+ * It gets a little complicated here (note the fact I'm actually using a multiline comment for once...)
+ *
+ * In a simple world if our data is greater than the last reliable packet received (reliableReceiveLast)
+ * then it is guaranteed to be a new packet, if it's not we can see if we are missing that packet (lookup
+ * in reliableDataPacketsMissing).
+ *
+ * --------rrl############# (1)
+ *
+ * (where --- are packets received already and #### are packets that will be counted as new)
+ *
+ * Unfortunately if id becomes greater than 65535 it will loop back to zero so we will add a pointer that
+ * specifies any packets with an id behind it are also new (overwritePointer).
+ *
+ * ####op----------rrl##### (2)
+ *
+ * ------rll#########op---- (3)
+ *
+ * Anything behind than the reliableReceiveLast pointer (but greater than the overwritePointer is either a
+ * missing packet or something we've already received so when we change the pointers we need to make sure
+ * we keep note of what hasn't been received yet (reliableDataPacketsMissing).
+ *
+ * So...
+ */
+
+ lock (reliableDataPacketsMissing)
+ {
+ //Calculate overwritePointer
+ ushort overwritePointer = (ushort)(reliableReceiveLast - 32768);
+
+ //Calculate if it is a new packet by examining if it is within the range
+ bool isNew;
+ if (overwritePointer < reliableReceiveLast)
+ isNew = id > reliableReceiveLast || id <= overwritePointer; //Figure (2)
+ else
+ isNew = id > reliableReceiveLast && id <= overwritePointer; //Figure (3)
+
+ //If it's new or we've not received anything yet
+ if (isNew)
+ {
+ // Mark items between the most recent receive and the id received as missing
+ if (id > reliableReceiveLast)
+ {
+ for (ushort i = (ushort)(reliableReceiveLast + 1); i < id; i++)
+ {
+ reliableDataPacketsMissing.Add(i);
+ }
+ }
+ else
+ {
+ int cnt = (ushort.MaxValue - reliableReceiveLast) + id;
+ for (ushort i = 1; i < cnt; ++i)
+ {
+ reliableDataPacketsMissing.Add((ushort)(i + reliableReceiveLast));
+ }
+ }
+
+ //Update the most recently received
+ reliableReceiveLast = id;
+ }
+
+ //Else it could be a missing packet
+ else
+ {
+ //See if we're missing it, else this packet is a duplicate as so we return false
+ if (!reliableDataPacketsMissing.Remove(id))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Handles acknowledgement packets to us.
+ /// </summary>
+ /// <param name="bytes">The buffer containing the data.</param>
+ private void AcknowledgementMessageReceive(ReadOnlySpan<byte> bytes)
+ {
+ this.pingsSinceAck = 0;
+
+ ushort id = (ushort)((bytes[1] << 8) + bytes[2]);
+ AcknowledgeMessageId(id);
+
+ if (bytes.Length == 4)
+ {
+ byte recentPackets = bytes[3];
+ for (int i = 1; i <= 8; ++i)
+ {
+ if ((recentPackets & 1) != 0)
+ {
+ AcknowledgeMessageId((ushort)(id - i));
+ }
+
+ recentPackets >>= 1;
+ }
+ }
+
+ Statistics.LogReliableReceive(0, bytes.Length);
+ }
+
+ private void AcknowledgeMessageId(ushort id)
+ {
+ // Dispose of timer and remove from dictionary
+ if (reliableDataPacketsSent.TryRemove(id, out Packet packet))
+ {
+ float rt = packet.Stopwatch.ElapsedMilliseconds;
+
+ packet.AckCallback?.Invoke();
+ packet.Recycle();
+
+ lock (PingLock)
+ {
+ this.AveragePingMs = Math.Max(50, this.AveragePingMs * .7f + rt * .3f);
+ }
+ }
+ else if (this.activePingPackets.TryRemove(id, out PingPacket pingPkt))
+ {
+ float rt = pingPkt.Stopwatch.ElapsedMilliseconds;
+
+ pingPkt.Recycle();
+
+ lock (PingLock)
+ {
+ this.AveragePingMs = Math.Max(50, this.AveragePingMs * .7f + rt * .3f);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Sends an acknowledgement for a packet given its identification bytes.
+ /// </summary>
+ /// <param name="byte1">The first identification byte.</param>
+ /// <param name="byte2">The second identification byte.</param>
+ private async ValueTask SendAck(ushort id)
+ {
+ byte recentPackets = 0;
+ lock (this.reliableDataPacketsMissing)
+ {
+ for (int i = 1; i <= 8; ++i)
+ {
+ if (!this.reliableDataPacketsMissing.Contains((ushort)(id - i)))
+ {
+ recentPackets |= (byte)(1 << (i - 1));
+ }
+ }
+ }
+
+ byte[] bytes = new byte[]
+ {
+ (byte)UdpSendOption.Acknowledgement,
+ (byte)(id >> 8),
+ (byte)(id >> 0),
+ recentPackets
+ };
+
+ try
+ {
+ await WriteBytesToConnection(bytes, bytes.Length);
+ }
+ catch (InvalidOperationException) { }
+ }
+
+ private void DisposeReliablePackets()
+ {
+ foreach (var kvp in reliableDataPacketsSent)
+ {
+ if (this.reliableDataPacketsSent.TryRemove(kvp.Key, out var pkt))
+ {
+ pkt.Recycle();
+ }
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.cs
new file mode 100644
index 0000000..5288d3c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.cs
@@ -0,0 +1,312 @@
+using System;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+using Microsoft.Extensions.ObjectPool;
+using Serilog;
+
+namespace Impostor.Hazel.Udp
+{
+ /// <summary>
+ /// Represents a connection that uses the UDP protocol.
+ /// </summary>
+ /// <inheritdoc />
+ public abstract partial class UdpConnection : NetworkConnection
+ {
+ protected static readonly byte[] EmptyDisconnectBytes = { (byte)UdpSendOption.Disconnect };
+
+ private static readonly ILogger Logger = Log.ForContext<UdpConnection>();
+ private readonly ConnectionListener _listener;
+ private readonly ObjectPool<MessageReader> _readerPool;
+ private readonly CancellationTokenSource _stoppingCts;
+
+ private bool _isDisposing;
+ private bool _isFirst = true;
+ private Task _executingTask;
+
+ protected UdpConnection(ConnectionListener listener, ObjectPool<MessageReader> readerPool)
+ {
+ _listener = listener;
+ _readerPool = readerPool;
+ _stoppingCts = new CancellationTokenSource();
+
+ Pipeline = Channel.CreateUnbounded<byte[]>(new UnboundedChannelOptions
+ {
+ SingleReader = true,
+ SingleWriter = true
+ });
+ }
+
+ internal Channel<byte[]> Pipeline { get; }
+
+ public Task StartAsync()
+ {
+ // Store the task we're executing
+ _executingTask = Task.Factory.StartNew(ReadAsync, TaskCreationOptions.LongRunning);
+
+ // If the task is completed then return it, this will bubble cancellation and failure to the caller
+ if (_executingTask.IsCompleted)
+ {
+ return _executingTask;
+ }
+
+ // Otherwise it's running
+ return Task.CompletedTask;
+ }
+
+ public void Stop()
+ {
+ // Stop called without start
+ if (_executingTask == null)
+ {
+ return;
+ }
+
+ // Signal cancellation to methods.
+ _stoppingCts.Cancel();
+
+ try
+ {
+ // Cancel reader.
+ Pipeline.Writer.Complete();
+ }
+ catch (ChannelClosedException)
+ {
+ // Already done.
+ }
+
+ // Remove references.
+ if (!_isDisposing)
+ {
+ Dispose(true);
+ }
+ }
+
+ private async Task ReadAsync()
+ {
+ var reader = new MessageReader(_readerPool);
+
+ while (!_stoppingCts.IsCancellationRequested)
+ {
+ var result = await Pipeline.Reader.ReadAsync(_stoppingCts.Token);
+
+ try
+ {
+ reader.Update(result);
+
+ await HandleReceive(reader);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Exception during ReadAsync");
+ Dispose(true);
+ break;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Writes the given bytes to the connection.
+ /// </summary>
+ /// <param name="bytes">The bytes to write.</param>
+ /// <param name="length"></param>
+ protected abstract ValueTask WriteBytesToConnection(byte[] bytes, int length);
+
+ /// <inheritdoc/>
+ public override async ValueTask SendAsync(IMessageWriter msg)
+ {
+ if (this._state != ConnectionState.Connected)
+ throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?");
+
+ byte[] buffer = new byte[msg.Length];
+ Buffer.BlockCopy(msg.Buffer, 0, buffer, 0, msg.Length);
+
+ switch (msg.SendOption)
+ {
+ case MessageType.Reliable:
+ ResetKeepAliveTimer();
+
+ AttachReliableID(buffer, 1, buffer.Length);
+ await WriteBytesToConnection(buffer, buffer.Length);
+ Statistics.LogReliableSend(buffer.Length - 3, buffer.Length);
+ break;
+
+ default:
+ await WriteBytesToConnection(buffer, buffer.Length);
+ Statistics.LogUnreliableSend(buffer.Length - 1, buffer.Length);
+ break;
+ }
+ }
+
+ /// <inheritdoc/>
+ /// <remarks>
+ /// <include file="DocInclude/common.xml" path="docs/item[@name='Connection_SendBytes_General']/*" />
+ /// <para>
+ /// Udp connections can currently send messages using <see cref="SendOption.None"/> and
+ /// <see cref="SendOption.Reliable"/>. Fragmented messages are not currently supported and will default to
+ /// <see cref="SendOption.None"/> until implemented.
+ /// </para>
+ /// </remarks>
+ public override async ValueTask SendBytes(byte[] bytes, MessageType sendOption = MessageType.Unreliable)
+ {
+ //Add header information and send
+ await HandleSend(bytes, (byte)sendOption);
+ }
+
+ /// <summary>
+ /// Handles the reliable/fragmented sending from this connection.
+ /// </summary>
+ /// <param name="data">The data being sent.</param>
+ /// <param name="sendOption">The <see cref="SendOption"/> specified as its byte value.</param>
+ /// <param name="ackCallback">The callback to invoke when this packet is acknowledged.</param>
+ /// <returns>The bytes that should actually be sent.</returns>
+ protected async ValueTask HandleSend(byte[] data, byte sendOption, Action ackCallback = null)
+ {
+ switch (sendOption)
+ {
+ case (byte)UdpSendOption.Ping:
+ case (byte)MessageType.Reliable:
+ case (byte)UdpSendOption.Hello:
+ await ReliableSend(sendOption, data, ackCallback);
+ break;
+
+ //Treat all else as unreliable
+ default:
+ await UnreliableSend(sendOption, data);
+ break;
+ }
+ }
+
+ /// <summary>
+ /// Handles the receiving of data.
+ /// </summary>
+ /// <param name="message">The buffer containing the bytes received.</param>
+ protected async ValueTask HandleReceive(MessageReader message)
+ {
+ // Check if the first message received is the hello packet.
+ if (_isFirst)
+ {
+ _isFirst = false;
+
+ // Slice 4 bytes to get handshake data.
+ if (_listener != null)
+ {
+ using (var handshake = message.Copy(4))
+ {
+ await _listener.InvokeNewConnection(handshake, this);
+ }
+ }
+ }
+
+ switch (message.Buffer[0])
+ {
+ //Handle reliable receives
+ case (byte)MessageType.Reliable:
+ await ReliableMessageReceive(message);
+ break;
+
+ //Handle acknowledgments
+ case (byte)UdpSendOption.Acknowledgement:
+ AcknowledgementMessageReceive(message.Buffer);
+ break;
+
+ //We need to acknowledge hello and ping messages but dont want to invoke any events!
+ case (byte)UdpSendOption.Ping:
+ await ProcessReliableReceive(message.Buffer, 1);
+ Statistics.LogHelloReceive(message.Length);
+ break;
+ case (byte)UdpSendOption.Hello:
+ await ProcessReliableReceive(message.Buffer, 1);
+ Statistics.LogHelloReceive(message.Length);
+ break;
+
+ case (byte)UdpSendOption.Disconnect:
+ using (var reader = message.Copy(1))
+ {
+ await DisconnectRemote("The remote sent a disconnect request", reader);
+ }
+ break;
+
+ //Treat everything else as unreliable
+ default:
+ using (var reader = message.Copy(1))
+ {
+ await InvokeDataReceived(reader, MessageType.Unreliable);
+ }
+ Statistics.LogUnreliableReceive(message.Length - 1, message.Length);
+ break;
+ }
+ }
+
+ /// <summary>
+ /// Sends bytes using the unreliable UDP protocol.
+ /// </summary>
+ /// <param name="sendOption">The SendOption to attach.</param>
+ /// <param name="data">The data.</param>
+ ValueTask UnreliableSend(byte sendOption, byte[] data)
+ {
+ return UnreliableSend(sendOption, data, 0, data.Length);
+ }
+
+ /// <summary>
+ /// Sends bytes using the unreliable UDP protocol.
+ /// </summary>
+ /// <param name="data">The data.</param>
+ /// <param name="sendOption">The SendOption to attach.</param>
+ /// <param name="offset"></param>
+ /// <param name="length"></param>
+ async ValueTask UnreliableSend(byte sendOption, byte[] data, int offset, int length)
+ {
+ byte[] bytes = new byte[length + 1];
+
+ //Add message type
+ bytes[0] = sendOption;
+
+ //Copy data into new array
+ Buffer.BlockCopy(data, offset, bytes, bytes.Length - length, length);
+
+ //Write to connection
+ await WriteBytesToConnection(bytes, bytes.Length);
+
+ Statistics.LogUnreliableSend(length, bytes.Length);
+ }
+
+ /// <summary>
+ /// Sends a hello packet to the remote endpoint.
+ /// </summary>
+ /// <param name="bytes"></param>
+ /// <param name="acknowledgeCallback">The callback to invoke when the hello packet is acknowledged.</param>
+ protected ValueTask SendHello(byte[] bytes, Action acknowledgeCallback)
+ {
+ //First byte of handshake is version indicator so add data after
+ byte[] actualBytes;
+ if (bytes == null)
+ {
+ actualBytes = new byte[1];
+ }
+ else
+ {
+ actualBytes = new byte[bytes.Length + 1];
+ Buffer.BlockCopy(bytes, 0, actualBytes, 1, bytes.Length);
+ }
+
+ return HandleSend(actualBytes, (byte)UdpSendOption.Hello, acknowledgeCallback);
+ }
+
+ /// <inheritdoc/>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _isDisposing = true;
+
+ Stop();
+ DisposeKeepAliveTimer();
+ DisposeReliablePackets();
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionListener.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionListener.cs
new file mode 100644
index 0000000..573a00c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionListener.cs
@@ -0,0 +1,281 @@
+using System;
+using System.Buffers;
+using System.Collections.Concurrent;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using Microsoft.Extensions.ObjectPool;
+using Serilog;
+
+namespace Impostor.Hazel.Udp
+{
+ /// <summary>
+ /// Listens for new UDP connections and creates UdpConnections for them.
+ /// </summary>
+ /// <inheritdoc />
+ public class UdpConnectionListener : NetworkConnectionListener
+ {
+ private static readonly ILogger Logger = Log.ForContext<UdpConnectionListener>();
+
+ /// <summary>
+ /// A callback for early connection rejection.
+ /// * Return false to reject connection.
+ /// * A null response is ok, we just won't send anything.
+ /// </summary>
+ public AcceptConnectionCheck AcceptConnection;
+ public delegate bool AcceptConnectionCheck(IPEndPoint endPoint, byte[] input, out byte[] response);
+
+ private readonly UdpClient _socket;
+ private readonly ObjectPool<MessageReader> _readerPool;
+ private readonly MemoryPool<byte> _pool;
+ private readonly Timer _reliablePacketTimer;
+ private readonly ConcurrentDictionary<EndPoint, UdpServerConnection> _allConnections;
+ private readonly CancellationTokenSource _stoppingCts;
+ private readonly UdpConnectionRateLimit _connectionRateLimit;
+ private Task _executingTask;
+
+ /// <summary>
+ /// Creates a new UdpConnectionListener for the given <see cref="IPAddress"/>, port and <see cref="IPMode"/>.
+ /// </summary>
+ /// <param name="endPoint">The endpoint to listen on.</param>
+ /// <param name="ipMode"></param>
+ public UdpConnectionListener(IPEndPoint endPoint, ObjectPool<MessageReader> readerPool, IPMode ipMode = IPMode.IPv4)
+ {
+ EndPoint = endPoint;
+ IPMode = ipMode;
+
+ _readerPool = readerPool;
+ _pool = MemoryPool<byte>.Shared;
+ _socket = new UdpClient(endPoint);
+
+ try
+ {
+ _socket.DontFragment = false;
+ }
+ catch (SocketException)
+ {
+ }
+
+ _reliablePacketTimer = new Timer(ManageReliablePackets, null, 100, Timeout.Infinite);
+
+ _allConnections = new ConcurrentDictionary<EndPoint, UdpServerConnection>();
+
+ _stoppingCts = new CancellationTokenSource();
+ _stoppingCts.Token.Register(() =>
+ {
+ _socket.Dispose();
+ });
+
+ _connectionRateLimit = new UdpConnectionRateLimit();
+ }
+
+ public int ConnectionCount => this._allConnections.Count;
+
+ private async void ManageReliablePackets(object state)
+ {
+ foreach (var kvp in _allConnections)
+ {
+ var sock = kvp.Value;
+ await sock.ManageReliablePackets();
+ }
+
+ try
+ {
+ this._reliablePacketTimer.Change(100, Timeout.Infinite);
+ }
+ catch { }
+ }
+
+ /// <inheritdoc />
+ public override Task StartAsync()
+ {
+ // Store the task we're executing
+ _executingTask = Task.Factory.StartNew(ListenAsync, TaskCreationOptions.LongRunning);
+
+ // If the task is completed then return it, this will bubble cancellation and failure to the caller
+ if (_executingTask.IsCompleted)
+ {
+ return _executingTask;
+ }
+
+ // Otherwise it's running
+ return Task.CompletedTask;
+ }
+
+ private async Task StopAsync()
+ {
+ // Stop called without start
+ if (_executingTask == null)
+ {
+ return;
+ }
+
+ try
+ {
+ // Signal cancellation to the executing method
+ _stoppingCts.Cancel();
+ }
+ finally
+ {
+ // Wait until the task completes or the timeout triggers
+ await Task.WhenAny(_executingTask, Task.Delay(TimeSpan.FromSeconds(5)));
+ }
+ }
+
+ /// <summary>
+ /// Instructs the listener to begin listening.
+ /// </summary>
+ private async Task ListenAsync()
+ {
+ try
+ {
+ while (!_stoppingCts.IsCancellationRequested)
+ {
+ UdpReceiveResult data;
+
+ try
+ {
+ data = await _socket.ReceiveAsync();
+
+ if (data.Buffer.Length == 0)
+ {
+ Logger.Fatal("Hazel read 0 bytes from UDP server socket.");
+ continue;
+ }
+ }
+ catch (SocketException)
+ {
+ // Client no longer reachable, pretend it didn't happen
+ continue;
+ }
+ catch (ObjectDisposedException)
+ {
+ // Socket was disposed, don't care.
+ return;
+ }
+
+ // Get client from active clients
+ if (!_allConnections.TryGetValue(data.RemoteEndPoint, out var client))
+ {
+ // Check for malformed connection attempts
+ if (data.Buffer[0] != (byte)UdpSendOption.Hello)
+ {
+ continue;
+ }
+
+ // Check rateLimit.
+ if (!_connectionRateLimit.IsAllowed(data.RemoteEndPoint.Address))
+ {
+ Logger.Warning("Ratelimited connection attempt from {0}.", data.RemoteEndPoint);
+ continue;
+ }
+
+ // Create new client
+ client = new UdpServerConnection(this, data.RemoteEndPoint, IPMode, _readerPool);
+
+ // Store the client
+ if (!_allConnections.TryAdd(data.RemoteEndPoint, client))
+ {
+ throw new HazelException("Failed to add a connection. This should never happen.");
+ }
+
+ // Activate the reader loop of the client
+ await client.StartAsync();
+ }
+
+ // Write to client.
+ await client.Pipeline.Writer.WriteAsync(data.Buffer);
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Listen loop error");
+ }
+ }
+
+#if DEBUG
+ public int TestDropRate = -1;
+ private int dropCounter = 0;
+#endif
+
+ /// <summary>
+ /// Sends data from the listener socket.
+ /// </summary>
+ /// <param name="bytes">The bytes to send.</param>
+ /// <param name="endPoint">The endpoint to send to.</param>
+ internal async ValueTask SendData(byte[] bytes, int length, IPEndPoint endPoint)
+ {
+ if (length > bytes.Length) return;
+
+#if DEBUG
+ if (TestDropRate > 0)
+ {
+ if (Interlocked.Increment(ref dropCounter) % TestDropRate == 0)
+ {
+ return;
+ }
+ }
+#endif
+
+ try
+ {
+ await _socket.SendAsync(bytes, length, endPoint);
+ }
+ catch (SocketException e)
+ {
+ Logger.Error(e, "Could not send data as a SocketException occurred");
+ }
+ catch (ObjectDisposedException)
+ {
+ //Keep alive timer probably ran, ignore
+ return;
+ }
+ }
+
+ /// <summary>
+ /// Sends data from the listener socket.
+ /// </summary>
+ /// <param name="bytes">The bytes to send.</param>
+ /// <param name="length"></param>
+ /// <param name="endPoint">The endpoint to send to.</param>
+ internal void SendDataSync(byte[] bytes, int length, IPEndPoint endPoint)
+ {
+ try
+ {
+ _socket.Send(bytes, length, endPoint);
+ }
+ catch (SocketException e)
+ {
+ Logger.Error(e, "Could not send data sync as a SocketException occurred");
+ }
+ }
+
+ /// <summary>
+ /// Removes a virtual connection from the list.
+ /// </summary>
+ /// <param name="endPoint">The endpoint of the virtual connection.</param>
+ internal void RemoveConnectionTo(EndPoint endPoint)
+ {
+ this._allConnections.TryRemove(endPoint, out var conn);
+ }
+
+ /// <inheritdoc />
+ public override async ValueTask DisposeAsync()
+ {
+ foreach (var kvp in _allConnections)
+ {
+ kvp.Value.Dispose();
+ }
+
+ await StopAsync();
+
+ await _reliablePacketTimer.DisposeAsync();
+
+ _connectionRateLimit.Dispose();
+
+ await base.DisposeAsync();
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionRateLimit.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionRateLimit.cs
new file mode 100644
index 0000000..64881d3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionRateLimit.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Concurrent;
+using System.Net;
+using System.Threading;
+using Serilog;
+
+namespace Impostor.Hazel.Udp
+{
+ public class UdpConnectionRateLimit : IDisposable
+ {
+ private static readonly ILogger Logger = Log.ForContext<UdpConnectionRateLimit>();
+
+ // Allow burst to 5 connections.
+ // Decrease by 1 every second.
+ private const int MaxConnections = 5;
+ private const int FalloffMs = 1000;
+
+ private readonly ConcurrentDictionary<IPAddress, int> _connectionCount;
+ private readonly Timer _timer;
+ private bool _isDisposed;
+
+ public UdpConnectionRateLimit()
+ {
+ _connectionCount = new ConcurrentDictionary<IPAddress, int>();
+ _timer = new Timer(UpdateRateLimit, null, FalloffMs, Timeout.Infinite);
+ }
+
+ private void UpdateRateLimit(object state)
+ {
+ try
+ {
+ foreach (var pair in _connectionCount)
+ {
+ var count = pair.Value - 1;
+ if (count > 0)
+ {
+ _connectionCount.TryUpdate(pair.Key, count, pair.Value);
+ }
+ else
+ {
+ _connectionCount.TryRemove(pair);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Exception caught in UpdateRateLimit.");
+ }
+ finally
+ {
+ if (!_isDisposed)
+ {
+ _timer.Change(FalloffMs, Timeout.Infinite);
+ }
+ }
+ }
+
+ public bool IsAllowed(IPAddress key)
+ {
+ if (_connectionCount.TryGetValue(key, out var value) && value >= MaxConnections)
+ {
+ return false;
+ }
+
+ _connectionCount.AddOrUpdate(key, _ => 1, (_, i) => i + 1);
+ return true;
+ }
+
+ public void Dispose()
+ {
+ _isDisposed = true;
+ _timer.Dispose();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpServerConnection.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpServerConnection.cs
new file mode 100644
index 0000000..22eed98
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpServerConnection.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Hazel.Udp
+{
+ /// <summary>
+ /// Represents a servers's connection to a client that uses the UDP protocol.
+ /// </summary>
+ /// <inheritdoc/>
+ internal sealed class UdpServerConnection : UdpConnection
+ {
+ /// <summary>
+ /// The connection listener that we use the socket of.
+ /// </summary>
+ /// <remarks>
+ /// Udp server connections utilize the same socket in the listener for sends/receives, this is the listener that
+ /// created this connection and is hence the listener this conenction sends and receives via.
+ /// </remarks>
+ public UdpConnectionListener Listener { get; private set; }
+
+ /// <summary>
+ /// Creates a UdpConnection for the virtual connection to the endpoint.
+ /// </summary>
+ /// <param name="listener">The listener that created this connection.</param>
+ /// <param name="endPoint">The endpoint that we are connected to.</param>
+ /// <param name="IPMode">The IPMode we are connected using.</param>
+ internal UdpServerConnection(UdpConnectionListener listener, IPEndPoint endPoint, IPMode IPMode, ObjectPool<MessageReader> readerPool) : base(listener, readerPool)
+ {
+ this.Listener = listener;
+ this.RemoteEndPoint = endPoint;
+ this.EndPoint = endPoint;
+ this.IPMode = IPMode;
+
+ State = ConnectionState.Connected;
+ this.InitializeKeepAliveTimer();
+ }
+
+ /// <inheritdoc />
+ protected override async ValueTask WriteBytesToConnection(byte[] bytes, int length)
+ {
+ await Listener.SendData(bytes, length, RemoteEndPoint);
+ }
+
+ /// <inheritdoc />
+ /// <remarks>
+ /// This will always throw a HazelException.
+ /// </remarks>
+ public override ValueTask ConnectAsync(byte[] bytes = null)
+ {
+ throw new InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?");
+ }
+
+ /// <summary>
+ /// Sends a disconnect message to the end point.
+ /// </summary>
+ protected override async ValueTask<bool> SendDisconnect(MessageWriter data = null)
+ {
+ lock (this)
+ {
+ if (this._state != ConnectionState.Connected) return false;
+ this._state = ConnectionState.NotConnected;
+ }
+
+ var bytes = EmptyDisconnectBytes;
+ if (data != null && data.Length > 0)
+ {
+ if (data.SendOption != MessageType.Unreliable) throw new ArgumentException("Disconnect messages can only be unreliable.");
+
+ bytes = data.ToByteArray(true);
+ bytes[0] = (byte)UdpSendOption.Disconnect;
+ }
+
+ try
+ {
+ await Listener.SendData(bytes, bytes.Length, RemoteEndPoint);
+ }
+ catch { }
+
+ return true;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ Listener.RemoveConnectionTo(RemoteEndPoint);
+
+ if (disposing)
+ {
+ SendDisconnect();
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Patcher/Directory.Build.props b/Impostor-dev/src/Impostor.Patcher/Directory.Build.props
new file mode 100644
index 0000000..5302edd
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Directory.Build.props
@@ -0,0 +1,8 @@
+<Project>
+ <PropertyGroup>
+ <AssemblyTitle>Impostor</AssemblyTitle>
+ <Product>Impostor</Product>
+ <Copyright>Copyright © AeonLucid 2020</Copyright>
+ <Version>1.0.0</Version>
+ </PropertyGroup>
+</Project> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj
new file mode 100644
index 0000000..c59fa87
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <AssemblyName>Impostor.Cli</AssemblyName>
+ <TargetFramework>net5.0</TargetFramework>
+ <RuntimeIdentifiers>win-x64;linux-x64;linux-arm;linux-arm64;osx-x64</RuntimeIdentifiers>
+ <OutputType>Exe</OutputType>
+ <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Patcher.Shared\Impostor.Patcher.Shared.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20478.1" />
+ </ItemGroup>
+
+</Project>
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Program.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Program.cs
new file mode 100644
index 0000000..76653a1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Program.cs
@@ -0,0 +1,88 @@
+using System;
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.Threading.Tasks;
+using Impostor.Patcher.Shared;
+using Impostor.Patcher.Shared.Events;
+
+namespace Impostor.Patcher.Cli
+{
+ internal static class Program
+ {
+ private static readonly AmongUsModifier Modifier = new AmongUsModifier();
+
+ internal static Task<int> Main(string[] args)
+ {
+ var rootCommand = new RootCommand
+ {
+ new Option<string>(
+ "--address",
+ "IP Address of the server, will prompt if not specified"
+ ),
+ new Option<string>(
+ "--name",
+ () => AmongUsModifier.DefaultRegionName,
+ "Name for server region"
+ )
+ };
+
+ rootCommand.Handler = CommandHandler.Create<string, string>((address, name) =>
+ {
+ Modifier.RegionName = name;
+ Modifier.Error += ModifierOnError;
+ Modifier.Saved += ModifierOnSaved;
+
+ Console.WriteLine("Welcome to Impostor");
+
+ if (Modifier.TryLoadRegionInfo(out var regionInfo))
+ {
+ Console.WriteLine($"Currently selected region: {regionInfo.Name} ({regionInfo.Ping}, {regionInfo.Servers.Count} server(s))");
+ }
+
+ if (address != null)
+ {
+ return Modifier.SaveIpAsync(address);
+ }
+
+ return PromptAsync();
+ });
+
+ return rootCommand.InvokeAsync(args);
+ }
+
+ private static void ModifierOnSaved(object sender, SavedEventArgs e)
+ {
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine("The IP Address was saved, please (re)start Among Us.");
+ Console.ResetColor();
+ }
+
+ private static void WriteError(string message)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine(message);
+ Console.ResetColor();
+ }
+
+ private static void ModifierOnError(object sender, ErrorEventArgs e)
+ {
+ WriteError(e.Message);
+ }
+
+ private static async Task PromptAsync()
+ {
+ Console.WriteLine("Please enter in the IP Address of the server you would like to use for Among Us");
+ Console.WriteLine("If you want to stop playing on the server, simply select another region");
+
+ while (true)
+ {
+ Console.Write("> ");
+
+ if (await Modifier.SaveIpAsync(Console.ReadLine()))
+ {
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/AmongUsModifier.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/AmongUsModifier.cs
new file mode 100644
index 0000000..95f5524
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/AmongUsModifier.cs
@@ -0,0 +1,247 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Gameloop.Vdf;
+using Gameloop.Vdf.Linq;
+using Impostor.Patcher.Shared.Events;
+using Impostor.Patcher.Shared.Innersloth;
+using ErrorEventArgs = Impostor.Patcher.Shared.Events.ErrorEventArgs;
+
+namespace Impostor.Patcher.Shared
+{
+ public class AmongUsModifier
+ {
+ private const uint AppId = 945360;
+ public const string DefaultRegionName = "Impostor";
+ public const ushort DefaultPort = 22023;
+
+ private readonly string _amongUsDir;
+ private readonly string _regionFile;
+
+ public string RegionName { get; set; } = DefaultRegionName;
+
+ public AmongUsModifier()
+ {
+ var appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "..", "LocalLow");
+
+ if (!Directory.Exists(appData))
+ {
+ appData = FindProtonAppData();
+ }
+
+ if (appData == null)
+ return;
+
+ _amongUsDir = Path.Combine(appData, "Innersloth", "Among Us");
+ _regionFile = Path.Combine(_amongUsDir, "regionInfo.dat");
+ }
+
+ private string FindProtonAppData()
+ {
+ string steamApps;
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ steamApps = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".steam", "steam", "steamapps");
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ steamApps = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Steam", "steamapps");
+ }
+ else
+ {
+ return null;
+ }
+
+ if (!Directory.Exists(steamApps))
+ return null;
+
+ var libraries = new List<string>
+ {
+ steamApps
+ };
+
+ var vdf = Path.Combine(steamApps, "libraryfolders.vdf");
+ if (File.Exists(vdf))
+ {
+ var libraryFolders = VdfConvert.Deserialize(File.ReadAllText(vdf));
+
+ foreach (var libraryFolder in libraryFolders.Value.Children<VProperty>())
+ {
+ if (!int.TryParse(libraryFolder.Key, out _))
+ continue;
+
+ libraries.Add(Path.Combine(libraryFolder.Value.Value<string>(), "steamapps"));
+ }
+ }
+
+ foreach (var library in libraries)
+ {
+ var path = Path.Combine(library, "compatdata", AppId.ToString(), "pfx", "drive_c", "users", "steamuser", "AppData", "LocalLow");
+ if (Directory.Exists(path))
+ {
+ return path;
+ }
+ }
+
+ return null;
+ }
+
+ public async Task<bool> SaveIpAsync(string input)
+ {
+ // Filter out whitespace.
+ input = input.Trim();
+
+ // Split port from ip.
+ // Only IPv4 is supported so just do it simple.
+ var ip = string.Empty;
+ var port = DefaultPort;
+
+ var parts = input.Split(':');
+ if (parts.Length >= 1)
+ {
+ ip = parts[0];
+ }
+
+ if (parts.Length >= 2)
+ {
+ ushort.TryParse(parts[1], out port);
+ }
+
+ // Check if a valid IP address was entered.
+ if (!IPAddress.TryParse(ip, out var ipAddress))
+ {
+ // Attempt to resolve DNS.
+ try
+ {
+ var hostAddresses = await Dns.GetHostAddressesAsync(ip);
+ if (hostAddresses.Length == 0)
+ {
+ OnError("Invalid IP Address entered");
+ return false;
+ }
+
+ // Use first IPv4 result.
+ ipAddress = hostAddresses.First(x => x.AddressFamily == AddressFamily.InterNetwork);
+ }
+ catch (SocketException)
+ {
+ OnError("Failed to resolve hostname.");
+ return false;
+ }
+ }
+
+ // Only IPv4.
+ if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ OnError("Invalid IP Address entered, only IPv4 is allowed.");
+ return false;
+ }
+
+ return WriteIp(ipAddress, port);
+ }
+
+ /// <summary>
+ /// Writes an IP Address to the Among Us region file.
+ /// </summary>
+ /// <param name="ipAddress">The IPv4 address to write.</param>
+ /// <param name="port"></param>
+ private bool WriteIp(IPAddress ipAddress, ushort port)
+ {
+ if (ipAddress == null ||
+ ipAddress.AddressFamily != AddressFamily.InterNetwork)
+ {
+ throw new ArgumentException(nameof(ipAddress));
+ }
+
+ if (!Directory.Exists(_amongUsDir))
+ {
+ OnError("Among Us directory was not found, is it installed? Try running it once.");
+ return false;
+ }
+
+ using (var file = File.Open(_regionFile, FileMode.Create, FileAccess.Write))
+ using (var writer = new BinaryWriter(file))
+ {
+ var ip = ipAddress.ToString();
+ var region = new RegionInfo(RegionName, ip, new[]
+ {
+ new ServerInfo($"{RegionName}-Master-1", ip, port)
+ });
+
+ region.Serialize(writer);
+
+ OnSaved(ip, port);
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Loads the existing region info from the Among Us.
+ /// </summary>
+ public bool TryLoadRegionInfo(out RegionInfo regionInfo)
+ {
+ regionInfo = null;
+
+ if (!File.Exists(_regionFile))
+ {
+ return false;
+ }
+
+ using (var file = File.Open(_regionFile, FileMode.Open, FileAccess.Read))
+ using (var reader = new BinaryReader(file))
+ {
+ try
+ {
+ regionInfo = RegionInfo.Deserialize(reader);
+ return true;
+ }
+ catch (Exception exception)
+ {
+ OnError("Couldn't parse region info\n" + exception);
+ return false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Loads the existing IP Address from the Among Us region file
+ /// if it was set by Impostor before.
+ /// </summary>
+ public bool TryLoadIp(out string ipAddress)
+ {
+ ipAddress = null;
+
+ if (!TryLoadRegionInfo(out var regionInfo))
+ {
+ return false;
+ }
+
+ if ((regionInfo.Name == RegionName || regionInfo.Name == DefaultRegionName) && regionInfo.Servers.Count >= 1)
+ {
+ ipAddress = regionInfo.Servers.ElementAt(0).Ip;
+ return true;
+ }
+
+ return false;
+ }
+
+ private void OnError(string message)
+ {
+ Error?.Invoke(this, new ErrorEventArgs(message));
+ }
+
+ private void OnSaved(string ipAddress, ushort port)
+ {
+ Saved?.Invoke(this, new SavedEventArgs(ipAddress, port));
+ }
+
+ public event EventHandler<ErrorEventArgs> Error;
+ public event EventHandler<SavedEventArgs> Saved;
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Configuration.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Configuration.cs
new file mode 100644
index 0000000..b256156
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Configuration.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Impostor.Patcher.Shared
+{
+ public class Configuration
+ {
+ private const string FileRecentIps = @"recent_ips.txt";
+ private const int MaxRecentIps = 5;
+
+ private readonly string _baseDir;
+ private readonly string _recentIpsPath;
+ private readonly List<string> _recentIps;
+
+ public Configuration()
+ {
+ var appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
+
+ _baseDir = Path.Combine(appData, "Impostor");
+ _recentIpsPath = Path.Combine(_baseDir, FileRecentIps);
+ _recentIps = new List<string>();
+ }
+
+ public IReadOnlyList<string> RecentIps => _recentIps;
+
+ public void Load()
+ {
+ if (File.Exists(_recentIpsPath))
+ {
+ _recentIps.AddRange(File.ReadAllLines(_recentIpsPath));
+ }
+ }
+
+ public void Save()
+ {
+ Directory.CreateDirectory(_baseDir);
+
+ if (!Directory.Exists(_baseDir))
+ {
+ return;
+ }
+
+ if (_recentIps.Count > 0)
+ {
+ File.WriteAllLines(_recentIpsPath, _recentIps);
+ }
+ }
+
+ public void AddIp(string ip)
+ {
+ if (_recentIps.Contains(ip))
+ {
+ _recentIps.Remove(ip);
+ }
+
+ _recentIps.Insert(0, ip);
+
+ if (_recentIps.Count > MaxRecentIps)
+ {
+ _recentIps.RemoveAt(MaxRecentIps);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/ErrorEventArgs.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/ErrorEventArgs.cs
new file mode 100644
index 0000000..7211d5d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/ErrorEventArgs.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Impostor.Patcher.Shared.Events
+{
+ public class ErrorEventArgs : EventArgs
+ {
+ public ErrorEventArgs(string message)
+ {
+ Message = message;
+ }
+
+ public string Message { get; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/SavedEventArgs.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/SavedEventArgs.cs
new file mode 100644
index 0000000..c91d071
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/SavedEventArgs.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Impostor.Patcher.Shared.Events
+{
+ public class SavedEventArgs : EventArgs
+ {
+ public SavedEventArgs(string ipAddress, ushort port)
+ {
+ IpAddress = ipAddress;
+ Port = port;
+ }
+
+ public string IpAddress { get; }
+ public ushort Port { get; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Impostor.Patcher.Shared.csproj b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Impostor.Patcher.Shared.csproj
new file mode 100644
index 0000000..e480870
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Impostor.Patcher.Shared.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
+ <Version>1.0.0</Version>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Gameloop.Vdf" Version="0.6.1" />
+ </ItemGroup>
+
+</Project>
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/RegionInfo.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/RegionInfo.cs
new file mode 100644
index 0000000..01b74d1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/RegionInfo.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Impostor.Patcher.Shared.Innersloth
+{
+ public class RegionInfo
+ {
+ public RegionInfo(string name, string ping, IReadOnlyList<ServerInfo> servers)
+ {
+ Name = name;
+ Ping = ping;
+ Servers = servers;
+ }
+
+ public string Name { get; }
+ public string Ping { get; }
+ public IReadOnlyList<ServerInfo> Servers { get; }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.Write(0);
+ writer.Write(Name);
+ writer.Write(Ping);
+ writer.Write(Servers.Count);
+
+ foreach (var server in Servers)
+ {
+ server.Serialize(writer);
+ }
+ }
+
+ public static RegionInfo Deserialize(BinaryReader reader)
+ {
+ var unknown = reader.ReadInt32();
+ var name = reader.ReadString();
+ var ping = reader.ReadString();
+ var servers = new List<ServerInfo>();
+ var serverCount = reader.ReadInt32();
+
+ for (var i = 0; i < serverCount; i++)
+ {
+ servers.Add(ServerInfo.Deserialize(reader));
+ }
+
+ return new RegionInfo(name, ping, servers);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/ServerInfo.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/ServerInfo.cs
new file mode 100644
index 0000000..7203c84
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/ServerInfo.cs
@@ -0,0 +1,37 @@
+using System.IO;
+using System.Net;
+
+namespace Impostor.Patcher.Shared.Innersloth
+{
+ public class ServerInfo
+ {
+ public string Name { get; }
+ public string Ip { get; }
+ public ushort Port { get; }
+
+ public ServerInfo(string name, string ip, ushort port)
+ {
+ Name = name;
+ Ip = ip;
+ Port = port;
+ }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.Write(Name);
+ writer.Write(IPAddress.Parse(Ip).GetAddressBytes());
+ writer.Write(Port);
+ writer.Write(0);
+ }
+
+ public static ServerInfo Deserialize(BinaryReader reader)
+ {
+ var name = reader.ReadString();
+ var ip = new IPAddress(reader.ReadBytes(4)).ToString();
+ var port = reader.ReadUInt16();
+ var unknown = reader.ReadInt32();
+
+ return new ServerInfo(name, ip, port);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/App.config b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/App.config
new file mode 100644
index 0000000..2a83c36
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/App.config
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<configuration>
+ <startup>
+ <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
+ </startup>
+</configuration> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.Designer.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.Designer.cs
new file mode 100644
index 0000000..0f6320b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.Designer.cs
@@ -0,0 +1,141 @@
+namespace Impostor.Patcher.WinForms.Forms
+{
+ partial class FrmMain
+ {
+ /// <summary>
+ /// Required designer variable.
+ /// </summary>
+ private System.ComponentModel.IContainer components = null;
+
+ /// <summary>
+ /// Clean up any resources being used.
+ /// </summary>
+ /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ /// <summary>
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ /// </summary>
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmMain));
+ this.label1 = new System.Windows.Forms.Label();
+ this.label2 = new System.Windows.Forms.Label();
+ this.buttonLaunch = new System.Windows.Forms.Button();
+ this.lblUrl = new System.Windows.Forms.Label();
+ this.label3 = new System.Windows.Forms.Label();
+ this.comboIp = new System.Windows.Forms.ComboBox();
+ this.SuspendLayout();
+ //
+ // label1
+ //
+ this.label1.AutoSize = true;
+ this.label1.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+ this.label1.Location = new System.Drawing.Point(28, 139);
+ this.label1.Name = "label1";
+ this.label1.Size = new System.Drawing.Size(60, 13);
+ this.label1.TabIndex = 1;
+ this.label1.Text = "IP Address";
+ //
+ // label2
+ //
+ this.label2.AutoSize = true;
+ this.label2.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+ this.label2.Location = new System.Drawing.Point(28, 23);
+ this.label2.Name = "label2";
+ this.label2.Size = new System.Drawing.Size(225, 91);
+ this.label2.TabIndex = 0;
+ this.label2.Text = "Welcome to Impostor\r\n\r\nPlease enter in the IP Address of the \r\nserver you would l" +
+ "ike to use for Among Us\r\n\r\nIf you want to stop playing on the server, \r\nsimply s" +
+ "elect another region";
+ //
+ // buttonLaunch
+ //
+ this.buttonLaunch.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+ this.buttonLaunch.Location = new System.Drawing.Point(179, 155);
+ this.buttonLaunch.Name = "buttonLaunch";
+ this.buttonLaunch.Size = new System.Drawing.Size(74, 22);
+ this.buttonLaunch.TabIndex = 3;
+ this.buttonLaunch.Text = "Save";
+ this.buttonLaunch.UseVisualStyleBackColor = true;
+ this.buttonLaunch.Click += new System.EventHandler(this.buttonLaunch_Click);
+ //
+ // lblUrl
+ //
+ this.lblUrl.AutoSize = true;
+ this.lblUrl.Cursor = System.Windows.Forms.Cursors.Hand;
+ this.lblUrl.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Underline, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+ this.lblUrl.ForeColor = System.Drawing.SystemColors.Highlight;
+ this.lblUrl.Location = new System.Drawing.Point(39, 232);
+ this.lblUrl.Name = "lblUrl";
+ this.lblUrl.Size = new System.Drawing.Size(212, 13);
+ this.lblUrl.TabIndex = 5;
+ this.lblUrl.Text = "https://github.com/AeonLucid/Impostor";
+ this.lblUrl.Click += new System.EventHandler(this.lblUrl_Click);
+ //
+ // label3
+ //
+ this.label3.AutoSize = true;
+ this.label3.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+ this.label3.Location = new System.Drawing.Point(54, 216);
+ this.label3.Name = "label3";
+ this.label3.Size = new System.Drawing.Size(182, 13);
+ this.label3.TabIndex = 6;
+ this.label3.Text = "Source code and latest versions at\r\n";
+ //
+ // comboIp
+ //
+ this.comboIp.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+ this.comboIp.FormattingEnabled = true;
+ this.comboIp.Location = new System.Drawing.Point(31, 155);
+ this.comboIp.Name = "comboIp";
+ this.comboIp.Size = new System.Drawing.Size(141, 21);
+ this.comboIp.TabIndex = 2;
+ this.comboIp.KeyDown += new System.Windows.Forms.KeyEventHandler(this.textIp_KeyDown);
+ //
+ // FrmMain
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(283, 262);
+ this.Controls.Add(this.comboIp);
+ this.Controls.Add(this.label3);
+ this.Controls.Add(this.lblUrl);
+ this.Controls.Add(this.buttonLaunch);
+ this.Controls.Add(this.label2);
+ this.Controls.Add(this.label1);
+ this.ForeColor = System.Drawing.SystemColors.ControlText;
+ this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
+ this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
+ this.MaximizeBox = false;
+ this.Name = "FrmMain";
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
+ this.Text = "Impostor";
+ this.Load += new System.EventHandler(this.FrmMain_Load);
+ this.Shown += new System.EventHandler(this.FrmMain_Shown);
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Label label1;
+ private System.Windows.Forms.Label label2;
+ private System.Windows.Forms.Button buttonLaunch;
+ private System.Windows.Forms.Label lblUrl;
+ private System.Windows.Forms.Label label3;
+ private System.Windows.Forms.ComboBox comboIp;
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.cs
new file mode 100644
index 0000000..5c06669
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Diagnostics;
+using System.Windows.Forms;
+using Impostor.Patcher.Shared;
+using Impostor.Patcher.Shared.Events;
+
+namespace Impostor.Patcher.WinForms.Forms
+{
+ public partial class FrmMain : Form
+ {
+ private readonly Configuration _config;
+ private readonly AmongUsModifier _modifier;
+
+ public FrmMain()
+ {
+ InitializeComponent();
+
+ AcceptButton = buttonLaunch;
+
+ _config = new Configuration();
+ _modifier = new AmongUsModifier();
+ _modifier.Error += ModifierOnError;
+ _modifier.Saved += ModifierOnSaved;
+ }
+
+ private void ModifierOnError(object sender, ErrorEventArgs e)
+ {
+ MessageBox.Show(e.Message, "Error",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Error);
+
+ comboIp.Text = string.Empty;
+ comboIp.Focus();
+
+ comboIp.Enabled = true;
+ buttonLaunch.Enabled = true;
+ }
+
+ private void ModifierOnSaved(object sender, SavedEventArgs e)
+ {
+ MessageBox.Show("The IP Address was saved, please (re)start Among Us.", "Success",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Information);
+
+ var ipText = e.Port == AmongUsModifier.DefaultPort
+ ? e.IpAddress
+ : $"{e.IpAddress}:{e.Port}";
+
+ comboIp.Text = ipText;
+ comboIp.Enabled = true;
+ buttonLaunch.Enabled = true;
+
+ _config.AddIp(ipText);
+ _config.Save();
+
+ RefreshComboIps();
+ }
+
+ private void FrmMain_Load(object sender, EventArgs e)
+ {
+ _config.Load();
+
+ RefreshComboIps();
+
+ if (_modifier.TryLoadIp(out var ipAddress))
+ {
+ comboIp.Text = ipAddress;
+ }
+ }
+
+ private void FrmMain_Shown(object sender, EventArgs e)
+ {
+ comboIp.Focus();
+ }
+
+ private void textIp_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.KeyCode != Keys.Enter)
+ {
+ return;
+ }
+
+ e.Handled = true;
+
+ buttonLaunch_Click(this, EventArgs.Empty);
+ }
+
+ private async void buttonLaunch_Click(object sender, EventArgs e)
+ {
+ comboIp.Enabled = false;
+ buttonLaunch.Enabled = false;
+
+ await _modifier.SaveIpAsync(comboIp.Text);
+ }
+
+ private void lblUrl_Click(object sender, EventArgs e)
+ {
+ Process.Start("https://github.com/AeonLucid/Impostor");
+ }
+
+ private void RefreshComboIps()
+ {
+ comboIp.Items.Clear();
+
+ if (_config.RecentIps.Count > 0)
+ {
+ foreach (var ip in _config.RecentIps)
+ {
+ comboIp.Items.Add(ip);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.resx b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.resx
new file mode 100644
index 0000000..839a9c4
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.resx
@@ -0,0 +1,2338 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
+ <data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>
+ 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
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIaXAQeMocbEwpxBBAA
+ ZwILAEcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAALXSbAShnjgUbQWcJDRc8CwoPNAsaPGIZH01yLRc1WkUUK1FNEB9FTwYAJVIGACVSBgAlUgYA
+ JVIGACVSBgAlUgYAJVIGACVSBgAlUgYAJVIGACVSDRRHUA8cU04WL3Q5FSxuIwkKNQsJCzgLEB9ZChcw
+ dQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAACdOpAcgMZEdFhB6BxIAcAISAHAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAKGeNBiBSeCYaPGNFFS9VWREkSW8QHUKAEB9EpwwRN70LDjTEDBM52wsRNvUIBiv/BwQp/wcD
+ KP8HAif/BwEm/wYBJv8GASb/BgAl/wYAJf8GACX/BgAl/wYAJf8GACX/BgAl/wYAJf8GACX/BgAl/wYA
+ Jf8GASb/BgEm/wYBJ/8GAij/BwIp/wcDKf8JCDP7CAgyzAkJNMEKCzi/Cw8+ug4XTJ8NFkp8DxtSaRQp
+ aU0XMXgSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACRC
+ nAEjPpkfFxR9CBIAcAMSAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAKGmQBxc1WxQXNFopDRg9WwkILV4KCzCeCQkuswsPNOAHBSruBwQp+QcDKP8GASb/BgAl/wYA
+ Jf8GACX/BgAl/wYAJf8GACX/BgAl/wYAJf8GACb/BgAn/wYAJ/8HACj/BwAp/wcAKf8HACr/BwAq/wcA
+ Kv8HACr/BwAq/wcAKv8HACr/BwAq/wcAKv8HACr/BwAq/wcAKf8HACj/BgAn/wYAJ/8GACX/BgAl/wYA
+ Jf8GACX/BgAl/wYAJf8GACX/BgAl/wYAJf8GACX/BwIo/wYBJvEHAijsBwIpuAYAJaUJCTOjCAcwYgsQ
+ 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
+ xv8AAMb/AADG/wAAxv8AAMb/BABe/wYAJf8JADf/DgCB/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADF/w8Afv8SAHD/EABi/wYAJ/8GACb/DABL/w0A
+ Uf8NAFH/DQBR/w0AUf8PAF7/EgBu/wsAQ/8GACX/BgAn/wMAdP8AAMT/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxf8GAKb/EQB1/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8QAGf/DQBS/w0A
+ Uf8NAFH/DQBR/w0AUf8JADf/BgAl/w0VOtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAGACXNBgAl/wkAO/8NAFH/DQBR/w0AUf8NAFH/DQBR/w0AUf8NAFP/EQBr/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcf8MAIr/AwC5/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAwP8FAD7/BgAl/wsA
+ Sf8GAKf/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/DACL/xIAcP8QAGL/BgAn/wYAJv8MAEn/DQBR/w0AUf8NAFH/DQBR/w0AUv8QAGT/EQBr/wkA
+ Ov8GACX/BgAp/wMAef8AAMP/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8FAK//EAB5/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAb/8OAFn/DQBR/w0AUf8NAFH/DQBR/wkAN/8GACX/DRU61AAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAJX0GACX/CQA7/w0A
+ Uf8NAFH/DQBR/w0AUf8NAFH/DQBR/w8AXv8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8RAHb/BwCj/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AACx/wYAKf8GACb/DABq/wEAwf8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8MAI7/EgBw/w8AX/8GACb/BgAl/wYA
+ KP8JADf/DABJ/w0AUf8NAFH/DQBR/w0AUv8QAGf/EQBp/wgANP8GACX/BgAo/wMAef8AAMT/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8DALT/EAB6/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xAA
+ Zf8NAFH/DQBR/w0AUf8NAFH/CQA3/wYAJf8NFTrUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAABgAlVwYAJf8JADv/DQBR/w0AUf8NAFH/DQBR/w0AUf8NAFL/EQBq/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DgCA/wQAtP8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAJH/BgAl/wcA
+ K/8HAJf/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/w0Aiv8SAHD/DwBc/wYAJv8GACX/BgAl/wYAJf8GACf/CQA3/wwATv8NAFH/DQBR/w0A
+ U/8QAGj/EABl/wgAM/8GACX/BgAp/wMAe/8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8DALX/EAB5/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBu/w4AVv8NAFH/DQBR/w0AUf8JADf/BgAl/w0V
+ OtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGACVhBgAl/wkA
+ O/8NAFH/DQBR/w0AUf8NAFH/DQBR/w4AWf8SAG//EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/wwAi/8CAL7/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wMAbf8GACX/CAA7/wIAu/8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMX/DwB9/xIAcP8NAFP/BgAl/wYA
+ Jv8HACv/BgAn/wYAJf8GACX/CAAw/wwATP8NAFH/DQBR/w0AVP8RAGr/EABl/wgAMf8GACX/BgAt/wEA
+ r/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8FAK7/EQB1/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/DwBg/w0AUf8NAFH/DQBR/wkAN/8GACX/DRU61AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBJ1YGACX/CQA4/w0AUf8NAFH/DQBR/w0AUf8NAFL/EABn/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcf8KAJT/AQDD/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMT/BABK/wYA
+ Jf8EAGj/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wIAu/8SAHL/EgBw/woAQP8GACX/CAAy/w0AUP8MAEr/CAA1/wYAJv8GACX/BwAt/wwA
+ S/8NAFH/DQBR/w4AVf8RAGv/DwBg/wcAKf8GACX/AgCB/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8HAKP/EgBx/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8RAGr/DQBS/w0AUf8NAFH/CQA3/wYA
+ Jf8NFTrUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgIoXwYA
+ Jf8JADf/DQBR/w0AUf8NAFH/DQBR/w4AWP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHH/CQCZ/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAtv8FADL/BgAl/wIAk/8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BwCl/xIAcP8RAGv/BwAu/wYA
+ Jf8KAED/DQBR/w0AUf8NAFD/CgBA/wYAKf8GACX/CAAv/wwATv8NAFH/DQBR/w4AV/8RAGz/CQA2/wYA
+ Jf8DAHH/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxP8MAIz/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAb/8OAFb/DQBR/w0AUf8JADf/BgAl/w0WO9EAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAihYBgAl/wkAN/8NAFH/DQBR/w0AUf8NAFH/EABj/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/woAlv8AAMT/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCc/wYA
+ Jv8GAC7/AQCw/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxf8MAI7/EgBw/w0AU/8GACX/BwAp/wwATf8NAFH/DQBR/w0AUf8NAFH/DABK/wcA
+ K/8GACX/CQA6/w0AUf8NAFH/DQBR/w4AWv8JADj/BgAl/wMAcf8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wIA
+ vP8QAHr/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/w8AXv8NAFH/DQBR/wgA
+ NP8GACX/EBxCsgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgF
+ K2IGACX/CAA0/w0AUf8NAFH/DQBR/w0AVP8RAGz/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8MAIz/AQDC/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8DAHf/BgAl/wUARP8AAMH/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQDC/w8Aff8RAGz/CAAy/wYA
+ Jf8IADb/DQBR/w0AUf8OAFX/DwBb/w4AWP8NAFL/CgA+/wYAJf8HACn/DABM/w0AUf8NAFH/DQBR/wgA
+ M/8GACX/AwBx/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wYAp/8SAHL/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EABm/w0AUf8NAFH/CAA0/wYAJf8QHEKuAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQgtZgYAJf8IADT/DQBR/w0AUf8NAFH/DwBc/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DwB8/wIAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wQA
+ U/8GACX/BABm/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8DALb/EQBz/w0AVP8GACb/BgAm/wsAR/8NAFH/DgBY/xIAbv8SAHD/EgBv/xAA
+ aP8NAE//BwAq/wYAJf8JADv/DQBR/w0AUf8NAFH/CQA3/wYAJf8DAGf/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADE/wwAiv8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8RAGz/DQBT/w0A
+ Uf8IADL/BgAl/w0XPZoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAKDDFtBgAl/wgAMf8NAFH/DQBR/w0AUf8QAGf/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cv8GAKr/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AALz/BQA4/wYAJf8CAIv/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wgAn/8RAG3/CAAx/wYA
+ Jf8HACv/DABM/w0ATv8LAEP/EQBt/w8Af/8MAIr/EgBw/xAAZf8JADv/BgAl/wcAKv8MAE7/DQBR/w0A
+ Uf8KADz/BgAl/wQAWP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwC5/xAAd/8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAb/8OAFb/DQBR/wcAMP8GACX/CxE5kAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsQNXwGACX/CAAw/w0AUf8NAFH/DQBU/xIA
+ bv8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DQCJ/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEA
+ qP8GACn/BgAp/wEAqf8AAMb/AADG/wAAxv8AAMb/AADG/wUArv8EALH/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMP/DgCB/w8AXv8GACb/BgAl/wYAJf8GACj/BwAq/wYAKf8PAGH/DACN/wEA
+ wf8OAIP/EgBw/xAAYv8HACr/BgAl/woAQf8NAFH/DQBR/wsAQv8GACX/BQBG/wAAw/8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/CACd/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/w8A
+ W/8NAFH/BwAt/wYAJf8LDz6OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAADBQ5fwYAJf8HAC3/DQBR/w0AUf8OAFr/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cv8FALD/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AgCG/wYAJv8FADP/AAC9/wAAxv8AAMb/AADG/wAA
+ xv8AAMT/DgCD/w0AiP8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAwv8QAHz/EQBo/wgA
+ Mf8HACn/BgAn/wYAJ/8HACn/CAAy/xAAZv8HAKL/AADG/wYAqP8SAHD/EgBw/wsAQv8GACX/CAAv/w0A
+ UP8NAFH/CwBI/wYAJv8FADT/AAC4/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAMH/DgCB/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DwBh/w0AUP8HACv/BgAl/w4ZToQAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMEzhrBgAl/wcALP8NAFH/DQBR/w8A
+ Yf8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DQCF/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8DAGP/BgAl/wUARv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxP8OAIH/DwB7/wEAwP8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wMAtv8MAIv/EABz/xAAaP8QAGP/EABi/xAAZf8RAGv/DwB+/wEA
+ v/8AAMb/AgC8/xEAdP8SAHD/DwBg/wYAJ/8GACb/CwBG/w0AUf8MAEv/BgAo/wYAJ/8BAKH/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8FAK3/EgBy/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8QAGX/DABO/wcAKv8GACX/EB5WZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAABMpTmgGACX/BwAr/w0AT/8NAFH/EABn/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8GAKf/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wUASf8GACX/BABb/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADE/w4Agf8SAHH/BwCk/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8BAL//AwC3/wQAsv8FAK7/BgCp/wUAq/8CALz/AADG/wAAxv8AAMX/DwB8/xIAcP8MAIf/CQA6/wYA
+ Jf8IADT/DQBR/w0AUP8HAC3/BgAl/wMAeP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xf8MAIv/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xEAaf8MAEz/BgAo/wYAJf8QHlhGAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFzRaXQYAJf8HACr/DABO/w0A
+ U/8RAGz/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EAB3/wEAv/8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/BQBH/wYAJf8EAEb/AADD/wAAxv8AAMb/AADG/wAAxv8AAMT/DgCE/xIAcP8QAHr/AwC2/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxP8QAHv/EgBw/wUArv8MAGT/BgAm/wYAKP8MAEz/DgBV/wsAQ/8GACX/BABL/wAA
+ xP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAtf8RAHT/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EQBr/wwATP8GACf/BgAl/xUsbz0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAZOmBKBgAl/wYAKP8MAEz/DQBU/xIAbv8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8MAI3/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8DAHD/BgAl/wYAJ/8DAHT/AADA/wAA
+ xv8AAMb/AADG/wAAxv8DALX/CwCQ/xEAdP8PAH3/BACw/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BACy/xEAc/8SAHD/AgC8/wkA
+ lP8IADL/BgAl/woAQP8OAFn/DwBc/wYAJv8GAC3/AQCu/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADF/wsAkP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8RAG3/DABK/wYAJv8HAyr/Gz2LKAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxGbDoGACX/BgAm/wwA
+ Sv8OAFX/EgBv/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/wYAqf8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAuP8FAEr/BgAl/wYAJv8EAFD/AQCr/wAAxv8AAMb/AADG/wAAxv8AAMb/AwC5/woA
+ lP8QAHn/CgCT/wMAuP8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wMAtf8PAH3/EgBw/xAAef8BAMH/BACy/wwAUP8GACX/CQA4/w8AX/8RAGz/BwAs/wYA
+ Jf8DAH//AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwC2/xEAdP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAb/8LAEj/BgAl/wkJNPkdQZMJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAI1mAKAcDKP8GACb/CwBH/w4AVv8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8RAHf/AgC9/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAr/8FADD/BgAl/wYA
+ Jf8FADT/AgCE/wAAwv8AAMb/AADG/wAAxv8AAMb/AADF/wIAuv8JAJn/DwB9/wwAjf8EALL/AADE/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAwf8IAJ7/EAB4/w8Af/8KAJX/BAC0/wAA
+ xv8BAMT/BwCF/wYAJ/8IADT/EQBo/xIAcP8JADf/BgAl/wQAUv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMX/DACK/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/wsARP8GACX/Cgs36Bk4
+ gwIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoZ44OCgsw+wYA
+ Jf8LAEP/DgBW/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/w0AiP8AAMX/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADB/wUAQ/8GACX/BgAl/wYAJf8GACb/BABY/wEAsP8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8BAL7/CQCc/w8Aff8NAIj/BQCs/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wEA
+ wP8HAKX/DwB//w4AgP8GAKf/AQDD/wAAxv8AAMb/AADG/wAAw/8DAGj/BgAl/wsARv8SAG//EgBv/wgA
+ Mv8GACX/BABM/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8FAK3/EgBx/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/CgBA/wYAJf8HAyq7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9OdAILDzXqBgAl/woAP/8OAFb/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/CACe/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwBm/wYA
+ Jf8GACb/BwAu/wYAJf8GACX/BQA3/wIAi/8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQC//wcA
+ o/8OAID/DgCD/wQAr/8AAMb/AADG/wIAvf8KAJT/DwB//w0Ah/8HAKP/AQDB/wAAxv8AAMb/AADG/wAA
+ xv8AAMX/AgB+/wYAKP8IADP/EQBp/xIAcP8PAF3/BgAn/wYAJf8DAHH/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wEAw/8PAH7/EgBw/xIAcP8SAHD/EgBw/xIAb/8KAD//BgAl/woM
+ ObMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmCHAAcE
+ KcIGACX/CQA6/w4AVf8SAG//EgBw/xIAcP8SAHD/EgBw/xIAcf8EALL/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAIn/BgAl/wYAJf8LAET/CgA9/wcAKf8GACX/BgAo/wQA
+ Yf8BALT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAwv8FAK3/BQCv/wAAxv8AAMb/BQCv/wYA
+ p/8CALr/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAn/8GAC7/BwAs/xAAYv8SAHD/EAB3/wgA
+ PP8GACX/BQAz/wEArv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wkA
+ mv8SAHD/EgBw/xIAcP8SAHD/EgBv/wkANf8GACX/BwQsfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgwxsQYAJf8IADX/DgBV/xIAbv8SAHD/EgBw/xIA
+ cP8SAHD/EAB5/wEAv/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEA
+ pP8GACX/BgAl/woAPf8NAFH/DABK/wgAMP8GACX/BgAl/wUAOv8CAJX/AADE/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8BALP/BQA7/wYAKP8OAFn/EgBw/wwAif8DAIz/BgAn/wYAJv8CAH7/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwC1/xEAcv8SAHD/EgBw/xIAcP8RAGz/BwAu/wYA
+ Jf8JCzdyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAGACV0BgAl/wgAMP8NAFT/EgBu/xIAcP8SAHD/EgBw/xIAcP8NAIb/AADF/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AAC2/wYALP8GACX/CQA3/w0AUf8NAFH/DQBP/woA
+ Pf8GACf/BgAl/wYAKv8DAGj/AAC5/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AAC7/wQATf8GACb/DABJ/xIAb/8OAIT/AQC+/wQA
+ Tf8GACX/BQBB/wAAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8BAMP/DwB//xIAcP8SAHD/EgBw/xAAZ/8HACn/BgEm/woLODcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcEKXQGACX/BwAs/w0AU/8RAG3/EgBw/xIA
+ cP8SAHD/EgBw/woAmP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAL7/BQA5/wYAJf8IADL/DQBU/w8AYf8QAGP/EABj/w4AV/8JADb/BgAl/wYAJf8FAD//AQCc/wAA
+ xP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ wP8EAF3/BgAl/woAPP8SAG7/EAB5/wMAt/8BAJ3/BgAp/wYAJf8CAIj/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8KAJb/EgBw/xIAcP8SAHD/DwBg/wYA
+ Jf8GAij/Cg47LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAADx1CWAYBJv8HACn/DQBQ/xEAbf8SAHD/EgBw/xIAcP8SAHD/BgCo/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAw/8FAEH/BgAl/wcALv8QAGf/EgBw/xIA
+ cP8SAHD/EgBw/xEAbP8MAEr/BgAo/wYAJf8GACz/AwBu/wAAvf8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMP/AwBr/wYAJv8HAC7/EABn/xEAc/8FAKz/AADE/wQA
+ Xv8GACX/BQA3/wAAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wUArf8SAHH/EgBw/xIAcP8OAFb/BgAl/wgGMPIWL3MXAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKDDEtBgEm/wYAJ/8MAE3/EQBs/xIA
+ cP8SAHD/EgBw/xEAc/8DALj/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wQAS/8GACX/BwAu/xEAbf8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAb/8PAF3/CAAy/wYA
+ Jf8GACX/BQBG/wEAof8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADB/wMA
+ dP8GACj/BgAl/w0AUP8RAHT/BwCk/wAAxv8BALH/BQAw/wYAJf8DAHL/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AgC+/xEAdv8SAHD/EgBw/wwA
+ Sv8GACX/CQgz4gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAA4aQCwHAyj+BgAl/wsASf8RAGz/EgBw/xIAcP8SAHD/EAB4/wEAwP8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BABV/wYAJf8HACv/EQBp/xIA
+ cf8MAIv/CgCT/w4AhP8RAHX/EgBw/xIAcP8RAGr/CgA8/wYAJf8GACX/BgAt/wMAeP8AAL//AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAv/8DAGj/BgAn/wYAJf8HACn/EABo/wcApP8AAMX/AADG/wIA
+ gv8GACX/BgAt/wEAqv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/DgCC/xIAcP8SAHD/CgA//wYAJf8MEkLHAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIE51FgkKL/EGACX/CgBC/xEA
+ a/8SAHD/EgBw/xIAcP8OAID/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8EAGH/BgAl/wcAKf8QAGT/DACN/wAAw/8AAMb/AADE/wMAuf8LAJH/EgBx/xIA
+ bv8JADj/BgAl/wYAJf8GACX/BgAm/wQATf8BAKj/AADG/wAAxv8AAMb/AADG/wAAxv8BAK7/BABS/wYA
+ Jv8GACX/BgAl/wgAM/8IAJn/AADF/wAAxv8AAMT/BABL/wYAJf8EAE3/AADD/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8KAJP/EgBw/xIA
+ cP8IADT/BgAl/wwTRJgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAACg0y4QYAJf8JADz/EQBq/xIAcP8SAHD/EgBw/wwAjP8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAa/8GACX/BgAn/w8A
+ ZP8EALT/AADG/wAAxv8AAMb/AADG/wAAxf8JAJn/EABp/wYAJv8GACX/BgAt/wQATP8GACf/BgAl/wYA
+ Lv8CAIL/AAC//wAAxv8AAMP/AgCS/wUAMv8GACX/BQBG/wQASf8GACX/BQBI/wAAw/8AAMb/AADG/wEA
+ qv8GACz/BgAl/wIAgf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wcApP8SAHD/EQBq/wcALf8GACX/DRZKYgAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOFz3LBgAl/wgA
+ Nf8QAGj/EgBw/xIAcP8SAHD/CgCV/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AwB2/wYAJf8GACX/DQBo/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wEA
+ w/8KAHP/BgAl/wYAJf8EAFz/AAC+/wIAff8GAC7/BgAl/wYAJ/8EAFT/AgCM/wQAYv8GACr/BgAo/wMA
+ Z/8AALr/AwBk/wYAJf8FAD3/AAC+/wAAxv8AAMb/AwB8/wYAJf8GACv/AQCw/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BACx/xIA
+ cP8QAGL/BgAn/wYAJf8LDj0YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4YPZQGACX/BwAv/xAAZv8SAHD/EgBw/xIAcP8IAJ7/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAID/BgAl/wYA
+ Jf8KAGr/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAg/8GACX/BgAm/wIAkf8AAMb/AADG/wEA
+ q/8EAFv/BgAq/wYAJf8GACX/BgAl/wUAOP8BAJD/AADE/wAAxv8CAID/BgAl/wYAKv8BAKL/AADG/wAA
+ v/8FAEL/BgAl/wQAVv8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CALr/EQB0/w0AUv8GACX/BwIo5AwQPggAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEB9EcgYA
+ Jf8HACr/DwBh/xIAcP8SAHD/EgBw/wcApf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wIAi/8GACb/BgAl/wgAa/8AAMX/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AwB7/wYAJf8GACv/AAC0/wAAxv8AAMb/AADG/wAAw/8BAJ7/BABb/wQAS/8DAGr/AQCy/wAA
+ xv8AAMb/AADG/wEAsf8FADL/BgAl/wUAPP8DAG3/BABX/wYAJv8GACj/AQCY/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEA
+ wP8QAHn/CwBF/wYAJf8HAijGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASJEovBgAl/wYAJ/8OAFv/EgBw/xIAcP8SAHD/BgCp/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AgCV/wYA
+ J/8GACX/BgBt/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8DAHv/BgAl/wYALf8AAML/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMX/AADD/wAAxf8AAMb/AADG/wAAxv8AAMb/AADF/wEAjP8FAC7/BgAl/wYA
+ Jf8GACX/BgAm/wQAYv8AAML/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADE/w8Aff8JADj/BgAl/wgGLo4AAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsQ
+ NQ4HBCn+BgAl/w0AUf8SAHD/EgBw/xIAcP8FAK3/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAJ//BgAo/wYAJf8DAGf/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wMAe/8GACX/BgAt/wAAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wEAmv8EAE//BQBB/wQASv8DAHb/AADA/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMX/DQCA/wcALf8GACX/CQo2fQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHklwBwkJLuMGACX/CwBG/xIAcP8SAHD/EgBw/wUA
+ rf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEA
+ qP8GACn/BgAl/wQAXf8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwB7/wYAJf8GACz/AAC9/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ w/8AAL//AADC/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxf8LAHz/BgAn/wYBJ/8NFkpBAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAACAcwwwYAJf8KAD7/EgBw/xIAcP8SAHD/BQCt/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCy/wYAKv8GACX/BABS/wAAxf8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8DAHP/BgAl/wYAKv8BALH/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADF/wgAcv8GACX/CAUu+hYwdycAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBjCGBgAl/wgAMv8SAG7/EgBw/xIA
+ cP8FAK3/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8BALb/BgAw/wYAJf8EAFH/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAbf8GACX/BgAp/wEA
+ p/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMX/BQBg/wYAJf8KDDjgIU+rAgAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAwTRWoGASb/BwAq/xEAaP8SAHD/EgBw/wYAp/8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAuP8FADT/BgAl/wQAUf8AAMX/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/BABh/wYAJf8GACf/AQCf/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAw/8FAEX/BgAl/woMOZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEB1VNAcCKf4GACX/DwBd/xIA
+ cP8SAHD/BwCk/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AAC8/wUAOv8GACX/BABR/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8EAFz/BgAl/wYA
+ J/8HAIP/CACg/wgAoP8IAKD/CQCa/wkAmP8KAJP/CwCR/wsAkf8LAJH/CgCY/wkAm/8IAKD/BwCl/wYA
+ qv8GAKj/BgCo/wYAqP8GAKr/BACy/wIAuv8CALv/AgC7/wIAvv8BAML/AQDD/wAAxf8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCy/wYAMP8GACX/Cw8+WAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAbPYoPCQgz8QYAJf8NAE7/EgBw/xIAcP8IAJ7/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMT/AQDD/wEAwf8DALP/BQA8/wYAJf8EAFH/AADF/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wQAVP8GACX/BgAn/xAAYf8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHL/EQBz/xEA
+ c/8RAHP/EQB0/xAAd/8PAH3/DQCI/wsAkv8JAJz/BgCq/wMAuP8BAML/AADF/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8CAJT/BgAl/wcCKP4JCTUZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALDz7OBgAl/woA
+ Pf8SAHD/EgBw/wkAmP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADE/wEAwf8CALv/BACx/wYApv8JAJj/DACN/w4A
+ hP8PAHz/EQB0/xEAcP8IADP/BgAl/wQAR/8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BABU/wYA
+ Jf8GACf/EABk/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBy/xAAef8NAIf/CQCa/wUArP8CALv/AQDD/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAbP8GACX/CAUu3Bcy
+ eQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgFLWsGACX/BwAu/xEAbP8SAHD/CQCY/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADF/wEAv/8FAKv/CQCc/wsA
+ j/8OAIX/EAB7/xEAdf8SAHL/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBv/wkANf8GACX/BQBE/wAA
+ xP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxP8EAEz/BgAl/wYAKP8QAGj/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBx/xEA
+ df8OAIL/CgCU/wQAsP8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMT/BQBE/wYAJf8LDz6uAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBNEPgYA
+ Jf8GACf/DwBh/xIAcP8KAJX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8BAMP/AwC1/wkAm/8OAIP/EQBz/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/CQA2/wYAJf8FAET/AADE/wAAxv8AAMb/AADG/wAAxv8AAMb/AADD/wUA
+ Sf8GACX/BgAo/xIBZ/8TAWv/EwFr/xMBa/8TAWv/EwFr/xMBa/8TAWv/EwFr/xMBa/8TAWz/EgFt/xIB
+ bf8SAW3/EgBu/xIAb/8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBx/w4Agf8HAKX/AQDB/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAsf8GAC3/BgAl/w8c
+ UW0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOGU8VCAUt+AYAJf8NAFH/EgBw/wsAkf8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAMP/BwCj/w4Agv8RAHP/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAb/8JADf/BgAl/wUA
+ RP8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMD/BQBA/wYAJf8IAST/IBYa/yAWGv8gFhr/IBYa/yAW
+ Gv8gFhr/IBYa/yAWGv8gFhr/IBYa/yAWHf8fFSD/HxUh/x8UI/8eEyf/HhIp/x0RLv8cDzb/Gw44/xoN
+ P/8ZCkf/GAhQ/xYGWf8UBGL/EwJp/xIBbv8SAG//EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcf8OAID/BQCr/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AgCO/wYAJv8HBCn8IE93JgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAICDLOBgAl/woAP/8SAHD/DACL/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwC5/w0A
+ hv8SAHH/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAW7/EwJq/xUF
+ Xv8XCFP/GQpJ/xoNPv8cEDP/HhMp/w4HI/8GACX/BQBE/wAAxP8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ vv8FADr/BgAl/wkDIv8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMa
+ Df8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoP/yIZEf8iGBP/IBYb/x4T
+ J/8cEDT/GgxC/xcIUf8UBGL/EwFs/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHH/CwCP/wIAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxf8EAF//BgAl/wgH
+ LMIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoOPIwGACX/CAAv/xEAbf8OAIT/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wIAuv8PAH3/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EwFs/xUEYP8YCUz/HA81/x8UIv8iGBT/IhkQ/yMaDv8jGg3/IxoN/yMaDf8jGg3/Dwgd/wYA
+ Jf8FAET/AADE/wAAxv8AAMb/AADG/wAAxv8AAMb/AAC7/wUAMf8GACX/FQ0n/1VELP9WRCz/VkQs/1ZE
+ LP9WRCz/VkQs/1ZELP9WRCz/VkQs/1ZELP9TQir/Tj4n/04+J/9LOyX/Rjci/0I0IP89MB3/OCwa/zYq
+ Gf8xJhb/LCIT/yceEP8mHA//JRsO/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoO/yIZEf8gFhz/HBAy/xgK
+ Sv8UBGH/EgFu/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DgCA/wIAu/8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AAC8/wUAOv8GACX/CxA1jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAESNfUwYBJv8GACf/EABh/w8AfP8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMT/DACL/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8TAWv/FwhS/xwPNP8gFR3/IhkR/yMaDf8jGg3/IxoN/yMa
+ Df8kGg3/JBsO/ygeEP8xJRX/Oi0b/0M1If8bEyX/BgAl/wUARv8AAMT/AADG/wAAxv8AAMb/AADG/wAA
+ xv8BALL/BgAr/wYAJf8jGS3/fWVF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf98ZET/e2RD/3pjQv94YUH/dl9A/2xXOv9mUjb/XUsx/1JB
+ Kv9FNiL/OSwa/yshEv8kGw7/IxoN/yMaDf8jGg3/IxoN/yIZEv8fFCP/Gg0//xUFXf8SAG//EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/DgCD/wEAvv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAKD/BgAo/wYB
+ Jv8OGT5DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaPIgMCAYv7wYAJf8MAE3/EAB4/wEA
+ wP8AAMb/AADG/wAAxv8AAMb/AADG/wUArf8SAHH/EgBw/xIAcP8SAHD/EgBw/xMCaf8XB1X/HBA0/yIY
+ Ff8jGg7/IxoN/yMaDf8kGw7/KB4Q/zQpGP9FNiL/VUQs/2VSNv9yXD7/emJC/3tkQ/99ZUT/fmZF/ysg
+ L/8GACX/BABW/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wEAo/8GACn/BgAl/zQqNv+Re1b/kXxW/5F8
+ Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+PeVT/jHZS/4x2Uv+MdlH/iHJO/4Rt
+ Sv+Cakj/gWlI/4BoR/9/Z0b/fmZF/35mRf9+ZkX/fmZF/35mRf99ZUT/emND/29aPP9cSjD/SDkk/zYq
+ GP8oHhD/IxoN/yMaDf8jGg3/IxkP/x8UJP8ZC0b/FANj/xIAb/8SAHD/EgBw/xIAcP8SAHD/DACO/wAA
+ xf8AAMb/AADG/wAAxv8AAMb/AADG/wMAdP8GACX/CQku7hQuVBMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAIBzGuBgAl/wkAOf8RAHP/AwC6/wAAxv8AAMb/AADG/wAAxv8AAMb/DACO/xIA
+ cP8SAHD/EgBw/xMBav8aDED/IBYb/yMaD/8jGg3/IxoN/yQbDf8wJRX/Sjol/2BNM/9yXD7/e2ND/35m
+ Rf9+ZkX/f2dF/4FqSP+Jck//kHpV/5mFXf+ij2X/MSg3/wYAJf8DAGb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AgCT/wYAJ/8GACX/ZFxg/+DVrP/e1Kr/18uc/8/Cjv/OwYz/0sWT/9vQo//d0qf/49mx/+Ta
+ sv/g1az/2s+j/9bKm//OwY7/zL+L/8y/i//LvYr/ybyI/8a3hf/BsoH/vK18/7eneP+yoXP/qphs/6OP
+ Zf+ZhV3/kHpV/4hxTv+AaEf/fmZF/35mRf9+ZkX/e2ND/3FcPf9bSTD/QTMf/ykfEP8jGg3/IxoN/yMa
+ Df8iGBX/HA82/xQDZf8SAHD/EgBw/xIAcP8RAHX/AgC7/wAAxv8AAMb/AADG/wAAxv8AAMT/BQBG/wYA
+ Jf8NFju6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkKNmgGACX/BwAq/xAA
+ aP8EALL/AADG/wAAxv8AAMb/AADG/wEAwf8PAH3/EgBw/xIAcP8VBVz/IBYc/yMaDf8jGg3/JRwO/zMn
+ F/9OPij/bFc6/3tjQ/9+ZkX/gGhH/4hxTv+WgVr/p5Rp/7OidP/BsoH/y72K/87Bjv/QxJD/1ciW/9XI
+ l/81Lj//BgAl/wMAbP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAID/BgAl/wYAJf98c2n/28+h/9rP
+ of/Zzp//1MiV/9HEj//bz6H/3tOn/97Tp//Zzp//1sqa/9PHlP/TxpP/0sWS/9DEj//Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/PwY3/zb+L/8q8iP++r37/tKR1/6iV
+ av+Wglv/hW9M/39nRv99ZUX/dmBA/19MMv9AMh//KiAR/yMaDf8jGg3/IRYa/xYGWP8SAHD/EgBw/xIA
+ cf8FAK3/AADG/wAAxv8AAMb/AADG/wEAqv8GACz/BgAl/xMpT2oAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAADRdLKAcDKv0GACX/DgBX/wYAp/8AAMb/AADG/wAAxv8AAMb/AgC9/xEA
+ df8SAHD/FANk/yEWGv8jGg3/JRwO/0EzH/9pVTj/emND/35mRf+AaEb/kXxW/6yabv++rn7/y76K/8/C
+ jf/SxZL/4NWq/+feuP/p4Lz/6+G+/+jeuf/l27T/3dKp/zIrQP8GACX/AwB0/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wMAav8GACX/BgAl/5GGbf/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0sWR/9PGk//KvIj/tKN1/5N9V/+AaEf/fmZF/4hy
+ Uv+FclX/TT0n/ycdD/8jGg3/IRcY/xQDYv8SAHD/EgBx/wYAqf8AAMb/AADG/wAAxv8AAMb/AwB7/wYA
+ Jf8IBiv8FSxSDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdRZkECgw52QYA
+ Jf8KAEH/CQCa/wAAxv8AAMb/AADG/wAAxv8CALz/EQBz/xIAcP8bDjv/IxoN/yYdD/9bSS//gWpJ/4Bo
+ SP9+ZkX/loFa/72tff/SxpT/3dKl/9TIlv/p4Lv/18ua/9LFkf/VyJb/08aT/9HFkf/Qw47/0MOO/9DD
+ jv/JvIr/KCE3/wYAJf8DAHr/AADG/wAAxv8AAMb/AADG/wAAxv8AAMX/BABT/wYAJf8NBij/p5x5/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/d0qX/8+zN/+Xbs//Qw4//0MOR/7urfP+Re1b/gGhH/7Cggv+qmHv/YE0z/ycdD/8jGg3/Gw44/xIA
+ cP8SAHH/BQCu/wAAxv8AAMb/AADG/wAAw/8EAEz/BgAl/woNMsIiWYABAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANFkuLBgAl/wcALf8LAIn/AADF/wAAxv8AAMb/AADG/wIA
+ vf8RAHX/EgBu/yAVIf8jGg3/QjQg/4BpSP+ciWr/f2dG/6CMY//OwY3/4des//rz2P/y6sv/1MeV/9fL
+ m//RxJD/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/8K2h/8eFzL/BgAl/wIAhf8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAwP8FAED/BgAl/xYPLf+9sYT/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/SxZL/08eU/9HFkf/z7M7/8OjH/8/C
+ kP+gjGP/gGhH/4p0VP+EbEz/RTYi/yMaDf8gFSD/EgFu/xEAc/8CALr/AADG/wAAxv8AAMb/AQCt/wYA
+ Lf8GACX/DBQ5dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwT
+ RBMGASf/BgAm/wsAa/8AAMX/AADG/wAAxv8AAMb/AQDA/xAAev8TAWv/IRYa/yMaDf9UQyv/gWlI/4Zv
+ Tv+HcE3/ybuI/+HXrP/999//6uG9/9PGk//Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/u6+D/xIMK/8GACX/AgCR/wAAxv8AAMb/AADG/wAAxv8AAMb/AAC6/wUAMv8GACX/LSY5/8m8
+ iv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/+DVq//++OD/5Nqy/8i6h/+EbUr/hm9P/72ukf9UQyz/IxoN/yEW
+ Gv8TAWv/DwB9/wEAwv8AAMb/AADG/wAAxv8CAH7/BgAl/wgHLPoYOmArAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBFBAAcELL0GACX/CQBK/wEAv/8AAMb/AADG/wAA
+ xv8AAMX/DgCE/xIBbv8gFSD/IxoN/08/KP+yoYT/gmtK/5WAWv/SxZL/+vPY/+nhvP/RxI//0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv+uo33/CwUo/wYAJf8BAJ//AADG/wAA
+ xv8AAMb/AADG/wAAxv8BAKv/BgAp/wYAJf8/NT3/rJtu/62cb/+tnG//rZxv/62cb/+tnG//rZxv/62c
+ b/+tnG//rZxv/62cb/+tnG//rZxv/62cb/+yoXP/tKN1/7Sjdf+0o3X/t6d4/72tff/Cs4H/x7iF/8y/
+ i//OwYz/z8KN/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0cSP//Pr
+ zP/17tD/z8KO/454U/9+Zkb/g2xM/04+J/8jGg3/IBUh/xIAbv8LAI7/AADG/wAAxv8AAMb/AADD/wQA
+ Sv8GACX/DRQ6yCltlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAACxA/eAYAJv8HADH/AwCy/wAAxv8AAMb/AADG/wAAxv8KAJX/EgBw/x0RL/8jGg3/PzEe/7yt
+ kf+Qe1v/loFa/9jMnf/89dz/1cmY/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Pwo3/zL+K/8S2
+ g/+9rn3/taV2/31vWv8IAib/BgAr/wEAq/8AAMb/AADG/wAAxv8AAMb/AADG/wIAjf8GACb/BgAl/0c4
+ Nv9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/f2dG/4FqSP+DbEr/h3BN/496VP+ZhV3/p5Rp/7emeP/DtIL/zcCM/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/5964//v02v/Qw5D/jHZR/5J9Xv+Bakr/PTAd/yMa
+ Df8dEDH/EgBw/wcApf8AAMb/AADG/wAAxv8BAKf/BgAq/wYAJf8OGj9sAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATJmUlCAUu+gYAJv8CAJT/AADG/wAA
+ xv8AAMb/AADG/wUArv8SAHH/GQpI/yMaDf8tIhP/jHhb/52Jav+MdVH/08aW//v02v/VyZf/0MOO/9DD
+ jv/Qw47/zsGN/8O0gv+xoHL/oY1k/5J9V/+Gb0z/gmpI/35nRf9+ZkX/Szs3/wYAJf8FADL/AQC2/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AwBk/wYAJf8IAib/Y08+/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/4FpSP+IcU7/mIRc/6iVav+6qnr/y76K/9DDjv/Qw47/0MOO/9DD
+ jv/m3bb/+fLX/8q9i/+EbEr/o5By/3ljRP8rIRL/IxoO/xgKSv8RAHT/AgC7/wAAxv8AAMb/AADG/wMA
+ dP8GACX/BwUq/BUwVhwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAACFOqAEKDDq/BgAl/wQAY/8AAMX/AADG/wAAxv8AAMb/AQDB/w8AfP8UBGD/IhkR/yQb
+ Dv9kUTb/jnhY/4FpR//BsoH/6+K//9bKmf/Qw47/yLmH/6WSZ/+Nd1L/gmpI/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf84KzL/BgAl/wUAN/8AAL7/AADG/wAAxv8AAMb/AADG/wAAuf8FADf/BgAl/xgP
+ Kv97ZET/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/39nRv+KdFD/p5Rp/8e5hv/Qw47/0MOO/+rhvP/w6Mf/tqZ3/39nRv+EbUz/YU4z/yQb
+ Df8iGBP/FANj/w0Ahv8AAMX/AADG/wAAxv8AAML/BQBB/wYAJf8LDzXCIE92AQAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgHMUgGACX/BQAz/wEA
+ tv8AAMb/AADG/wAAxv8AAMb/CQCZ/xIBbv8fFCP/IxoN/0Y3I/9+ZkX/fmZF/6KPZf/RxI//0MOO/8W3
+ hf+MdlL/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fWVF/yMZLf8GACX/BQBC/wAA
+ wv8AAMb/AADG/wAAxv8AAMb/AwB5/wYAJf8GACX/RDU1/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/iHJO/7en
+ eP/Qw47/3tOn/9rOof+Xglv/fmZF/35mRf9DNSH/IxoN/x8TJv8SAG//BgCo/wAAxv8AAMb/AADG/wEA
+ n/8GACj/BgEm/xEkSWcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAADBNEDwcELPYGACX/AgCF/wAAxv8AAMb/AADG/wAAxv8DALn/EQB0/xoM
+ Qv8jGg3/KyES/3dgQf9+ZkX/hm9M/8q8if/Pwo7/nYph/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf93YEP/Ewso/wYAJf8FAD//AADC/wAAxv8AAMb/AADG/wEAnP8FADH/BgAl/xcP
+ Kf9xW0L/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/g2tJ/7qre//Qw47/u6x8/4BpR/9+ZkX/dF4//ykf
+ Ef8jGg3/GQtG/w4Agf8BAMP/AADG/wAAxv8AAMX/AwBl/wYAJf8JCS7rIVR7FQAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQo1vgYA
+ Jf8EAEv/AADE/wAAxv8AAMb/AADG/wAAxf8MAI3/FANi/yIZEP8jGg3/W0kw/35mRf9+ZkX/qZdr/8W2
+ hP+Ca0n/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/3ReQv8OByf/BgAl/wYA
+ L/8CAIT/AgCV/wIAlP8DAHD/BgAw/wYAJf8PCCf/ZFA+/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/jXdS/8u+iv+Uf1n/fmZF/35mRf9XRS3/IxoN/yIZEv8UA2b/BwCj/wAAxv8AAMb/AADG/wAA
+ uP8FADb/BgAl/woLMJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARIFpeBgEn/wYAKf8BAKX/AADG/wAAxv8AAMb/AADG/wQA
+ tP8RAHP/HhIr/yMaDf85LBr/fGRE/35mRf+AaEf/jHZR/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fWVF/zgrMv8HASX/BgAl/wYAJf8GACX/BgAl/wYAJf8GACX/EAkn/15L
+ PP9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/jnhT/39nRv9+ZkX/e2ND/zUp
+ GP8jGg3/HRAx/w8Ae/8BAL//AADG/wAAxv8AAMb/AgCJ/wYAJv8HAif/ECBGPwAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk7
+ hw8IBi/dBgAl/wMAZ/8AAMX/AADG/wAAxv8AAMb/AADF/wwAjv8XB1P/IxoP/yYcD/9oVDj/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/emJE/0s7
+ N/8ZECr/DQYn/woEJv8KBCb/Egoo/zIlMP9oVD//fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9lUTb/JRwO/yIZEP8WBlf/BwCh/wAAxv8AAMb/AADG/wAA
+ xf8EAE3/BgAl/wsRNtgcRWsHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkKMHQGACX/BQAz/wEAtP8AAMb/AADG/wAA
+ xv8AAMb/AwC4/xIBcP8fFCH/IxoN/0I0IP9+ZkX/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39n
+ Rv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/3tkRf9uWEH/aVRA/2pVQP92X0P/f2dG/39n
+ Rv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39n
+ Rv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39n
+ Rv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/fWVF/z4w
+ Hf8jGg3/HxMl/w8BfP8BAMH/AADG/wAAxv8AAMb/AQCl/wYAKv8GACX/EiRJdQAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAESFHHggHLPgGACX/AgCA/wAAxv8AAMb/AADG/wAAxv8AAMb/CACd/xgKSv8jGg7/Jx4Q/29b
+ Pf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4Jr
+ Sf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4Jr
+ Sf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4Jr
+ Sf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4Jr
+ Sf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf9sWDv/Jh0P/yMaDv8XCVD/BgCp/wAAxv8AAMb/AADG/wAA
+ xf8DAGf/BgAl/wgHLPQWM1kFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADRU6tgYAJf8FAD//AADA/wAA
+ xv8AAMb/AADG/wAAxv8BAMH/DwF8/yAWHv8jGg3/Rjgj/4VuS/+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4Zv
+ TP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4Zv
+ TP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4Zv
+ TP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4Zv
+ TP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hW5L/0I0
+ IP8jGg3/HxQi/w4Bg/8BAMP/AADG/wAAxv8AAMb/AAC2/wUANf8GACX/CQgukQAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAXNltABgEm/wYAJ/8CAI3/AADG/wAAxv8AAMb/AADG/wAAxv8EALD/GApM/yMa
+ Dv8mHA//b1w+/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pz
+ T/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pz
+ T/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pz
+ T/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pz
+ T/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/9pVzr/JRsO/yMaDv8XCVD/BACz/wAAxv8AAMb/AADG/wAA
+ xv8CAIH/BgAl/wgFKvwUKk84AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJCC2/BgAl/wUA
+ S/8AAMH/AADG/wAAxv8AAMb/AADG/wAAxf8MAoz/IBYb/yMaDf8+Mh//inVR/454U/+OeFP/jnhT/454
+ U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454
+ U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454
+ U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454
+ U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/lH9Y/495VP+OeFP/iHNP/zkt
+ G/8jGg3/IBYc/wsCjf8AAMX/AADG/wAAxv8AAMb/AADA/wUAQ/8GACX/DBM4wSdjiQMAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8cQVgHAif/BgAo/wIAmP8AAMb/AADG/wAAxv8AAMb/AADG/wEA
+ vv8WClb/IxoO/yUcDv9tXD7/kXxW/6KPZf+hjmX/lH9Z/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8
+ Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8
+ Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8
+ Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8
+ Vv+VgFr/ppNo/7ysfP+1pXb/kn1X/5F8Vv9lVDn/JBsO/yMaDv8WClT/AgC9/wAAxv8AAMb/AADG/wAA
+ xv8CAJH/BgAn/wYAJf8PG0BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHktxCgsQ
+ NtkGACX/BABS/wAAw/8AAMb/AADG/wAAxv8AAMb/AADG/wYBpv8fFCX/IxoN/zgsG/+Qe1b/moZd/8Kz
+ gf/GuIX/tKR1/6OQZv+ZhV3/loFa/5WBWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WA
+ Wv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WA
+ Wv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WA
+ Wv+VgFr/lYBa/5WAWv+VgFr/lYFa/5iDXP+ij2X/tqZ3/8i6h//Qw47/yryI/52JYP+VgFr/i3hT/zEm
+ Fv8jGg3/HxQk/wcBof8AAMb/AADG/wAAxv8AAMb/AADD/wQAT/8GACX/CQkw1hMmTAQAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBI3UQYAJf8GACj/AQCZ/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADF/xAHdP8jGg//IxoN/2JTOP+ZhV3/oo9l/8q8if/Qw47/0MOO/8q8iP+9rn3/rZxv/6CN
+ Y/+ahl7/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mF
+ Xf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mF
+ Xf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/moVe/6GOZP+zonT/xreF/8/C
+ jf/Qw47/0sWS/8/Cjf+unHD/mYVd/5iEXf9YSTH/IxoN/yMaDv8RB27/AADE/wAAxv8AAMb/AADG/wAA
+ xv8BAKD/BgAp/wYBJv8OF0t4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAULFIFCgwx3gYAJf8EAFD/AADC/wAAxv8AAMb/AADG/wAAxv8AAMb/AgC7/xsROf8jGg3/LyUW/416
+ Vf+diWD/q5pt/83Ai//Qw47/0MOO/9DDjv/Qw47/zb+L/8S1g/+3p3j/rJpu/6ORZ/+fjGL/nYlh/52J
+ Yf+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlg/52J
+ YP+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlh/56L
+ Yv+ij2X/qJZr/7Khc//Cs4L/zcCL/9DDjv/Qw47/0MOO/9/Uqf/k2rH/wLB//56LYv+diWD/hHJP/yoh
+ Ev8jGg3/GxE1/wMAuP8AAMb/AADG/wAAxv8AAMb/AADD/wQAWv8GACX/CAQs1RYxdw8AAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARIkdqBgEm/wYAKP8CAJf/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/CQSX/yEYF/8jGg3/U0Yu/5+MY/+hjmT/tKN1/8/Cjv/bz6H/0cSQ/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/zsGM/8m7iP/Cs4L/u6t7/7ald/+xoHL/rZtv/6iWav+mk2j/pJFm/6KP
+ Zf+ij2X/oY5k/6GNZP+hjWT/oY1k/6GNZP+hjWT/oY1k/6GNZP+hjWT/oY1k/6GNZP+hjWT/oY1k/6GN
+ ZP+hjWT/oY5k/6OQZv+mlGj/q5lt/7Wkdf+/sH//x7mG/82/i//Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/az6H/+vPZ/9rOov+olWr/oY1k/52KYf9GOiX/IxoN/yEYGv8IBJv/AADG/wAAxv8AAMb/AADG/wAA
+ xv8BAKf/BgAs/wYAJf8KDjt3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAACNYfgkKCzDNBgAl/wUASP8AAL7/AADG/wAAxv8AAMb/AADG/wAAxv8AAMP/Egxl/yMa
+ Df8mHQ//gXFP/6SRZ/+kkmf/uqp7/93Spv/l27T/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MKO/87AjP/Lvor/ybyI/8m8iP/Iuof/x7mG/8a3hf/EtYP/w7SD/8Cx
+ gP/AsYD/wLGA/8CxgP/AsYD/wLGA/8CxgP/AsYD/wbKB/8S1g//Iuof/yr2J/87BjP/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/08aT//fv0//n3rj/t6d5/6SRZ/+kkWf/cWJD/yQb
+ Dv8jGg3/EQ1q/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wQAYf8GACX/CAYv8g4ZUBEAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0VOk0HAif/BgAn/wIA
+ if8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAbz/HBUv/yMaDf8+MyD/oZBm/6iWav+pl2z/wbKB/+PZ
+ sf/k2rL/0cSQ/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9XI
+ l//d0qX/2Myc/8W2hP+qmGz/qJZq/5mIYP8zKBj/IxoN/xwVMP8CAbz/AADG/wAAxv8AAMb/AADG/wAA
+ xv8BAKv/BgAs/wYAJf8NFUiSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAHERpBwwUOc0GACX/BQA7/wAAu/8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8KB5H/IhkT/yQbDf9hVDn/q5lt/6yabv+unG//xbaE/+XbtP/v58b/2Myd/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0sWS/9bKmf/l27P/+vTa//fw1P/NwI3/sJ9y/6yabv+pmGz/VUgw/yMa
+ Df8iGRT/CQeU/wAAxv8AAMb/AADG/wAAxv8AAMb/AADD/wQAXv8GACX/BwMq6hImZQ8AAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFi9VOQcC
+ J/cGACb/AwB0/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wEAw/8XEU3/IxoN/yogEv+LfFf/sJ5x/7Ce
+ cf+xoHP/v7B//9jMn//o37n/4das/9HEkP/h163/6uG8/9vPof/Qw47/0MOO/9PHlP/Xy5v/3NGj/9/U
+ qP/h163/49mx/93Spv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/RxI//3NCj/+HXrf/Wypj/0MOO/+DWq//38NX//Pbd//33
+ 3v/w6Mj/0MOV/7emd/+wnnH/sJ5x/4N0Uf8nHhD/IxoO/xQPWf8AAMP/AADG/wAAxv8AAMb/AADG/wAA
+ xv8BAKP/BgAs/wYAJv8JCzd8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACg0ymQYAJf8GADH/AQCp/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wQDsP8eFiX/IxoN/zkuHf+hkWf/tKN1/7Sjdf+0o3X/uKh4/8O0gv/KvIn/zcCM/9DE
+ kf/Tx5b/0cSS/87BjP/PwY3/1MeW/9bKmv/Wypr/1sqa/9bKmv/VyZr/0cSR/87BjP/OwYz/zsGM/87B
+ jP/OwYz/zsGM/87BjP/OwYz/zsGM/87BjP/OwYz/zsGM/87BjP/OwYz/zsGM/87BjP/OwYz/zsGM/8/C
+ jv/azqH/29Ck/9PGlf/OwYz/0cSS/9rOo//ZzaP/zsCU/7ytff+0o3X/tKN1/7OidP95a0v/MCYW/yMa
+ Df8dFS7/AwK3/wAAxv8AAMb/AADG/wAAxv8AAMb/AADE/wQAVv8GACX/CQkz7BIkYRMAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAZOmAjCgsw7gYAJf8EAFX/AADD/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wkHlf8hGRf/IxoN/zwx
+ H/+WiGD/t6d4/7eneP+3p3j/t6d4/7ioef+5qXr/uap6/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qr
+ e/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qr
+ e/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6qnv/ual5/7io
+ ef+3p3j/t6d4/7eneP+2pnf/kYJc/y8lFf8jGg3/IBgc/wcFnv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8CAJT/BgAo/wYAJf8KDTtbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOGj9bBgAl/wYAJ/8CAIr/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADF/w0Kf/8iGRT/IxoN/ysiE/9mWj3/iHtW/5mLY/+ll2v/qptv/66f
+ cv+xonT/tKR2/7eoeP+3qHj/uqp6/7use/+7rHv/u6x7/7use/+7rHv/u6x7/7use/+7rHv/u6x7/7us
+ e/+7rHv/u6x7/7use/+7rHv/u6x7/7use/+7rHv/u6x7/7use/+7rHv/u6x7/7use/+7rHv/u6x7/7us
+ e/+7q3v/t6h5/7Okdf+zo3X/sqN1/6+fcv+tnnH/qptv/6WXa/+ajGP/jH5Z/19UOf8rIhP/IxoN/yEY
+ Gf8LCIz/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AAC+/wUARv8GACX/CAYv1woOPAEAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAABEkSQEKDTLBBgAl/wUANv8BALH/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADF/wwJ
+ h/8gFx//IxoN/yMaDf8jGg3/JBoN/yQbDv8kGw7/KyIT/zEnF/82LBv/PDIf/zwyH/9COCP/Rjwn/1FH
+ L/9SRy//Ukcv/1JHL/9SRy//Ukcv/1JHL/9SRy//VEkw/19UOP9gVTn/YFU5/2BVOf9fVDn/VEkx/1JH
+ L/9SRy//Ukcv/1JHL/9SRy//SD0o/0Q6Jf9EOiX/RDol/0Q6Jf89MyD/NSsa/zQqGv8zKhn/LCMU/ykg
+ Ef8kGw7/JBsO/yQbDf8jGg3/IxoN/yMaDf8eFib/CQaX/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8CAH7/BgAm/wcCKf0OG1JVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABc0Wi0ICC3wBgAl/wQA
+ Wv8AAMH/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wUEq/8VEFX/HhYn/yEYGv8iGRP/IxoP/yMa
+ Dv8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMa
+ Df8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMa
+ Df8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDv8jGg//IhkT/yEYGP8eFyX/Eg1l/wMC
+ tP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCt/wUAM/8GACX/Cgs4sxo7iAMAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsQNVYHAif/BgAn/wIAg/8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AwK1/wYEp/8IBpv/CQeV/wwJhv8OC3n/Dwt2/xENaf8SDWj/FA9b/xUP
+ V/8XEUv/FxFK/xcRSv8XEUr/FxFK/xcRSv8XEUr/FxFK/xcRSv8ZEz//GhM9/xoTPf8aEz3/GhM+/xcR
+ SP8XEUr/FxFK/xcRSv8XEUr/FxFK/xUQVf8VD1j/FQ9Y/xUPWP8VD1n/Eg1n/w8Ld/8OC3j/Dgt4/wwJ
+ gv8LCIr/CQaY/wgGnP8GBKb/BAOz/wAAxP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ w/8EAFr/BgAl/wcELPIKDDocAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHkhuAw4Z
+ PrMGACX/BQAw/wEAoP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxP8AAMT/AADE/wAAxP8AAMT/AADE/wAA
+ xP8AAMT/AADE/wEAw/8BAMP/AQDD/wEAw/8BAMP/AADE/wAAxP8AAMT/AADE/wAAxP8AAMT/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AgCK/wYAKP8GACb/DRVJgwAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHkpwFwkJL98GACX/BQA9/wEAsv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEA
+ r/8FADf/BgAl/wkIM8cYNX4IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAEihPSwcFLPoGACX/BABM/wAAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMH/BABT/wYAJf8IBS33EB1VNwAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiVKkBDxtSfgYBJ/8GACX/BABc/wAA
+ v/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADF/wMA
+ ev8GACb/BgAm/w4YTX4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAACw89lgYBJv8GACb/AwBo/wAAwf8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAJn/BgAs/wYAJf8JCjW2AAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQHlYECAcxmAYA
+ Jf8GACf/AwBv/wAAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCq/wUA
+ OP8GACX/CQk05RMrbSMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBi8BCgs4twYAJf8GACf/AwBt/wAAwf8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAvP8FAEf/BgAl/wcDKvEOFkk1AAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAATJ2cSCgw5xwYAJf8GACf/AwBo/wAAwP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAL//AwBe/wYA
+ Jv8HAin6DhpPYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASJWQSCw49xgYAJf8GACb/BABd/wAA
+ uv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADA/wMAYf8GACb/BwMp/g0URmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAQIFoDCAYumQYBJv8GACX/BABI/wEAq/8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAvf8DAF//BgAl/wYB
+ J/0LDj1wESJeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQHlYEDBJCkwYB
+ J/8GACX/BQA2/wIAkv8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AALj/BABV/wYAJv8HAij+Cg48dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBFCZgcELPcGACX/BgAp/wMAav8AAL3/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCn/wUARP8GACX/BwQr/AwT
+ RGoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAADxtSPAgHMN4GACX/BgAm/wQAS/8BAKP/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADD/wIAi/8FADT/BgAl/wcDKu8PGlBdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADhdLCwsQQKAHAyv9BgAl/wYA
+ Lv8DAGz/AQC1/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAtP8EAGH/BgAo/wYAJf8JCjXkDxtSNAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAFS1xAQ0WSjwIBS7SBgEm/wYAJf8FADb/AgB7/wAAt/8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ vv8CAIb/BQA2/wYAJf8HAij9CAcxnA4bUhcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABcy
+ ehEOGE2DCAcw9gYAJf8GACb/BQA4/wMAcf8BALD/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAwf8CAI3/BQBG/wYAJ/8GACX/CAcx8g4ZT2YZN4ECAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxBkgANFkoOCw48mAgGL/cGACX/BgAl/wYA
+ K/8EAFb/AgCN/wAAtv8AAMP/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADD/wEAsf8CAID/BQBA/wYA
+ Jv8GACX/CAcs+QkLMYMSJmEaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAgTacBFSxvLAwRQYwIBS7nBgAm/wYAJf8GACb/BQA0/wQAU/8CAH//AQCm/wAA
+ wP8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMT/AAC2/wIAj/8EAFb/BQAw/wYAJf8GACX/BwMo+wsPNKMWM1k0J2aMAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAOGE1YCw8+uwgGL/oGACb/BgAl/wYAJf8GACn/BQA0/wQATP8DAHD/AgCH/wIAm/8BAKj/AACy/wAA
+ tP8AALT/AAC0/wAAtP8BALD/AQCn/wIAl/8CAIL/AwBp/wUAR/8GAC7/BgAm/wYAJf8GASb/Cgwx7A8b
+ QZMRIkgdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYNn8AEiRhHQoNOl8JCDKsCAYv5wYB
+ JvEGACX/BgAl/wYAJf8GACX/BgAn/wYAKv8GAC3/BgAt/wYALf8GAC3/BgAt/wYALP8GACr/BgAm/wYA
+ Jf8GACX/BgAl/wYBJv8HAifvCQkutRAfRFwXNlsOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFzNZFBImTGsPHUKnDBE33wkLMPsHAyj/BwIn/wYB
+ Jv8GASb/BgEm/wYBJv8GASb/BwEm/wcDKP8HBCn/CxA27gsQNcINFTuHFS9UVhxDaAoAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAgUXcFEyhNCgoNMgsOGT4TFCtQTRQrUE4UK1BOFCtQThQrUE4YNlxDHENpHQsS
+ NwsbQmgIK22VAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//////////////////4P/////////wAAAAf/////B////////w
+ AAAAAAB////4P//////wAAAAAAAAAD///B//////AAAAAAAAAAAD//8P////wAAAAAAAAAAAAA//h///
+ /AAAAAAAAAAAAAAA/8P//+AAAAAAAAAAAAAAAB/h//4AAAAAAAAAAAAAAAAA8P/4AAAAAAAAAAAAAAAA
+ ADj/gAAAAAAAAAAAAAAAAAA4/gAAAAAAAAAAAAAAAAAAPPwAAAAAAAAAAAAAAAAAAD78AAAAAAAAAAAA
+ AAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAA
+ AAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wA
+ AAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAA
+ AD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAA
+ AAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAA
+ AAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wA
+ AAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAA
+ AD/8AAAAAAAAAAAAAAAAAAB//AAAAAAAAAAAAAAAAAAAf/wAAAAAAAAAAAAAAAAAAH/+AAAAAAAAAAAA
+ AAAAAAB//gAAAAAAAAAAAAAAAAAAf/4AAAAAAAAAAAAAAAAAAH/+AAAAAAAAAAAAAAAAAAB//gAAAAAA
+ AAAAAAAAAAAA//4AAAAAAAAAAAAAAAAAAP/+AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAAAAAAAAA//8A
+ AAAAAAAAAAAAAAAAAP//AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAAAAAAAAB//8AAAAAAAAAAAAAAAAA
+ Af//AAAAAAAAAAAAAAAAAAH//wAAAAAAAAAAAAAAAAAB//+AAAAAAAAAAAAAAAAAAf//gAAAAAAAAAAA
+ AAAAAAH//4AAAAAAAAAAAAAAAAAD//+AAAAAAAAAAAAAAAAAA///gAAAAAAAAAAAAAAAAAP//8AAAAAA
+ AAAAAAAAAAAD///AAAAAAAAAAAAAAAAAB///wAAAAAAAAAAAAAAAAAf//8AAAAAAAAAAAAAAAAAH///g
+ AAAAAAAAAAAAAAAAD///4AAAAAAAAAAAAAAAAA///+AAAAAAAAAAAAAAAAAP///gAAAAAAAAAAAAAAAA
+ D///8AAAAAAAAAAAAAAAAB////AAAAAAAAAAAAAAAAAf///wAAAAAAAAAAAAAAAAH///8AAAAAAAAAAA
+ AAAAAB////gAAAAAAAAAAAAAAAA////4AAAAAAAAAAAAAAAAP///+AAAAAAAAAAAAAAAAD////wAAAAA
+ AAAAAAAAAAB////8AAAAAAAAAAAAAAAAf////AAAAAAAAAAAAAAAAH////4AAAAAAAAAAAAAAAD////+
+ AAAAAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAf////8AAAAAAAAAAAAAAAH/////AAAAAAAAAAAAAAAB
+ /////4AAAAAAAAAAAAAAA/////+AAAAAAAAAAAAAAAP/////wAAAAAAAAAAAAAAH/////8AAAAAAAAAA
+ AAAAB//////gAAAAAAAAAAAAAAf/////4AAAAAAAAAAAAAAP/////+AAAAAAAAAAAAAAD//////wAAAA
+ AAAAAAAAAB//////8AAAAAAAAAAAAAAf//////gAAAAAAAAAAAAAP//////4AAAAAAAAAAAAAD//////
+ /AAAAAAAAAAAAAB///////wAAAAAAAAAAAAAf//////+AAAAAAAAAAAAAP///////wAAAAAAAAAAAAD/
+ //////8AAAAAAAAAAAAB////////gAAAAAAAAAAAAf///////4AAAAAAAAAAAAP////////AAAAAAAAA
+ AAAD////////4AAAAAAAAAAAB////////+AAAAAAAAAAAA/////////wAAAAAAAAAAAP////////+AAA
+ AAAAAAAAH/////////gAAAAAAAAAAD/////////+AAAAAAAAAAB//////////gAAAAAAAAAAf///////
+ //8AAAAAAAAAAP//////////gAAAAAAAAAH//////////8AAAAAAAAAD///////////gAAAAAAAAA///
+ ////////8AAAAAAAAA////////////wAAAAAAAAf///////////+AAAAAAAAP////////////wAAAAAA
+ AH////////////+AAAAAAAD/////////////4AAAAAAB//////////////AAAAAAB//////////////8
+ AAAAAA///////////////8AAAAB////////////////gAAAB/////////////////wAAD///////////
+ ///////gAH//////////////////////////////KAAAAEAAAACAAAAAAQAgAAAAAAAAQAAAAAAAAAAA
+ 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
+ LV0HAC7/EABi/xIAcP8SAHD/CgCX/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8EAF7/CAAz/woAQP8GACn/BABd/wAAuf8AAMb/AADG/wAAxf8CALr/AADG/wMA
+ uf8BAMP/AADG/wAAxv8AAMH/BQBM/w4AVv8MAH//BwAs/wIAif8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/DACM/xIAcP8SAG//BwAr/wgHMTwAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAHAic6BwAq/w8AYf8SAHD/EgBw/wYAqv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwB2/wcALf8OAFb/DQBV/wkANv8FADf/AgCW/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMT/BABi/wsARv8NAIn/AwB0/wUARf8AAMT/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wYAp/8SAHD/EQBq/wYB
+ J/8KDDkZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADRc9IQYAJ/8PAF3/EgBw/xIAcf8CALv/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wIAhf8HACn/EQBt/xIA
+ cP8SAG//DQBP/wYAKv8DAGb/AAC9/wAAxv8AAMb/AADG/wAAxv8AAMX/AwBy/wgAMv8OAHz/AgC6/wUA
+ Nv8CAIv/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8CAL7/EgBy/w8AYP8HBCv0Fi9zBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQsURAHAyj7DgBY/xIA
+ cP8RAHb/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8CAJH/BgAn/xAAc/8GAKr/CACd/xAAef8NAFP/BgAl/wUAPP8BAJ7/AADG/wAAxv8AAMD/AwBo/wYA
+ Jv8KAFj/AgC9/wIAlv8FADH/AAC+/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/w8Aff8OAFX/CAcx1wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAACQgt6w0AUf8SAHD/DwCA/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AQCb/wYAJf8IAJH/AADG/wAAxv8CALr/CgBK/wUANf8DAGv/BgAq/wMA
+ b/8BAJ7/BQBG/wQAY/8FAD7/AwCB/wAAxv8EAF7/BABg/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8MAI3/DABI/wcELJ4AAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkJLsELAEj/EgBw/w0Aif8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEApv8GACX/BQCY/wAAxv8AAMb/AADG/wQA
+ Uv8DAGX/AADG/wEApP8EAFL/BQBA/wIAkP8AAMb/BABi/wQAS/8CAJL/BgAt/wEAnv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/CQCa/wkA
+ OP8HAilsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBSqOCgA+/xIAcP8MAI3/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BALD/BgAm/wIA
+ mP8AAMb/AADG/wAAxv8EAFD/AwB4/wAAxv8AAMb/AADG/wAAxf8AAMb/AADG/wAAt/8EAE//BQA1/wMA
+ b/8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wcAov8HACz/CQgyQwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQkwawgA
+ M/8SAHD/DACO/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AAC6/wYAJ/8CAI7/AADG/wAAxv8AAMb/BABO/wMAcf8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADF/wAAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8FAJ7/BwIo/hEgWxoAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAoMOTwHACr/EgBu/wwAjf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAv/8GACv/AgCL/wAAxv8AAMb/AADG/wUARv8EAGX/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AgCL/wgFLd0hT6sBAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASJGERBwMp+xAAY/8NAIn/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxf8BAL3/BgAw/wIAi/8AAMb/AADG/wAA
+ xv8FAD//CQBM/w0AiP8NAIf/DgCD/w4AgP8OAIL/DQCH/wwAjP8MAIz/DACN/woAlP8KAJf/CQCb/wcA
+ o/8FAK//AgC8/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMA
+ Z/8HAyqcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcELM4NAFL/DgCE/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMT/AwC1/wYAqP8JAJv/DACO/w4Agf8QAHj/EgBx/wcA
+ LP8CAIX/AADG/wAAxv8AAMX/BQA6/wsAR/8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcf8QAHj/DQCK/wgAnf8EALT/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8FAD//CQo2ZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAIBS2SCgA//w4Agf8AAMb/AADG/wAAxv8AAMb/AADF/wUArP8MAI3/EQB1/xIAcP8SAHD/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8HAC7/AgCE/wAAxv8AAMb/AADE/wUANf8QBjP/GgxD/xoMQ/8aDEP/GgxD/xoM
+ Q/8ZC0f/GQpJ/xgJTv8XB1P/FgZa/xQEYv8TAWv/EgBv/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/w4A
+ gv8FAKz/AADG/wAAxv8AAMb/AADG/wAAxv8BALP/BgEo/hMqWyUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAACQo2VgcALv8PAHv/AADG/wAAxv8AAMb/AQDD/wwAi/8SAHD/EgBw/xIA
+ cP8SAHD/EwFr/xYGWP8aDEP/HBA0/x4TKP8gFh7/CgQj/wIAhP8AAMb/AADG/wAAwf8GAC3/JRsg/zwv
+ HP88Lxz/PC8c/zwvHP88Lxz/OCwa/zYqGP8xJhb/LSIT/ykfEf8lGw//IxoQ/yEXF/8fFCT/HBA0/xgK
+ Sv8UA2P/EgBv/xIAcP8SAHD/EgBw/wsAj/8BAMP/AADG/wAAxv8AAMb/AgCH/wcEKdMAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABMmZBgHASj7DwBp/wAAxP8AAMb/AADG/wkA
+ m/8SAHD/EgBw/xMCZ/8ZCkn/HhMo/yIZEv8pHxH/OCwa/0g5JP9TQiv/Xksx/xUNJ/8CAIn/AADG/wAA
+ xv8AALj/BgAn/1lJP/+HcU3/h3FN/4dxTf+HcU3/h3FN/4dwTf+Fbkv/hG1K/39oRv99ZUX/eGFB/3Ba
+ PP9lUTb/V0Ut/0U2Iv8xJhb/JBsP/yAVH/8aDED/FANi/xIAcP8SAHD/CwCQ/wAAxv8AAMb/AADG/wQA
+ WP8JCS6RAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwMqxQwA
+ UP8CAL7/AADG/wAAxf8QAHv/EwFr/xwPNf8jGRH/MiYW/08/KP9qVTn/g2xK/5WAWv+jkGb/rpxw/7mp
+ fP8dFTD/AgCX/wAAxv8AAMb/AQCo/wYAJf+nnYX/2s+h/9DEkP/azqD/3tOo/9vQo//VyZj/z8KN/87B
+ jP/Mvor/x7mG/8K0gv+7q3v/sqFz/6iVav+ciGD/jnhT/3ReQP9XRi3/OSwb/yQbEP8dETH/EwJq/xIA
+ cv8CAL3/AADG/wAAvv8GAC//Dx1CSQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAkJNIAJADn/BACz/wAAxv8BAMH/EgBy/x0QMv8yJxb/a1Y6/4NsSv+olmz/x7iK/9fL
+ m//Wypn/3dKm/93Rpf/Xy53/GhMw/wEAn/8AAMb/AADG/wIAkv8IAib/tqqA/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/93Spv/Nv5D/p5Vr/4Zv
+ Tf+Lel7/NCgY/x0RMP8SAHH/AwC5/wAAxv8CAJT/BwQp7xUvVQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANFkonBgAo/wUAn/8AAMb/AQDC/xEAcv8iGBX/ZlI3/4p0
+ U//GuIn/8OjI/9rPoP/SxZL/0MOO/9DDjv/Qw47/x7uJ/w8JKv8BAKn/AADG/wAAxv8DAHv/FA0s/8q9
+ i//Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/RxI//0cSQ//DoyP/HuYv/hW5N/3ZlS/8iGBX/EQBy/wEAwv8AAMb/BABf/wkKL6YAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBFBAAcDK80FAHv/AADG/wAA
+ xv8PAH7/IRYa/39wV/+Pelb/6N65/9jMnP/Qw47/0MOO/9DDjv/Pwo3/yLqH/6yed/8IAij/AQC1/wAA
+ xv8AAMb/BABi/yQbL/+VgVr/loFa/5aBWv+WgVr/loFa/5aBWv+WgVr/mIRc/5mEXf+ciGD/o5Bm/6ya
+ bv+4qHj/xriF/8/Cjf/Qw47/0MOO/9DDjv/f1Kj/5Nmy/4t0Uv9kUTf/IRYb/w4AhP8AAMb/AAC9/wYA
+ MP8NFjxNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAKCzd4BQBQ/wAAxv8AAMb/CgCX/x0QMv9QQSz/jnhX/97UrP/TxpP/w7SC/6iWav+Tflj/hW5L/39n
+ Rv9gTD3/BgAt/wAAwP8AAMb/AADD/wUAOf8/MTT/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/39nRv+HcE3/mINc/7OidP/OwIz/3NGk/9rPqP+KdFT/Sjsm/xwQ
+ M/8IAJ7/AADG/wIAj/8HBCnvFTFXBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAACQk0FgYBKv0BALL/AADG/wMAt/8XCFL/LiMU/3xlRP+xn3L/wbKA/4Jq
+ SP9+ZkX/fmZF/35mRf9+ZkX/Szo3/wUAM/8AAMT/AADG/wIAg/8KBCb/bFdA/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/kHtV/87B
+ j/+rmXD/e2RD/y0iE/8WCFf/AgC+/wAAxv8EAFT/CgwymQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBjDGAwB3/wAAxv8AAMb/DQGF/yIY
+ Ff9jUDX/iXNP/5R/Wf9+ZkX/fmZF/35mRf9+ZkX/fmZF/049OP8GACj/BABZ/wQAVP8IAij/VEI6/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf+ZhV3/hG1K/2FOM/8hFxf/CwGR/wAAxv8BALP/BgEq/wwSNzQAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQkzWAUA
+ Of8AAMH/AADG/wQAtP8bDj3/PS8d/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf99ZUX/V0U7/zsu
+ M/8/MTT/ZlI//35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf87Lhz/Gg1C/wIAu/8AAMb/AwB4/wkJ
+ LtMcRWsCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAABEhRwcIBivrAgCR/wAAxv8AAMb/DAOJ/yMaEv9vWz3/hG1K/4RtSv+EbUr/hG1K/4Rt
+ Sv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv+EbUr/hG1K/4Rt
+ Sv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv9tWTz/IxkT/wsC
+ kP8AAMb/AADC/wUAOv8ICC1jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQswfwUASf8AAMX/AADG/wEAwP8aD0D/PTEe/4t1
+ Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2
+ Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/414
+ U/+KdFD/Oi4c/xkPQv8BAMH/AADG/wIAkv8IBSrvFS1SDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgRhkHBCr1AQCd/wAA
+ xv8AAMb/BwOg/yIZFP9yYEH/qJVq/6iWa/+Yg1z/k35Y/5N+WP+Tflj/k35Y/5N+WP+Tflj/k35Y/5N+
+ WP+Tflj/k35Y/5N+WP+Tflj/k35Y/5N+WP+Tflj/k35Y/5N+WP+Tflj/k35Y/5N+WP+Tflj/k35Y/5N+
+ WP+Xglv/qZdr/7+vf/+ei2L/bFs+/yIZE/8IA57/AADG/wAAxf8FAEv/CActhgAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAACQgtjQQATv8AAMX/AADG/wAAxv8UDF7/Nisa/5mGXv/FtoT/zsGN/8O0gv+yoXT/pZNo/52K
+ Yf+bh1//m4df/5uHX/+bh1//m4df/5uHX/+bh1//m4df/5uHX/+bh1//m4df/5uHX/+bh1//m4df/5uH
+ X/+bh1//n4xj/6qXbP+8rXz/zcCM/9nOn/+3pnf/lYFa/zIoF/8UDVr/AADG/wAAxv8BAKH/BgEo9A8a
+ UCIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAABImTB0HAyjzAgCZ/wAAxv8AAMb/AgG6/x4WJv9mWDz/p5Vq/9DD
+ lP/WyZj/0MOO/9DDjv/OwIz/x7mG/8Kzgf+9rn3/uKl5/7ald/+0o3X/s6J0/7Ggcv+wn3L/sJ9y/7Cf
+ cv+xn3L/s6N0/7ioef/AsX//yryI/8/Cjf/Qw47/0MOO/93Spf/d0qv/pJFn/15QNv8eFif/AgG7/wAA
+ xv8AAMb/BABW/wgGLp4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgwxiAUARP8AAMP/AADG/wAA
+ xv8KB5H/KiAT/5WFXf+xoHL/3NGn/9rPoP/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD
+ jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0sWS/+HWrP/e06j/s6J0/5CA
+ Wv8nHRH/CgeS/wAAxv8AAMb/AQCl/wYBKPoNFkooAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYv
+ VQ4HAyjjAgCF/wAAxv8AAMb/AADF/xcRTP9EOSX/rZxw/7ald//PwpT/0saU/9zRpP/SxpP/0cWR/9jM
+ nf/b0KL/2s6g/8/Cjf/Pwo3/z8KN/8/Cjf/Pwo3/z8KN/8/Cjf/Pwo3/z8KN/9XJmP/ZzqD/1MiW/+rg
+ vv/e067/vKx9/6OSaP8/NSH/FhBT/wAAxf8AAMb/AADF/wQAUv8IBzCfAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAADBM5WwYAMv8BALb/AADG/wAAxv8CArn/HRUu/0g9KP+XiWH/q5xw/7Kj
+ df+2p3j/ual6/7ure/+7q3v/u6t7/7ure/+7q3v/u6t7/7ure/+7q3v/u6t7/7ure/+7q3v/u6t7/7ur
+ e/+6qnr/t6d4/7Wmd/+yonT/q5xw/5aIYP9DOST/HBQz/wIBvP8AAMb/AADG/wIAl/8GASj1Cg07FwAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEkSQAJCS63BABa/wAAxf8AAMb/AADG/wMC
+ tv8XEUv/IRkX/yMaD/8lHA//KyIT/zAmFv8zKhn/OjAe/zsxHv87MR7/OzEe/z40If9BNyP/QTcj/zsx
+ Hv87MR7/OzEe/zUrGv80Khn/MigY/ywiE/8pIBL/JRwO/yMaD/8hGRb/FhBT/wICuv8AAMb/AADG/wAA
+ wP8FAD//CQo1ggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBI3FggF
+ K+wCAIb/AADG/wAAxv8AAMb/AADG/wEBwv8DA7P/BQSq/wcFn/8JB5f/CgiQ/wwJiP8MCYf/DAmH/wwJ
+ h/8MCYT/DQqA/w0KgP8MCYf/DAmH/wwJh/8LCI7/CgiP/woHk/8HBZ//BwWi/wUErP8DA7P/AQHB/wAA
+ xv8AAMb/AADG/wAAxv8DAHT/BwQs3QoMOgcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAANFTtQBgEt/gEAoP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAKL/BgEr/QsOPEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIlSpAAoLN4UFADP/AQCs/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AALr/BQA8/wkJNI0AAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQHlYBCAUulAUA
+ Of8BAK7/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMP/BABT/wgF
+ LsMTK20JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAABMnZwUJCTSnBQA3/wEAp/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMX/AwBp/wcELNkOGk8ZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECBaAQgHMIwGAC7/AgCO/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMP/AwBm/wcDKtsLDz0cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQs3ZgYC
+ KPcDAGb/AAC9/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BALb/BABR/wgFLdIME0QbAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAALEUErBwQswwUAOf8CAIv/AADC/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxP8CAI7/BgA1/wgGL6UPG1INAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABcyegQKDTtiBwQs4wUAPf8DAH3/AQC0/wAA
+ xf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADA/wIAkv8FAEj/BwQp3gsO
+ PFwZN4EBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUt
+ cAsJCjVdCAYwxAYCLP4FAEf/AwBw/wIAkv8BAKz/AAC6/wAAvf8AAL3/AAC5/wEAqf8CAI//AwBm/wUA
+ Nf8JCC3fCQkvbxYzWQ0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAABg2fwAMEkQfCAcwZQkILpwJCi/hBwMp/gYBKf8GACn/BgAp/wYB
+ Kf8IBSr7CAcs0gkJL5MLEDZEFzZbBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ 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
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkJNWYDAHP/AADG/wgAn/8gFSb/gW5R/83A
+ mf/SxZH/vKx8/6WSaP+VgFr/iXJP/xMLLv8AAMD/AADG/wQAYP9LOzf/fmZF/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35nRf+AaEf/h3BN/5aCW/+pl2v/xreF/9nNnv/HuZT/eWVI/yAUJ/8HAKX/AAC//wYC
+ NfYQIUcNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkJNAwFAT/4AADD/wIA
+ vv8ZC0f/U0Iq/5+MY/+tm2//fmZF/35mRf9+ZkX/cVtC/wgCL/8AAL3/AQCf/xEJLv90XkL/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/gmpI/7urff+ahl//UUEp/xgL
+ Tf8BAMH/AgCR/wgHLaUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAIBS+tAgCa/wAAxv8MA4n/MCQa/31mRf+DbEn/fmZF/35mRf9+ZkX/emNE/y8jMP8SCjD/IBYu/2VR
+ Pv9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/4Rt
+ Sv98ZUT/LiMa/woCkv8AAMb/BABU/wsQNkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAALDjNDBABX/wAAxv8CAL3/GxA7/2JPNP+Da0n/g2tJ/4NrSf+Da0n/g2tJ/4Nr
+ Sf+AaUn/gmtJ/4NrSf+Da0n/g2tJ/4NrSf+Da0n/g2tJ/4NrSf+Da0n/g2tJ/4NrSf+Da0n/g2tJ/4Nr
+ Sf+Da0n/g2tJ/4NrSf9gTjP/Gg8+/wEAwP8BAK//BwMs1BUuUwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYOF4DBwQtzQEAq/8AAMb/CQSW/zEmGv+KdVH/jnlU/413
+ Uv+Nd1L/jXdS/413Uv+Nd1L/jXdS/413Uv+Nd1L/jXdS/413Uv+Nd1L/jXdS/413Uv+Nd1L/jXdS/413
+ Uv+Nd1L/jXdS/413Uv+OeFP/lYBa/4lzUP8wJRn/CQSX/wAAxv8DAG7/CgswbQAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgwxWgQAYf8AAMb/AADF/xcP
+ T/9lVTn/taV2/7emd/+mlGn/moZe/5eDXP+Xg1v/l4Nb/5eDW/+Xg1v/l4Nb/5eDW/+Xg1v/l4Nb/5eD
+ W/+Xg1v/l4Nb/5eDW/+Xg1z/noph/7Cfcv/GuIb/rZxv/2BQNv8XD0z/AADE/wAAvP8GAjXuDRY/EAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEiVKBgcF
+ LtYBAKz/AADG/wQCsv8qISH/l4Ve/8q8jP/TxpT/zb+L/8S2hP+8rHz/t6d4/7Khc/+vnnH/rpxw/6yb
+ bv+smm7/rJpu/6yabv+unXD/tKN1/72uff/HuYb/zsGN/9fLm//Sxp7/kX9Z/ycfIf8EArL/AADG/wMA
+ fP8IBS2EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAsPNFQEAFj/AADF/wAAxv8NCn//UUUu/66dcP/Xy6D/18ub/9PHlP/RxI//0cSP/9LG
+ kv/SxZL/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9LFkf/Ux5T/4das/93Sqf+vnnH/TEAr/w0K
+ gP8AAMb/AAC8/wYBOPENFkoXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAABo8YgEIBi29AgCX/wAAxv8BAMP/GRNF/3FkRf+1pHb/wrOD/8i6
+ i//GuIb/yLqK/8u9j//Ju4v/xbaE/8W2hP/FtoT/xbaE/8W2hP/FtoT/xbeF/8u+j//Iuov/yr2R/7am
+ eP9nWz7/GBJJ/wAAxP8AAMb/AwB1/wgFLoEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKDjMnBgI/9QAAvP8AAMb/AgK6/xcR
+ TP80KiD/Qjgk/0tAKv9SRy//WE00/1tPNv9bTzb/XFA2/2BUOf9fUzj/W082/1tPNv9WSzL/VUox/05D
+ LP9JPin/Qjgk/zQqIP8WEVL/AgG8/wAAxv8BALD/BgMz5AsPPw8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgwxawQA
+ X/4AAMX/AADG/wAAxv8BAcH/AwK0/wUEqv8HBaL/CAaa/wkGl/8JBpf/CQeW/woHkv8KB5P/CQaX/wkG
+ l/8IBpz/CAad/wYEp/8FBKz/AwK1/wEBwP8AAMb/AADG/wAAxf8EAFT+CQgzVwAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAFS9VAwgHMK8DAHr/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wIAgv8IBS6sDxtSAQAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsRQQwHAyzDAgCE/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCh/wcD
+ Md0MFUgUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAME0YUBwMtxgMAe/8AAMX/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8BAKr/BgI66AsOPCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ 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
+ AADAAwAA4AcAAPAPAAD4HwAA
+</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Impostor.Patcher.WinForms.csproj b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Impostor.Patcher.WinForms.csproj
new file mode 100644
index 0000000..7149436
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Impostor.Patcher.WinForms.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
+
+ <PropertyGroup>
+ <AssemblyName>Impostor</AssemblyName>
+ <ProjectGuid>{804CF172-0C87-4423-9688-BD97D549891E}</ProjectGuid>
+ <OutputType>WinExe</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <UseWindowsForms>true</UseWindowsForms>
+ <Copyright>Copyright © AeonLucid 2020</Copyright>
+ <ApplicationIcon>icon.ico</ApplicationIcon>
+ <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
+ <GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Patcher.Shared\Impostor.Patcher.Shared.csproj" />
+ <PackageReference Include="System.Resources.Extensions" Version="5.0.0" />
+ <Reference Include="System.Runtime.InteropServices.RuntimeInformation" />
+ <Reference Include="System.Windows.Forms" />
+ </ItemGroup>
+
+</Project> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Program.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Program.cs
new file mode 100644
index 0000000..7ca9035
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Program.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Windows.Forms;
+using Impostor.Patcher.WinForms.Forms;
+
+namespace Impostor.Patcher.WinForms
+{
+ internal static class Program
+ {
+ [STAThread]
+ private static void Main()
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+ Application.Run(new FrmMain());
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.Designer.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..bb5c4cf
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.Designer.cs
@@ -0,0 +1,69 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Impostor.Patcher.WinForms.Properties
+{
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder",
+ "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources
+ {
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance",
+ "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources()
+ {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
+ .Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if ((resourceMan == null))
+ {
+ global::System.Resources.ResourceManager temp =
+ new global::System.Resources.ResourceManager("Impostor.Client.WinForms.Properties.Resources",
+ typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
+ .Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture
+ {
+ get { return resourceCulture; }
+ set { resourceCulture = value; }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.resx b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.resx
new file mode 100644
index 0000000..af7dbeb
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.resx
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.Designer.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..42356db
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.Designer.cs
@@ -0,0 +1,26 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Impostor.Patcher.Properties
+{
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute(
+ "Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+ {
+ private static Settings defaultInstance =
+ ((Settings) (global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default
+ {
+ get { return defaultInstance; }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.settings b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.settings
new file mode 100644
index 0000000..3964565
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.settings
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
+ <Profiles>
+ <Profile Name="(Default)" />
+ </Profiles>
+ <Settings />
+</SettingsFile>
diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/icon.ico b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/icon.ico
new file mode 100644
index 0000000..8cc46f3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/icon.ico
Binary files differ
diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/App.razor b/Impostor-dev/src/Impostor.Plugins.Debugger/App.razor
new file mode 100644
index 0000000..38e8633
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Debugger/App.razor
@@ -0,0 +1,10 @@
+<Router AppAssembly="@typeof(DebugPlugin).Assembly">
+ <Found Context="routeData">
+ <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
+ </Found>
+ <NotFound>
+ <LayoutView Layout="@typeof(MainLayout)">
+ <p>Sorry, there's nothing at this address.</p>
+ </LayoutView>
+ </NotFound>
+</Router> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPlugin.cs b/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPlugin.cs
new file mode 100644
index 0000000..8f57e89
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPlugin.cs
@@ -0,0 +1,13 @@
+using Impostor.Api.Plugins;
+
+namespace Impostor.Plugins.Debugger
+{
+ [ImpostorPlugin(
+ package: "gg.impostor.debugger",
+ name: "Debugger",
+ author: "Gerard",
+ version: "1.0.0")]
+ public class DebugPlugin : PluginBase
+ {
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPluginStartup.cs b/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPluginStartup.cs
new file mode 100644
index 0000000..cc36ce6
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPluginStartup.cs
@@ -0,0 +1,35 @@
+using Impostor.Api.Plugins;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Impostor.Plugins.Debugger
+{
+ public class DebugPluginStartup : IPluginStartup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddRazorPages();
+ services.AddServerSideBlazor();
+ }
+
+ public void ConfigureHost(IHostBuilder host)
+ {
+ host.ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.Configure(app =>
+ {
+ app.UseStaticFiles();
+ app.UseRouting();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapBlazorHub();
+ endpoints.MapFallbackToPage("/_Host");
+ });
+ });
+ });
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj b/Impostor-dev/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj
new file mode 100644
index 0000000..9518e48
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ <OutputType>Library</OutputType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Folder Include="Properties\" />
+ </ItemGroup>
+
+</Project> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/Index.razor b/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/Index.razor
new file mode 100644
index 0000000..1bfa478
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/Index.razor
@@ -0,0 +1,69 @@
+@page "/"
+@implements IDisposable
+@using Impostor.Api.Events.Managers
+@using Impostor.Api.Games.Managers
+@using Impostor.Api.Events
+@using Impostor.Api.Events.Player
+@implements Impostor.Api.Events.IEventListener
+@inject IEventManager EventManager
+@inject IGameManager GameManager
+
+<div class="container">
+ <h2>Games</h2>
+ @if (GameManager.Games.Any())
+ {
+ <table class="table table-striped">
+ <thead>
+ <tr>
+ <th>Code</th>
+ <th>Players</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var game in GameManager.Games)
+ {
+ <tr>
+ <td>@game.Code</td>
+ <td>
+ <ul class="mb-0">
+ @foreach (var player in game.Players)
+ {
+ <li>@player.Client.Name</li>
+ }
+ </ul>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ }
+ else
+ {
+ <div class="text-center">
+ <i class="text-muted">There are no active games.</i>
+ </div>
+ }
+</div>
+
+@code {
+ private IDisposable _disposable;
+
+ [EventListener(typeof(IGameCreatedEvent))]
+ [EventListener(typeof(IGameDestroyedEvent))]
+ [EventListener(typeof(IGamePlayerJoinedEvent))]
+ [EventListener(typeof(IGamePlayerLeftEvent))]
+ public void OnGameCreated(IGameEvent e)
+ {
+ StateHasChanged();
+ }
+
+ protected override void OnInitialized()
+ {
+ _disposable = EventManager.RegisterListener(this, InvokeAsync);
+ }
+
+ public void Dispose()
+ {
+ _disposable?.Dispose();
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/_Host.cshtml b/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/_Host.cshtml
new file mode 100644
index 0000000..eed3aaf
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/_Host.cshtml
@@ -0,0 +1,19 @@
+@page "/"
+@namespace Impostor.Plugins.Debugger.Pages
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+ <title>Impostor Debugger</title>
+ <base href="~/"/>
+ <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
+</head>
+<body>
+<component type="typeof(App)" render-mode="Server"/>
+
+<script src="_framework/blazor.server.js"></script>
+</body>
+</html> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/Shared/MainLayout.razor b/Impostor-dev/src/Impostor.Plugins.Debugger/Shared/MainLayout.razor
new file mode 100644
index 0000000..07da9d6
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Debugger/Shared/MainLayout.razor
@@ -0,0 +1,5 @@
+@inherits LayoutComponentBase
+
+<div class="page">
+ @Body
+</div> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/_Imports.razor b/Impostor-dev/src/Impostor.Plugins.Debugger/_Imports.razor
new file mode 100644
index 0000000..109ec4e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Debugger/_Imports.razor
@@ -0,0 +1,8 @@
+@using System.Net.Http
+@using Microsoft.AspNetCore.Authorization
+@using Microsoft.AspNetCore.Components.Authorization
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.JSInterop
+@using Impostor.Plugins.Debugger.Shared \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Plugins.Example/ExamplePlugin.cs b/Impostor-dev/src/Impostor.Plugins.Example/ExamplePlugin.cs
new file mode 100644
index 0000000..dafba7c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Example/ExamplePlugin.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using Impostor.Api.Plugins;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Plugins.Example
+{
+ [ImpostorPlugin(
+ package: "gg.impostor.example",
+ name: "Example",
+ author: "AeonLucid",
+ version: "1.0.0")]
+ public class ExamplePlugin : PluginBase
+ {
+ private readonly ILogger<ExamplePlugin> _logger;
+
+ public ExamplePlugin(ILogger<ExamplePlugin> logger)
+ {
+ _logger = logger;
+ }
+
+ public override ValueTask EnableAsync()
+ {
+ _logger.LogInformation("Example is being enabled.");
+ return default;
+ }
+
+ public override ValueTask DisableAsync()
+ {
+ _logger.LogInformation("Example is being disabled.");
+ return default;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Plugins.Example/ExamplePluginStartup.cs b/Impostor-dev/src/Impostor.Plugins.Example/ExamplePluginStartup.cs
new file mode 100644
index 0000000..936f15e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Example/ExamplePluginStartup.cs
@@ -0,0 +1,22 @@
+using Impostor.Api.Events;
+using Impostor.Api.Plugins;
+using Impostor.Plugins.Example.Handlers;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Impostor.Plugins.Example
+{
+ public class ExamplePluginStartup : IPluginStartup
+ {
+ public void ConfigureHost(IHostBuilder host)
+ {
+ }
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddSingleton<IEventListener, GameEventListener>();
+ services.AddSingleton<IEventListener, PlayerEventListener>();
+ services.AddSingleton<IEventListener, MeetingEventListener>();
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Plugins.Example/Handlers/GameEventListener.cs b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/GameEventListener.cs
new file mode 100644
index 0000000..be2d0f3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/GameEventListener.cs
@@ -0,0 +1,64 @@
+using System;
+using Impostor.Api.Events;
+
+namespace Impostor.Plugins.Example.Handlers
+{
+ public class GameEventListener : IEventListener
+ {
+ [EventListener(EventPriority.Monitor)]
+ public void OnGame(IGameEvent e)
+ {
+ Console.WriteLine(e.GetType().Name + " triggered");
+ }
+
+ [EventListener]
+ public void OnGameCreated(IGameCreatedEvent e)
+ {
+ Console.WriteLine("Game > created");
+ }
+
+ [EventListener]
+ public void OnGameStarting(IGameStartingEvent e)
+ {
+ Console.WriteLine("Game > starting");
+ }
+
+ [EventListener]
+ public void OnGameStarted(IGameStartedEvent e)
+ {
+ Console.WriteLine("Game > started");
+
+ foreach (var player in e.Game.Players)
+ {
+ var info = player.Character.PlayerInfo;
+
+ Console.WriteLine($"- {info.PlayerName} {info.IsImpostor}");
+ }
+ }
+
+ [EventListener]
+ public void OnGameEnded(IGameEndedEvent e)
+ {
+ Console.WriteLine("Game > ended");
+ Console.WriteLine("- Reason: " + e.GameOverReason);
+ }
+
+ [EventListener]
+ public void OnGameDestroyed(IGameDestroyedEvent e)
+ {
+ Console.WriteLine("Game > destroyed");
+ }
+
+ [EventListener]
+ public void OnPlayerJoined(IGamePlayerJoinedEvent e)
+ {
+ Console.WriteLine("Player joined a game.");
+ }
+
+ [EventListener]
+ public void OnPlayerLeftGame(IGamePlayerLeftEvent e)
+ {
+ Console.WriteLine("Player left a game.");
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs
new file mode 100644
index 0000000..847532c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs
@@ -0,0 +1,21 @@
+using System;
+using Impostor.Api.Events;
+using Impostor.Api.Events.Meeting;
+
+namespace Impostor.Plugins.Example.Handlers
+{
+ public class MeetingEventListener : IEventListener
+ {
+ [EventListener]
+ public void OnMeetingStarted(IMeetingStartedEvent e)
+ {
+ Console.WriteLine("Meeting > started");
+ }
+
+ [EventListener]
+ public void OnMeetingEnded(IMeetingEndedEvent e)
+ {
+ Console.WriteLine("Meeting > ended");
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs
new file mode 100644
index 0000000..0190d3b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Numerics;
+using System.Threading.Tasks;
+using Impostor.Api.Events;
+using Impostor.Api.Events.Player;
+using Impostor.Api.Innersloth.Customization;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Plugins.Example.Handlers
+{
+ public class PlayerEventListener : IEventListener
+ {
+ private static readonly Random Random = new Random();
+
+ private readonly ILogger<PlayerEventListener> _logger;
+
+ public PlayerEventListener(ILogger<PlayerEventListener> logger)
+ {
+ _logger = logger;
+ }
+
+ [EventListener]
+ public void OnPlayerSpawned(IPlayerSpawnedEvent e)
+ {
+ _logger.LogDebug(e.PlayerControl.PlayerInfo.PlayerName + " spawned");
+
+ // Need to make a local copy because it might be possible that
+ // the event gets changed after being handled.
+ var clientPlayer = e.ClientPlayer;
+ var playerControl = e.PlayerControl;
+
+ /*
+ Task.Run(async () =>
+ {
+ Console.WriteLine("Starting player task.");
+
+ // Give the player time to load.
+ await Task.Delay(TimeSpan.FromSeconds(3));
+
+ while (clientPlayer.Client.Connection != null &&
+ clientPlayer.Client.Connection.IsConnected)
+ {
+ // Modify player properties.
+ await playerControl.SetColorAsync((byte) Random.Next(1, 9));
+ await playerControl.SetHatAsync((uint) Random.Next(1, 9));
+ await playerControl.SetSkinAsync((uint) Random.Next(1, 9));
+ await playerControl.SetPetAsync((uint) Random.Next(1, 9));
+
+ await Task.Delay(TimeSpan.FromMilliseconds(5000));
+ }
+
+ _logger.LogDebug("Stopping player task.");
+ });
+ */
+ }
+
+ [EventListener]
+ public void OnPlayerDestroyed(IPlayerDestroyedEvent e)
+ {
+ _logger.LogDebug(e.PlayerControl.PlayerInfo.PlayerName + " destroyed");
+ }
+
+ [EventListener]
+ public async ValueTask OnPlayerChat(IPlayerChatEvent e)
+ {
+ _logger.LogDebug(e.PlayerControl.PlayerInfo.PlayerName + " said " + e.Message);
+
+ if (e.Message == "test")
+ {
+ e.Game.Options.KillCooldown = 0;
+ e.Game.Options.NumImpostors = 2;
+ e.Game.Options.PlayerSpeedMod = 5;
+
+ await e.Game.SyncSettingsAsync();
+ }
+
+ if (e.Message == "look")
+ {
+ await e.PlayerControl.SetColorAsync(ColorType.Pink);
+ await e.PlayerControl.SetHatAsync(HatType.Cheese);
+ await e.PlayerControl.SetSkinAsync(SkinType.Police);
+ await e.PlayerControl.SetPetAsync(PetType.Ufo);
+ }
+
+ if (e.Message == "snap")
+ {
+ await e.PlayerControl.NetworkTransform.SnapToAsync(new Vector2(1, 1));
+ }
+
+ await e.PlayerControl.SetNameAsync(e.Message);
+ await e.PlayerControl.SendChatAsync(e.Message);
+ }
+
+ [EventListener]
+ public void OnPlayerStartMeetingEvent(IPlayerStartMeetingEvent e)
+ {
+ _logger.LogDebug($"Player {e.PlayerControl.PlayerInfo.PlayerName} start meeting, reason: " + (e.Body==null ? "Emergency call button" : "Found the body of the player "+e.Body.PlayerInfo.PlayerName));
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj b/Impostor-dev/src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj
new file mode 100644
index 0000000..dd4724d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.1</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Impostor-dev/src/Impostor.Server/Config/AntiCheatConfig.cs b/Impostor-dev/src/Impostor.Server/Config/AntiCheatConfig.cs
new file mode 100644
index 0000000..f4807e7
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Config/AntiCheatConfig.cs
@@ -0,0 +1,9 @@
+namespace Impostor.Server.Config
+{
+ public class AntiCheatConfig
+ {
+ public const string Section = "AntiCheat";
+
+ public bool BanIpFromGame { get; set; } = true;
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Config/DebugConfig.cs b/Impostor-dev/src/Impostor.Server/Config/DebugConfig.cs
new file mode 100644
index 0000000..630d1b4
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Config/DebugConfig.cs
@@ -0,0 +1,11 @@
+namespace Impostor.Server.Config
+{
+ public class DebugConfig
+ {
+ public const string Section = "Debug";
+
+ public bool GameRecorderEnabled { get; set; }
+
+ public string GameRecorderPath { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Config/DisconnectMessages.cs b/Impostor-dev/src/Impostor.Server/Config/DisconnectMessages.cs
new file mode 100644
index 0000000..a86735f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Config/DisconnectMessages.cs
@@ -0,0 +1,19 @@
+namespace Impostor.Server.Config
+{
+ public static class DisconnectMessages
+ {
+ public const string Error = "There was an internal server error. " +
+ "Check the server console for more information. " +
+ "Please report the issue on the AmongUsServer GitHub if it keeps happening.";
+
+ public const string Destroyed = "The game you tried to join is being destroyed. " +
+ "Please create a new game.";
+
+ public const string NotImplemented = "Game listing has not been implemented in Impostor yet for servers " +
+ "running in server redirection mode.";
+
+ public const string UsernameLength = "Your username is too long, please make it shorter.";
+
+ public const string UsernameIllegalCharacters = "Your username contains illegal characters, please remove them.";
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Config/ServerConfig.cs b/Impostor-dev/src/Impostor.Server/Config/ServerConfig.cs
new file mode 100644
index 0000000..1c58433
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Config/ServerConfig.cs
@@ -0,0 +1,30 @@
+using Impostor.Server.Utils;
+
+namespace Impostor.Server.Config
+{
+ internal class ServerConfig
+ {
+ private string? _resolvedPublicIp;
+ private string? _resolvedListenIp;
+
+ public const string Section = "Server";
+
+ public string PublicIp { get; set; } = "127.0.0.1";
+
+ public ushort PublicPort { get; set; } = 22023;
+
+ public string ListenIp { get; set; } = "127.0.0.1";
+
+ public ushort ListenPort { get; set; } = 22023;
+
+ public string ResolvePublicIp()
+ {
+ return _resolvedPublicIp ??= IpUtils.ResolveIp(PublicIp);
+ }
+
+ public string ResolveListenIp()
+ {
+ return _resolvedListenIp ??= IpUtils.ResolveIp(ListenIp);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorConfig.cs b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorConfig.cs
new file mode 100644
index 0000000..0ccfa0d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorConfig.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace Impostor.Server.Config
+{
+ public class ServerRedirectorConfig
+ {
+ public const string Section = "ServerRedirector";
+
+ public bool Enabled { get; set; }
+
+ public bool Master { get; set; }
+
+ public NodeLocator Locator { get; set; }
+
+ public List<ServerRedirectorNode> Nodes { get; set; }
+
+ public class NodeLocator
+ {
+ public string Redis { get; set; }
+
+ public string UdpMasterEndpoint { get; set; }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorNode.cs b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorNode.cs
new file mode 100644
index 0000000..d11b60f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorNode.cs
@@ -0,0 +1,9 @@
+namespace Impostor.Server.Config
+{
+ public class ServerRedirectorNode
+ {
+ public string Ip { get; set; }
+
+ public ushort Port { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Constants.cs b/Impostor-dev/src/Impostor.Server/Constants.cs
new file mode 100644
index 0000000..62d90b2
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Constants.cs
@@ -0,0 +1,8 @@
+namespace Impostor.Server
+{
+ internal static class Constants
+ {
+ public const int SpawnTimeout = 2500;
+ public const int ConnectionTimeout = 2500;
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Events/EventHandler.cs b/Impostor-dev/src/Impostor.Server/Events/EventHandler.cs
new file mode 100644
index 0000000..190f7f3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/EventHandler.cs
@@ -0,0 +1,24 @@
+using Impostor.Api.Events;
+using Impostor.Server.Events.Register;
+
+namespace Impostor.Server.Events
+{
+ internal readonly struct EventHandler
+ {
+ public EventHandler(IEventListener o, IRegisteredEventListener listener)
+ {
+ Object = o;
+ Listener = listener;
+ }
+
+ public IEventListener Object { get; }
+
+ public IRegisteredEventListener Listener { get; }
+
+ public void Deconstruct(out IEventListener o, out IRegisteredEventListener listener)
+ {
+ o = Object;
+ listener = Listener;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Events/EventManager.cs b/Impostor-dev/src/Impostor.Server/Events/EventManager.cs
new file mode 100644
index 0000000..5625c4d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/EventManager.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Events;
+using Impostor.Api.Events.Managers;
+using Impostor.Server.Events.Register;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Events
+{
+ internal class EventManager : IEventManager
+ {
+ private readonly ConcurrentDictionary<Type, TemporaryEventRegister> _temporaryEventListeners;
+ private readonly ConcurrentDictionary<Type, List<EventHandler>> _cachedEventHandlers;
+ private readonly ILogger<EventManager> _logger;
+ private readonly IServiceProvider _serviceProvider;
+
+ public EventManager(ILogger<EventManager> logger, IServiceProvider serviceProvider)
+ {
+ _logger = logger;
+ _serviceProvider = serviceProvider;
+ _temporaryEventListeners = new ConcurrentDictionary<Type, TemporaryEventRegister>();
+ _cachedEventHandlers = new ConcurrentDictionary<Type, List<EventHandler>>();
+ }
+
+ /// <inheritdoc />
+ public IDisposable RegisterListener<TListener>(TListener listener, Func<Func<Task>, Task> invoker = null)
+ where TListener : IEventListener
+ {
+ if (listener == null)
+ {
+ throw new ArgumentNullException(nameof(listener));
+ }
+
+ var eventListeners = RegisteredEventListener.FromType(listener.GetType());
+ var disposes = new IDisposable[eventListeners.Count];
+
+ foreach (var eventListener in eventListeners)
+ {
+ IRegisteredEventListener wrappedEventListener = new WrappedRegisteredEventListener(eventListener, listener);
+
+ if (invoker != null)
+ {
+ wrappedEventListener = new InvokedRegisteredEventListener(wrappedEventListener, invoker);
+ }
+
+ var register = _temporaryEventListeners.GetOrAdd(
+ wrappedEventListener.EventType,
+ _ => new TemporaryEventRegister());
+
+ register.Add(wrappedEventListener);
+ }
+
+ if (eventListeners.Count > 0)
+ {
+ _cachedEventHandlers.TryRemove(typeof(TListener), out _);
+ }
+
+ return new MultiDisposable(disposes);
+ }
+
+ /// <inheritdoc />
+ public bool IsRegistered<TEvent>()
+ where TEvent : IEvent
+ {
+ if (_cachedEventHandlers.TryGetValue(typeof(TEvent), out var handlers))
+ {
+ return handlers.Count > 0;
+ }
+
+ return GetHandlers<TEvent>().Any();
+ }
+
+ /// <inheritdoc />
+ public async ValueTask CallAsync<T>(T @event)
+ where T : IEvent
+ {
+ try
+ {
+ if (!_cachedEventHandlers.TryGetValue(typeof(T), out var handlers))
+ {
+ handlers = CacheEventHandlers<T>();
+ }
+
+ foreach (var (handler, eventListener) in handlers)
+ {
+ await eventListener.InvokeAsync(handler, @event, _serviceProvider);
+ }
+ }
+ catch (ImpostorCheatException)
+ {
+ throw;
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Invocation of event {0} threw an exception.", @event.GetType().Name);
+ }
+ }
+
+ private List<EventHandler> CacheEventHandlers<TEvent>()
+ where TEvent : IEvent
+ {
+ var handlers = GetHandlers<TEvent>()
+ .OrderByDescending(e => e.Listener.Priority)
+ .ToList();
+
+ _cachedEventHandlers[typeof(TEvent)] = handlers;
+
+ return handlers;
+ }
+
+ /// <summary>
+ /// Get all the event listeners for the given event type.
+ /// </summary>
+ /// <returns>The event listeners.</returns>
+ private IEnumerable<EventHandler> GetHandlers<TEvent>()
+ where TEvent : IEvent
+ {
+ var eventType = typeof(TEvent);
+ var interfaces = eventType.GetInterfaces();
+
+ foreach (var @interface in interfaces)
+ {
+ if (_temporaryEventListeners.TryGetValue(@interface, out var cb))
+ {
+ foreach (var eventListener in cb.GetEventListeners())
+ {
+ yield return new EventHandler(null, eventListener);
+ }
+ }
+ }
+
+ foreach (var handler in _serviceProvider.GetServices<IEventListener>())
+ {
+ if (handler is IManualEventListener manualEventListener && manualEventListener.CanExecute<TEvent>())
+ {
+ yield return new EventHandler(handler, new ManualRegisteredEventListener(manualEventListener));
+ continue;
+ }
+
+ var events = RegisteredEventListener.FromType(handler.GetType());
+
+ foreach (var eventHandler in events)
+ {
+ if (eventHandler.EventType != typeof(TEvent) && !interfaces.Contains(eventHandler.EventType))
+ {
+ continue;
+ }
+
+ yield return new EventHandler(handler, eventHandler);
+ }
+ }
+
+ if (_temporaryEventListeners.TryGetValue(eventType, out var cb2))
+ {
+ foreach (var eventListener in cb2.GetEventListeners())
+ {
+ yield return new EventHandler(null, eventListener);
+ }
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameAlterEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameAlterEvent.cs
new file mode 100644
index 0000000..3fb2368
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameAlterEvent.cs
@@ -0,0 +1,18 @@
+using Impostor.Api.Events;
+using Impostor.Api.Games;
+
+namespace Impostor.Server.Events
+{
+ public class GameAlterEvent : IGameAlterEvent
+ {
+ public GameAlterEvent(IGame game, bool isPublic)
+ {
+ Game = game;
+ IsPublic = isPublic;
+ }
+
+ public IGame Game { get; }
+
+ public bool IsPublic { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameCreatedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameCreatedEvent.cs
new file mode 100644
index 0000000..57e7a20
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameCreatedEvent.cs
@@ -0,0 +1,15 @@
+using Impostor.Api.Events;
+using Impostor.Api.Games;
+
+namespace Impostor.Server.Events
+{
+ public class GameCreatedEvent : IGameCreatedEvent
+ {
+ public GameCreatedEvent(IGame game)
+ {
+ Game = game;
+ }
+
+ public IGame Game { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameDestroyedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameDestroyedEvent.cs
new file mode 100644
index 0000000..5ee1b11
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameDestroyedEvent.cs
@@ -0,0 +1,15 @@
+using Impostor.Api.Events;
+using Impostor.Api.Games;
+
+namespace Impostor.Server.Events
+{
+ public class GameDestroyedEvent : IGameDestroyedEvent
+ {
+ public GameDestroyedEvent(IGame game)
+ {
+ Game = game;
+ }
+
+ public IGame Game { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameEndedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameEndedEvent.cs
new file mode 100644
index 0000000..4ec4dcf
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameEndedEvent.cs
@@ -0,0 +1,19 @@
+using Impostor.Api.Events;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+
+namespace Impostor.Server.Events
+{
+ public class GameEndedEvent : IGameEndedEvent
+ {
+ public GameEndedEvent(IGame game, GameOverReason gameOverReason)
+ {
+ Game = game;
+ GameOverReason = gameOverReason;
+ }
+
+ public IGame Game { get; }
+
+ public GameOverReason GameOverReason { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerJoinedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerJoinedEvent.cs
new file mode 100644
index 0000000..d728c59
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerJoinedEvent.cs
@@ -0,0 +1,19 @@
+using Impostor.Api.Events;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+
+namespace Impostor.Server.Events
+{
+ public class GamePlayerJoinedEvent : IGamePlayerJoinedEvent
+ {
+ public GamePlayerJoinedEvent(IGame game, IClientPlayer player)
+ {
+ Game = game;
+ Player = player;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer Player { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerLeftEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerLeftEvent.cs
new file mode 100644
index 0000000..d295103
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerLeftEvent.cs
@@ -0,0 +1,22 @@
+using Impostor.Api.Events;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+
+namespace Impostor.Server.Events
+{
+ public class GamePlayerLeftEvent : IGamePlayerLeftEvent
+ {
+ public GamePlayerLeftEvent(IGame game, IClientPlayer player, bool isBan)
+ {
+ Game = game;
+ Player = player;
+ IsBan = isBan;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer Player { get; }
+
+ public bool IsBan { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameStartedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartedEvent.cs
new file mode 100644
index 0000000..d21f9ec
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartedEvent.cs
@@ -0,0 +1,15 @@
+using Impostor.Api.Events;
+using Impostor.Api.Games;
+
+namespace Impostor.Server.Events
+{
+ public class GameStartedEvent : IGameStartedEvent
+ {
+ public GameStartedEvent(IGame game)
+ {
+ Game = game;
+ }
+
+ public IGame Game { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameStartingEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartingEvent.cs
new file mode 100644
index 0000000..a0763e8
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartingEvent.cs
@@ -0,0 +1,15 @@
+using Impostor.Api.Events;
+using Impostor.Api.Games;
+
+namespace Impostor.Server.Events
+{
+ public class GameStartingEvent : IGameStartingEvent
+ {
+ public GameStartingEvent(IGame game)
+ {
+ Game = game;
+ }
+
+ public IGame Game { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs
new file mode 100644
index 0000000..cf55f7d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs
@@ -0,0 +1,19 @@
+using Impostor.Api.Events.Meeting;
+using Impostor.Api.Games;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Meeting
+{
+ public class MeetingEndedEvent : IMeetingEndedEvent
+ {
+ public MeetingEndedEvent(IGame game, IInnerMeetingHud meetingHud)
+ {
+ Game = game;
+ MeetingHud = meetingHud;
+ }
+
+ public IGame Game { get; }
+
+ public IInnerMeetingHud MeetingHud { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingStartedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingStartedEvent.cs
new file mode 100644
index 0000000..aa689a3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingStartedEvent.cs
@@ -0,0 +1,19 @@
+using Impostor.Api.Events.Meeting;
+using Impostor.Api.Games;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Meeting
+{
+ public class MeetingStartedEvent : IMeetingStartedEvent
+ {
+ public MeetingStartedEvent(IGame game, IInnerMeetingHud meetingHud)
+ {
+ Game = game;
+ MeetingHud = meetingHud;
+ }
+
+ public IGame Game { get; }
+
+ public IInnerMeetingHud MeetingHud { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerChatEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerChatEvent.cs
new file mode 100644
index 0000000..7b7eb22
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerChatEvent.cs
@@ -0,0 +1,26 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ public class PlayerChatEvent : IPlayerChatEvent
+ {
+ public PlayerChatEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, string message)
+ {
+ Game = game;
+ ClientPlayer = clientPlayer;
+ PlayerControl = playerControl;
+ Message = message;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+
+ public string Message { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerCompletedTaskEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerCompletedTaskEvent.cs
new file mode 100644
index 0000000..330135d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerCompletedTaskEvent.cs
@@ -0,0 +1,27 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ public class PlayerCompletedTaskEvent : IPlayerCompletedTaskEvent
+ {
+ public PlayerCompletedTaskEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, ITaskInfo task)
+ {
+ Game = game;
+ ClientPlayer = clientPlayer;
+ PlayerControl = playerControl;
+ Task = task;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+
+ public ITaskInfo Task { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerDestroyedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerDestroyedEvent.cs
new file mode 100644
index 0000000..69a20c9
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerDestroyedEvent.cs
@@ -0,0 +1,23 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ public class PlayerDestroyedEvent : IPlayerDestroyedEvent
+ {
+ public PlayerDestroyedEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl)
+ {
+ Game = game;
+ ClientPlayer = clientPlayer;
+ PlayerControl = playerControl;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerExileEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerExileEvent.cs
new file mode 100644
index 0000000..a8660da
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerExileEvent.cs
@@ -0,0 +1,23 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ public class PlayerExileEvent : IPlayerExileEvent
+ {
+ public PlayerExileEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl)
+ {
+ Game = game;
+ ClientPlayer = clientPlayer;
+ PlayerControl = playerControl;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMovementEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMovementEvent.cs
new file mode 100644
index 0000000..31ac388
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMovementEvent.cs
@@ -0,0 +1,24 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ // TODO: Finish and use event, needs to be pooled
+ public class PlayerMovementEvent : IPlayerEvent
+ {
+ public PlayerMovementEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl)
+ {
+ Game = game;
+ ClientPlayer = clientPlayer;
+ PlayerControl = playerControl;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs
new file mode 100644
index 0000000..ca64c35
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs
@@ -0,0 +1,26 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ public class PlayerMurderEvent : IPlayerMurderEvent
+ {
+ public PlayerMurderEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, IInnerPlayerControl victim)
+ {
+ Game = game;
+ ClientPlayer = clientPlayer;
+ PlayerControl = playerControl;
+ Victim = victim;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+
+ public IInnerPlayerControl Victim { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSetStartCounterEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSetStartCounterEvent.cs
new file mode 100644
index 0000000..71c25d7
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSetStartCounterEvent.cs
@@ -0,0 +1,26 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ public class PlayerSetStartCounterEvent : IPlayerSetStartCounterEvent
+ {
+ public PlayerSetStartCounterEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, byte secondsLeft)
+ {
+ Game = game;
+ ClientPlayer = clientPlayer;
+ PlayerControl = playerControl;
+ SecondsLeft = secondsLeft;
+ }
+
+ public byte SecondsLeft { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+
+ public IGame Game { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSpawnedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSpawnedEvent.cs
new file mode 100644
index 0000000..4b6fda1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSpawnedEvent.cs
@@ -0,0 +1,23 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ public class PlayerSpawnedEvent : IPlayerSpawnedEvent
+ {
+ public PlayerSpawnedEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl)
+ {
+ Game = game;
+ ClientPlayer = clientPlayer;
+ PlayerControl = playerControl;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerStartMeetingEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerStartMeetingEvent.cs
new file mode 100644
index 0000000..70cb0d8
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerStartMeetingEvent.cs
@@ -0,0 +1,26 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ public class PlayerStartMeetingEvent : IPlayerStartMeetingEvent
+ {
+ public PlayerStartMeetingEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, IInnerPlayerControl? body)
+ {
+ Game = game;
+ ClientPlayer = clientPlayer;
+ PlayerControl = playerControl;
+ Body = body;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+
+ public IInnerPlayerControl? Body { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerVentEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerVentEvent.cs
new file mode 100644
index 0000000..0798d40
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerVentEvent.cs
@@ -0,0 +1,30 @@
+using Impostor.Api.Events.Player;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Events.Player
+{
+ public class PlayerVentEvent : IPlayerVentEvent
+ {
+ public PlayerVentEvent(IGame game, IClientPlayer sender, IInnerPlayerControl innerPlayerPhysics, VentLocation ventId, bool ventEnter)
+ {
+ Game = game;
+ ClientPlayer = sender;
+ PlayerControl = innerPlayerPhysics;
+ VentId = ventId;
+ VentEnter = ventEnter;
+ }
+
+ public IGame Game { get; }
+
+ public IClientPlayer ClientPlayer { get; }
+
+ public IInnerPlayerControl PlayerControl { get; }
+
+ public VentLocation VentId { get; }
+
+ public bool VentEnter { get; }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/MultiDisposable.cs b/Impostor-dev/src/Impostor.Server/Events/MultiDisposable.cs
new file mode 100644
index 0000000..b68f064
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/MultiDisposable.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+
+namespace Impostor.Server.Events
+{
+ /// <summary>
+ /// Disposes multiple <see cref="IDisposable"/>.
+ /// </summary>
+ internal class MultiDisposable : IDisposable
+ {
+ private readonly IEnumerable<IDisposable> _disposables;
+
+ public MultiDisposable(IEnumerable<IDisposable> disposables)
+ {
+ _disposables = disposables;
+ }
+
+ public void Dispose()
+ {
+ foreach (var disposable in _disposables)
+ {
+ disposable?.Dispose();
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/IRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/IRegisteredEventListener.cs
new file mode 100644
index 0000000..479a3f6
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Register/IRegisteredEventListener.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Threading.Tasks;
+using Impostor.Api.Events;
+
+namespace Impostor.Server.Events.Register
+{
+ internal interface IRegisteredEventListener
+ {
+ Type EventType { get; }
+
+ EventPriority Priority { get; }
+
+ ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/InvokedRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/InvokedRegisteredEventListener.cs
new file mode 100644
index 0000000..a21c3b1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Register/InvokedRegisteredEventListener.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Threading.Tasks;
+using Impostor.Api.Events;
+
+namespace Impostor.Server.Events.Register
+{
+ internal class InvokedRegisteredEventListener : IRegisteredEventListener
+ {
+ private readonly IRegisteredEventListener _innerObject;
+ private readonly Func<Func<Task>, Task> _invoker;
+
+ public InvokedRegisteredEventListener(IRegisteredEventListener innerObject, Func<Func<Task>, Task> invoker)
+ {
+ _innerObject = innerObject;
+ _invoker = invoker;
+ }
+
+ public Type EventType => _innerObject.EventType;
+
+ public EventPriority Priority => _innerObject.Priority;
+
+ public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider)
+ {
+ return new ValueTask(_invoker(() => _innerObject.InvokeAsync(eventHandler, @event, provider).AsTask()));
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/ManualRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/ManualRegisteredEventListener.cs
new file mode 100644
index 0000000..e81e8f8
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Register/ManualRegisteredEventListener.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Threading.Tasks;
+using Impostor.Api.Events;
+
+namespace Impostor.Server.Events.Register
+{
+ internal class ManualRegisteredEventListener : IRegisteredEventListener
+ {
+ public Type EventType { get; } = typeof(object);
+
+ private readonly IManualEventListener _manualEventListener;
+
+ public ManualRegisteredEventListener(IManualEventListener manualEventListener)
+ {
+ _manualEventListener = manualEventListener;
+ }
+
+ public EventPriority Priority => _manualEventListener.Priority;
+
+ public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider)
+ {
+ if (@event is IEvent typedEvent)
+ {
+ return _manualEventListener.Execute(typedEvent);
+ }
+
+ return ValueTask.CompletedTask;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/RegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/RegisteredEventListener.cs
new file mode 100644
index 0000000..120a45e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Register/RegisteredEventListener.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Threading.Tasks;
+using Impostor.Api.Events;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Impostor.Server.Events.Register
+{
+ internal class RegisteredEventListener : IRegisteredEventListener
+ {
+ private static readonly PropertyInfo IsCancelledProperty = typeof(IEventCancelable).GetProperty(nameof(IEventCancelable.IsCancelled))!;
+
+ private static readonly ConcurrentDictionary<Type, RegisteredEventListener[]> Instances = new ConcurrentDictionary<Type, RegisteredEventListener[]>();
+ private readonly Func<object, object, IServiceProvider, ValueTask> _invoker;
+ private readonly Type _eventListenerType;
+
+ public RegisteredEventListener(Type eventType, MethodInfo method, EventListenerAttribute attribute, Type eventListenerType)
+ {
+ EventType = eventType;
+ _eventListenerType = eventListenerType;
+ Priority = attribute.Priority;
+ IgnoreCancelled = attribute.IgnoreCancelled;
+ Method = method.GetFriendlyName(showParameters: false);
+ _invoker = CreateInvoker(method, attribute.IgnoreCancelled);
+ }
+
+ public Type EventType { get; }
+
+ public EventPriority Priority { get; }
+
+ public int PriorityOrder { get; set; }
+
+ public bool IgnoreCancelled { get; }
+
+ public string Method { get; }
+
+ public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider)
+ {
+ return _invoker(eventHandler, @event, provider);
+ }
+
+ private Func<object, object, IServiceProvider, ValueTask> CreateInvoker(MethodInfo method, bool ignoreCancelled)
+ {
+ var instance = Expression.Parameter(typeof(object), "instance");
+ var eventParameter = Expression.Parameter(typeof(object), "event");
+ var provider = Expression.Parameter(typeof(IServiceProvider), "provider");
+ var @event = Expression.Convert(eventParameter, EventType);
+
+ var getRequiredService = typeof(ServiceProviderServiceExtensions)
+ .GetMethod("GetRequiredService", new[] { typeof(IServiceProvider) });
+
+ if (getRequiredService == null)
+ {
+ throw new InvalidOperationException("The method GetRequiredService could not be found.");
+ }
+
+ var methodArguments = method.GetParameters();
+ var arguments = new Expression[methodArguments.Length];
+
+ for (var i = 0; i < methodArguments.Length; i++)
+ {
+ var methodArgument = methodArguments[i];
+
+ if (typeof(IEvent).IsAssignableFrom(methodArgument.ParameterType)
+ && methodArgument.ParameterType.IsAssignableFrom(EventType))
+ {
+ arguments[i] = @event;
+ }
+ else
+ {
+ arguments[i] = Expression.Call(
+ getRequiredService.MakeGenericMethod(methodArgument.ParameterType),
+ provider);
+ }
+ }
+
+ var returnTarget = Expression.Label(typeof(ValueTask));
+
+ Expression invoke = Expression.Call(Expression.Convert(instance, _eventListenerType), method, arguments);
+
+ if (method.ReturnType == typeof(void))
+ {
+ if (!ignoreCancelled && typeof(IEventCancelable).IsAssignableFrom(EventType))
+ {
+ invoke = Expression.Block(
+ Expression.IfThenElse(
+ Expression.Property(@event, IsCancelledProperty),
+ Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))),
+ Expression.Block(
+ invoke,
+ Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))))),
+ Expression.Label(returnTarget, Expression.Default(typeof(ValueTask))));
+ }
+ else
+ {
+ invoke = Expression.Block(
+ invoke,
+ Expression.Label(returnTarget, Expression.Default(typeof(ValueTask))));
+ }
+ }
+ else if (method.ReturnType == typeof(ValueTask))
+ {
+ if (!ignoreCancelled && typeof(IEventCancelable).IsAssignableFrom(EventType))
+ {
+ invoke = Expression.Block(
+ Expression.IfThenElse(
+ Expression.Property(@event, IsCancelledProperty),
+ Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))),
+ Expression.Return(returnTarget, invoke)),
+ Expression.Label(returnTarget, Expression.Default(typeof(ValueTask))));
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException($"The method {method.GetFriendlyName()} must return void or ValueTask.");
+ }
+
+ return Expression.Lambda<Func<object, object, IServiceProvider, ValueTask>>(invoke, instance, eventParameter, provider)
+ .Compile();
+ }
+
+ public static IReadOnlyList<RegisteredEventListener> FromType(Type type)
+ {
+ return Instances.GetOrAdd(type, t =>
+ {
+ return t.GetMethods()
+ .Where(m => !m.IsStatic && m.GetCustomAttributes(typeof(EventListenerAttribute), false).Any())
+ .SelectMany(m => FromMethod(t, m))
+ .ToArray();
+ });
+ }
+
+ public static IEnumerable<RegisteredEventListener> FromMethod(Type listenerType, MethodInfo methodType)
+ {
+ // Get the return type.
+ var returnType = methodType.ReturnType;
+
+ if (returnType != typeof(void) && returnType != typeof(ValueTask))
+ {
+ throw new InvalidOperationException($"The method {methodType.GetFriendlyName()} does not return void or ValueTask.");
+ }
+
+ // Register the event.
+ foreach (var attribute in methodType.GetCustomAttributes<EventListenerAttribute>(false))
+ {
+ var eventType = attribute.Event;
+
+ if (eventType == null)
+ {
+ if (methodType.GetParameters().Length == 0 || !typeof(IEvent).IsAssignableFrom(methodType.GetParameters()[0].ParameterType))
+ {
+ throw new InvalidOperationException($"The first parameter of the method {methodType.GetFriendlyName()} should be the type {nameof(IEvent)}.");
+ }
+
+ eventType = methodType.GetParameters()[0].ParameterType;
+ }
+
+ yield return new RegisteredEventListener(eventType, methodType, attribute, listenerType);
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/TemporaryEventRegister.cs b/Impostor-dev/src/Impostor.Server/Events/Register/TemporaryEventRegister.cs
new file mode 100644
index 0000000..1446ad1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Register/TemporaryEventRegister.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+
+namespace Impostor.Server.Events.Register
+{
+ internal class TemporaryEventRegister
+ {
+ private readonly ConcurrentDictionary<int, IRegisteredEventListener> _callbacks;
+ private int _idLast;
+
+ public TemporaryEventRegister()
+ {
+ _callbacks = new ConcurrentDictionary<int, IRegisteredEventListener>();
+ }
+
+ public IEnumerable<IRegisteredEventListener> GetEventListeners()
+ {
+ return _callbacks.Select(i => i.Value);
+ }
+
+ public IDisposable Add(IRegisteredEventListener callback)
+ {
+ var id = Interlocked.Increment(ref _idLast);
+
+ if (!_callbacks.TryAdd(id, callback))
+ {
+ Debug.Fail("Failed to register the event listener");
+ }
+
+ return new UnregisterEvent(this, id);
+ }
+
+ private void Remove(int id)
+ {
+ _callbacks.TryRemove(id, out _);
+ }
+
+ private class UnregisterEvent : IDisposable
+ {
+ private readonly TemporaryEventRegister _register;
+ private readonly int _id;
+
+ public UnregisterEvent(TemporaryEventRegister register, int id)
+ {
+ _register = register;
+ _id = id;
+ }
+
+ public void Dispose()
+ {
+ _register.Remove(_id);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/WrappedRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/WrappedRegisteredEventListener.cs
new file mode 100644
index 0000000..dd668c5
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Events/Register/WrappedRegisteredEventListener.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Threading.Tasks;
+using Impostor.Api.Events;
+
+namespace Impostor.Server.Events.Register
+{
+ internal class WrappedRegisteredEventListener : IRegisteredEventListener
+ {
+ private readonly IRegisteredEventListener _innerObject;
+ private readonly object _object;
+
+ public WrappedRegisteredEventListener(IRegisteredEventListener innerObject, object o)
+ {
+ _innerObject = innerObject;
+ _object = o;
+ }
+
+ public Type EventType => _innerObject.EventType;
+
+ public EventPriority Priority => _innerObject.Priority;
+
+ public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider)
+ {
+ return _innerObject.InvokeAsync(_object, @event, provider);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Extensions/MessageReaderExtensions.cs b/Impostor-dev/src/Impostor.Server/Extensions/MessageReaderExtensions.cs
new file mode 100644
index 0000000..5f25e89
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Extensions/MessageReaderExtensions.cs
@@ -0,0 +1,15 @@
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Net.Inner;
+using Impostor.Server.Net.State;
+
+namespace Impostor.Server
+{
+ internal static class MessageReaderExtensions
+ {
+ public static T ReadNetObject<T>(this IMessageReader reader, Game game)
+ where T : InnerNetObject
+ {
+ return game.FindObjectByNetId<T>(reader.ReadPackedUInt32());
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Extensions/NodeLocatorExtensions.cs b/Impostor-dev/src/Impostor.Server/Extensions/NodeLocatorExtensions.cs
new file mode 100644
index 0000000..370bca6
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Extensions/NodeLocatorExtensions.cs
@@ -0,0 +1,13 @@
+using System.Threading.Tasks;
+using Impostor.Server.Net.Redirector;
+
+namespace Impostor.Server
+{
+ public static class NodeLocatorExtensions
+ {
+ public static async ValueTask<bool> ExistsAsync(this INodeLocator nodeLocator, string gameCode)
+ {
+ return await nodeLocator.FindAsync(gameCode) != null;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Extensions/TypeExtensions.cs b/Impostor-dev/src/Impostor.Server/Extensions/TypeExtensions.cs
new file mode 100644
index 0000000..55d42fb
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Extensions/TypeExtensions.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+
+namespace Impostor.Server
+{
+ internal static class TypeExtensions
+ {
+ /// <summary>
+ /// Get the friendly name for the type.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>The friendly name.</returns>
+ [SuppressMessage("ReSharper", "SA1503", Justification = "Readability")]
+ public static string GetFriendlyName(this Type type)
+ {
+ if (type == null)
+ return "null";
+ if (type == typeof(int))
+ return "int";
+ if (type == typeof(short))
+ return "short";
+ if (type == typeof(byte))
+ return "byte";
+ if (type == typeof(bool))
+ return "bool";
+ if (type == typeof(long))
+ return "long";
+ if (type == typeof(float))
+ return "float";
+ if (type == typeof(double))
+ return "double";
+ if (type == typeof(decimal))
+ return "decimal";
+ if (type == typeof(string))
+ return "string";
+ if (type.IsGenericType)
+ return type.Name.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(GetFriendlyName).ToArray()) + ">";
+ return type.Name;
+ }
+
+ /// <summary>
+ /// Get the friendly name for the method.
+ /// </summary>
+ /// <param name="method">The method.</param>
+ /// <param name="showParameters">True if the parameters should be included in the name.</param>
+ /// <returns>Friendly name of the method</returns>
+ public static string GetFriendlyName(this MethodBase method, bool showParameters = true)
+ {
+ var str = method.Name;
+
+ if (method.DeclaringType != null)
+ {
+ str = method.DeclaringType.GetFriendlyName() + '.' + str;
+ }
+
+ if (showParameters)
+ {
+ var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.GetFriendlyName()));
+ str += $"({parameters})";
+ }
+
+ return str;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj
new file mode 100644
index 0000000..ad5a6db
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj
@@ -0,0 +1,58 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ <RuntimeIdentifiers>win-x64;linux-x64;linux-arm;linux-arm64;osx-x64</RuntimeIdentifiers>
+ <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
+ <ApplicationIcon>icon.ico</ApplicationIcon>
+ <CodeAnalysisRuleSet>ProjectRules.ruleset</CodeAnalysisRuleSet>
+ <Nullable>enable</Nullable>
+ <SelfContained>false</SelfContained>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <AssemblyName>Impostor.Server</AssemblyName>
+ <AssemblyTitle>Impostor.Server</AssemblyTitle>
+ <Product>Impostor.Server</Product>
+ <Copyright>Copyright © AeonLucid 2020</Copyright>
+ <Version>1.0.0</Version>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
+ <ProjectReference Include="..\Impostor.Hazel\Impostor.Hazel.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.0" />
+ <PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" />
+ <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <Content Include="config.json">
+ <CopyToPublishDirectory>Always</CopyToPublishDirectory>
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+ </Content>
+ <Content Include="config.*.json">
+ <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+ </Content>
+ <Content Include="config-full.json">
+ <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+ <CopyToOutputDirectory>Never</CopyToOutputDirectory>
+ <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+ </Content>
+ </ItemGroup>
+
+</Project> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj.DotSettings b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj.DotSettings
new file mode 100644
index 0000000..5c07b47
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj.DotSettings
@@ -0,0 +1,3 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cgame/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Client.cs b/Impostor-dev/src/Impostor.Server/Net/Client.cs
new file mode 100644
index 0000000..87a1bb4
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Client.cs
@@ -0,0 +1,338 @@
+using System;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Api.Net.Messages.C2S;
+using Impostor.Api.Net.Messages.S2C;
+using Impostor.Hazel;
+using Impostor.Server.Config;
+using Impostor.Server.Net.Manager;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Net
+{
+ internal class Client : ClientBase
+ {
+ private readonly ILogger<Client> _logger;
+ private readonly AntiCheatConfig _antiCheatConfig;
+ private readonly ClientManager _clientManager;
+ private readonly GameManager _gameManager;
+
+ public Client(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, IHazelConnection connection)
+ : base(name, connection)
+ {
+ _logger = logger;
+ _antiCheatConfig = antiCheatOptions.Value;
+ _clientManager = clientManager;
+ _gameManager = gameManager;
+ }
+
+ public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType)
+ {
+ var flag = reader.Tag;
+
+ _logger.LogTrace("[{0}] Server got {1}.", Id, flag);
+
+ switch (flag)
+ {
+ case MessageFlags.HostGame:
+ {
+ // Read game settings.
+ var gameInfo = Message00HostGameC2S.Deserialize(reader);
+
+ // Create game.
+ var game = await _gameManager.CreateAsync(gameInfo);
+
+ // Code in the packet below will be used in JoinGame.
+ using (var writer = MessageWriter.Get(MessageType.Reliable))
+ {
+ Message00HostGameS2C.Serialize(writer, game.Code);
+ await Connection.SendAsync(writer);
+ }
+
+ break;
+ }
+
+ case MessageFlags.JoinGame:
+ {
+ Message01JoinGameC2S.Deserialize(
+ reader,
+ out var gameCode,
+ out _);
+
+ var game = _gameManager.Find(gameCode);
+ if (game == null)
+ {
+ await DisconnectAsync(DisconnectReason.GameMissing);
+ return;
+ }
+
+ var result = await game.AddClientAsync(this);
+
+ switch (result.Error)
+ {
+ case GameJoinError.None:
+ break;
+ case GameJoinError.InvalidClient:
+ await DisconnectAsync(DisconnectReason.Custom, "Client is in an invalid state.");
+ break;
+ case GameJoinError.Banned:
+ await DisconnectAsync(DisconnectReason.Banned);
+ break;
+ case GameJoinError.GameFull:
+ await DisconnectAsync(DisconnectReason.GameFull);
+ break;
+ case GameJoinError.InvalidLimbo:
+ await DisconnectAsync(DisconnectReason.Custom, "Invalid limbo state while joining.");
+ break;
+ case GameJoinError.GameStarted:
+ await DisconnectAsync(DisconnectReason.GameStarted);
+ break;
+ case GameJoinError.GameDestroyed:
+ await DisconnectAsync(DisconnectReason.Custom, DisconnectMessages.Destroyed);
+ break;
+ case GameJoinError.Custom:
+ await DisconnectAsync(DisconnectReason.Custom, result.Message);
+ break;
+ default:
+ await DisconnectAsync(DisconnectReason.Custom, "Unknown error.");
+ break;
+ }
+
+ break;
+ }
+
+ case MessageFlags.StartGame:
+ {
+ if (!IsPacketAllowed(reader, true))
+ {
+ return;
+ }
+
+ await Player.Game.HandleStartGame(reader);
+ break;
+ }
+
+ // No idea how this flag is triggered.
+ case MessageFlags.RemoveGame:
+ break;
+
+ case MessageFlags.RemovePlayer:
+ {
+ if (!IsPacketAllowed(reader, true))
+ {
+ return;
+ }
+
+ Message04RemovePlayerC2S.Deserialize(
+ reader,
+ out var playerId,
+ out var reason);
+
+ await Player.Game.HandleRemovePlayer(playerId, (DisconnectReason)reason);
+ break;
+ }
+
+ case MessageFlags.GameData:
+ case MessageFlags.GameDataTo:
+ {
+ if (!IsPacketAllowed(reader, false))
+ {
+ return;
+ }
+
+ var toPlayer = flag == MessageFlags.GameDataTo;
+
+ // Handle packet.
+ using var readerCopy = reader.Copy();
+
+ // TODO: Return value, either a bool (to cancel) or a writer (to cancel (null) or modify/overwrite).
+ try
+ {
+ var verified = await Player.Game.HandleGameDataAsync(readerCopy, Player, toPlayer);
+ if (verified)
+ {
+ // Broadcast packet to all other players.
+ using (var writer = MessageWriter.Get(messageType))
+ {
+ if (toPlayer)
+ {
+ var target = reader.ReadPackedInt32();
+ reader.CopyTo(writer);
+ await Player.Game.SendToAsync(writer, target);
+ }
+ else
+ {
+ reader.CopyTo(writer);
+ await Player.Game.SendToAllExceptAsync(writer, Id);
+ }
+ }
+ }
+ }
+ catch (ImpostorCheatException e)
+ {
+ if (_antiCheatConfig.BanIpFromGame)
+ {
+ Player.Game.BanIp(Connection.EndPoint.Address);
+ }
+
+ await DisconnectAsync(DisconnectReason.Hacking, e.Message);
+ }
+
+ break;
+ }
+
+ case MessageFlags.EndGame:
+ {
+ if (!IsPacketAllowed(reader, true))
+ {
+ return;
+ }
+
+ Message08EndGameC2S.Deserialize(
+ reader,
+ out var gameOverReason);
+
+ await Player.Game.HandleEndGame(reader, gameOverReason);
+ break;
+ }
+
+ case MessageFlags.AlterGame:
+ {
+ if (!IsPacketAllowed(reader, true))
+ {
+ return;
+ }
+
+ Message10AlterGameC2S.Deserialize(
+ reader,
+ out var gameTag,
+ out var value);
+
+ if (gameTag != AlterGameTags.ChangePrivacy)
+ {
+ return;
+ }
+
+ await Player.Game.HandleAlterGame(reader, Player, value);
+ break;
+ }
+
+ case MessageFlags.KickPlayer:
+ {
+ if (!IsPacketAllowed(reader, true))
+ {
+ return;
+ }
+
+ Message11KickPlayerC2S.Deserialize(
+ reader,
+ out var playerId,
+ out var isBan);
+
+ await Player.Game.HandleKickPlayer(playerId, isBan);
+ break;
+ }
+
+ case MessageFlags.GetGameListV2:
+ {
+ Message16GetGameListC2S.Deserialize(reader, out var options);
+ await OnRequestGameListAsync(options);
+ break;
+ }
+
+ default:
+ _logger.LogWarning("Server received unknown flag {0}.", flag);
+ break;
+ }
+
+#if DEBUG
+ if (flag != MessageFlags.GameData &&
+ flag != MessageFlags.GameDataTo &&
+ flag != MessageFlags.EndGame &&
+ reader.Position < reader.Length)
+ {
+ _logger.LogWarning(
+ "Server did not consume all bytes from {0} ({1} < {2}).",
+ flag,
+ reader.Position,
+ reader.Length);
+ }
+#endif
+ }
+
+ public override async ValueTask HandleDisconnectAsync(string reason)
+ {
+ try
+ {
+ if (Player != null)
+ {
+ await Player.Game.HandleRemovePlayer(Id, DisconnectReason.ExitGame);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Exception caught in client disconnection.");
+ }
+
+ _logger.LogInformation("Client {0} disconnecting, reason: {1}", Id, reason);
+ _clientManager.Remove(this);
+ }
+
+ private bool IsPacketAllowed(IMessageReader message, bool hostOnly)
+ {
+ if (Player == null)
+ {
+ return false;
+ }
+
+ var game = Player.Game;
+
+ // GameCode must match code of the current game assigned to the player.
+ if (message.ReadInt32() != game.Code)
+ {
+ return false;
+ }
+
+ // Some packets should only be sent by the host of the game.
+ if (hostOnly)
+ {
+ if (game.HostId == Id)
+ {
+ return true;
+ }
+
+ _logger.LogWarning("[{0}] Client sent packet only allowed by the host ({1}).", Id, game.HostId);
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Triggered when the connected client requests the game listing.
+ /// </summary>
+ /// <param name="options">
+ /// All options given.
+ /// At this moment, the client can only specify the map, impostor count and chat language.
+ /// </param>
+ private ValueTask OnRequestGameListAsync(GameOptionsData options)
+ {
+ using var message = MessageWriter.Get(MessageType.Reliable);
+
+ var games = _gameManager.FindListings((MapFlags)options.MapId, options.NumImpostors, options.Keywords);
+
+ var skeldGameCount = _gameManager.GetGameCount(MapFlags.Skeld);
+ var miraHqGameCount = _gameManager.GetGameCount(MapFlags.MiraHQ);
+ var polusGameCount = _gameManager.GetGameCount(MapFlags.Polus);
+
+ Message16GetGameListS2C.Serialize(message, skeldGameCount, miraHqGameCount, polusGameCount, games);
+
+ return Connection.SendAsync(message);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/ClientBase.cs b/Impostor-dev/src/Impostor.Server/Net/ClientBase.cs
new file mode 100644
index 0000000..5279192
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/ClientBase.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Api.Net.Messages.S2C;
+using Impostor.Hazel;
+using Impostor.Server.Net.State;
+
+namespace Impostor.Server.Net
+{
+ internal abstract class ClientBase : IClient
+ {
+ protected ClientBase(string name, IHazelConnection connection)
+ {
+ Name = name;
+ Connection = connection;
+ Items = new ConcurrentDictionary<object, object>();
+ }
+
+ public int Id { get; set; }
+
+ public string Name { get; }
+
+ public IHazelConnection Connection { get; }
+
+ public IDictionary<object, object> Items { get; }
+
+ public ClientPlayer? Player { get; set; }
+
+ IClientPlayer? IClient.Player => Player;
+
+ public abstract ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType);
+
+ public abstract ValueTask HandleDisconnectAsync(string reason);
+
+
+ public async ValueTask DisconnectAsync(DisconnectReason reason, string? message = null)
+ {
+ if (!Connection.IsConnected)
+ {
+ return;
+ }
+
+ using var packet = MessageWriter.Get(MessageType.Reliable);
+ Message01JoinGameS2C.SerializeError(packet, false, reason, message);
+
+ await Connection.SendAsync(packet);
+
+ // Need this to show the correct message, otherwise it shows a generic disconnect message.
+ await Task.Delay(TimeSpan.FromMilliseconds(250));
+
+ await Connection.DisconnectAsync(message ?? reason.ToString());
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs b/Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs
new file mode 100644
index 0000000..ad1fdc7
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs
@@ -0,0 +1,24 @@
+using System;
+using Impostor.Api.Net;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Impostor.Server.Net.Factories
+{
+ internal class ClientFactory<TClient> : IClientFactory
+ where TClient : ClientBase
+ {
+ private readonly IServiceProvider _serviceProvider;
+
+ public ClientFactory(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public ClientBase Create(IHazelConnection connection, string name, int clientVersion)
+ {
+ var client = ActivatorUtilities.CreateInstance<TClient>(_serviceProvider, name, connection);
+ connection.Client = client;
+ return client;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs b/Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs
new file mode 100644
index 0000000..6859ae3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs
@@ -0,0 +1,9 @@
+using Impostor.Api.Net;
+
+namespace Impostor.Server.Net.Factories
+{
+ internal interface IClientFactory
+ {
+ ClientBase Create(IHazelConnection connection, string name, int clientVersion);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs b/Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs
new file mode 100644
index 0000000..2a0553f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs
@@ -0,0 +1,12 @@
+using Impostor.Api.Games;
+
+namespace Impostor.Server.Net
+{
+ public class GameCodeFactory : IGameCodeFactory
+ {
+ public GameCode Create()
+ {
+ return GameCode.Create();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs b/Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs
new file mode 100644
index 0000000..45d2be8
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs
@@ -0,0 +1,74 @@
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Hazel;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Hazel
+{
+ internal class HazelConnection : IHazelConnection
+ {
+ private readonly ILogger<HazelConnection> _logger;
+
+ public HazelConnection(Connection innerConnection, ILogger<HazelConnection> logger)
+ {
+ _logger = logger;
+ InnerConnection = innerConnection;
+ innerConnection.DataReceived = ConnectionOnDataReceived;
+ innerConnection.Disconnected = ConnectionOnDisconnected;
+ }
+
+ public Connection InnerConnection { get; }
+
+ public IPEndPoint EndPoint => InnerConnection.EndPoint;
+
+ public bool IsConnected => InnerConnection.State == ConnectionState.Connected;
+
+ public IClient Client { get; set; }
+
+ public ValueTask SendAsync(IMessageWriter writer)
+ {
+ return InnerConnection.SendAsync(writer);
+ }
+
+ public ValueTask DisconnectAsync(string reason)
+ {
+ return InnerConnection.Disconnect(reason);
+ }
+
+ public void DisposeInnerConnection()
+ {
+ InnerConnection.Dispose();
+ }
+
+ private async ValueTask ConnectionOnDisconnected(DisconnectedEventArgs e)
+ {
+ if (Client != null)
+ {
+ await Client.HandleDisconnectAsync(e.Reason);
+ }
+ }
+
+ private async ValueTask ConnectionOnDataReceived(DataReceivedEventArgs e)
+ {
+ if (Client == null)
+ {
+ return;
+ }
+
+ while (true)
+ {
+ if (e.Message.Position >= e.Message.Length)
+ {
+ break;
+ }
+
+ using (var message = e.Message.ReadMessage())
+ {
+ await Client.HandleMessageAsync(message, e.Type);
+ }
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs
new file mode 100644
index 0000000..fa9ddb8
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs
@@ -0,0 +1,13 @@
+namespace Impostor.Server.Net.Inner
+{
+ public static class GameDataTag
+ {
+ public const byte DataFlag = 1;
+ public const byte RpcFlag = 2;
+ public const byte SpawnFlag = 4;
+ public const byte DespawnFlag = 5;
+ public const byte SceneChangeFlag = 6;
+ public const byte ReadyFlag = 7;
+ public const byte ChangeSettingsFlag = 8;
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs
new file mode 100644
index 0000000..9b53d70
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+
+namespace Impostor.Server.Net.Inner
+{
+ internal class GameObject
+ {
+ public GameObject()
+ {
+ Components = new List<object>();
+ }
+
+ protected List<object> Components { get; }
+
+ public List<T> GetComponentsInChildren<T>()
+ {
+ var result = new List<T>();
+
+ foreach (var component in Components)
+ {
+ if (component is T c)
+ {
+ result.Add(c);
+ }
+ }
+
+ return result;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs
new file mode 100644
index 0000000..78f1a55
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs
@@ -0,0 +1,31 @@
+using System.Threading.Tasks;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Net.State;
+
+namespace Impostor.Server.Net.Inner
+{
+ internal abstract class InnerNetObject : GameObject, IInnerNetObject
+ {
+ private const int HostInheritId = -2;
+
+ public uint NetId { get; internal set; }
+
+ public int OwnerId { get; internal set; }
+
+ public SpawnFlags SpawnFlags { get; internal set; }
+
+ public abstract ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader);
+
+ public abstract bool Serialize(IMessageWriter writer, bool initialState);
+
+ public abstract void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState);
+
+ public bool IsOwnedBy(IClientPlayer player)
+ {
+ return OwnerId == player.Client.Id ||
+ (OwnerId == HostInheritId && player.IsHost);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs
new file mode 100644
index 0000000..6cdd6b9
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs
@@ -0,0 +1,25 @@
+using System.Numerics;
+using System.Threading.Tasks;
+using Impostor.Api.Net.Inner.Objects.Components;
+
+namespace Impostor.Server.Net.Inner.Objects.Components
+{
+ internal partial class InnerCustomNetworkTransform : IInnerCustomNetworkTransform
+ {
+ public async ValueTask SnapToAsync(Vector2 position)
+ {
+ var minSid = (ushort)(_lastSequenceId + 5U);
+
+ // Snap in the server.
+ SnapTo(position, minSid);
+
+ // Broadcast to all clients.
+ using (var writer = _game.StartRpc(NetId, RpcCalls.SnapTo))
+ {
+ WriteVector2(writer, position);
+ writer.Write(_lastSequenceId);
+ await _game.FinishRpcAsync(writer);
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs
new file mode 100644
index 0000000..d261c11
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs
@@ -0,0 +1,148 @@
+using System.Numerics;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Net.State;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Inner.Objects.Components
+{
+ internal partial class InnerCustomNetworkTransform : InnerNetObject
+ {
+ private static readonly FloatRange XRange = new FloatRange(-40f, 40f);
+ private static readonly FloatRange YRange = new FloatRange(-40f, 40f);
+
+ private readonly ILogger<InnerCustomNetworkTransform> _logger;
+ private readonly InnerPlayerControl _playerControl;
+ private readonly Game _game;
+
+ private ushort _lastSequenceId;
+ private Vector2 _targetSyncPosition;
+ private Vector2 _targetSyncVelocity;
+
+ public InnerCustomNetworkTransform(ILogger<InnerCustomNetworkTransform> logger, InnerPlayerControl playerControl, Game game)
+ {
+ _logger = logger;
+ _playerControl = playerControl;
+ _game = game;
+ }
+
+ private static bool SidGreaterThan(ushort newSid, ushort prevSid)
+ {
+ var num = (ushort)(prevSid + (uint) short.MaxValue);
+
+ return (int) prevSid < (int) num
+ ? newSid > prevSid && newSid <= num
+ : newSid > prevSid || newSid <= num;
+ }
+
+ private static void WriteVector2(IMessageWriter writer, Vector2 vec)
+ {
+ writer.Write((ushort)(XRange.ReverseLerp(vec.X) * (double) ushort.MaxValue));
+ writer.Write((ushort)(YRange.ReverseLerp(vec.Y) * (double) ushort.MaxValue));
+ }
+
+ private static Vector2 ReadVector2(IMessageReader reader)
+ {
+ var v1 = reader.ReadUInt16() / (float) ushort.MaxValue;
+ var v2 = reader.ReadUInt16() / (float) ushort.MaxValue;
+
+ return new Vector2(XRange.Lerp(v1), YRange.Lerp(v2));
+ }
+
+ public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
+ {
+ if (call == RpcCalls.SnapTo)
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SnapTo)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SnapTo)} to a specific player instead of broadcast");
+ }
+
+ if (!sender.Character.PlayerInfo.IsImpostor)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SnapTo)} as crewmate");
+ }
+
+ SnapTo(ReadVector2(reader), reader.ReadUInt16());
+ }
+ else
+ {
+ _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerCustomNetworkTransform), call);
+ }
+
+ return default;
+ }
+
+ public override bool Serialize(IMessageWriter writer, bool initialState)
+ {
+ if (initialState)
+ {
+ writer.Write(_lastSequenceId);
+ WriteVector2(writer, _targetSyncPosition);
+ WriteVector2(writer, _targetSyncVelocity);
+ return true;
+ }
+
+ // TODO: DirtyBits == 0 return false.
+ _lastSequenceId++;
+
+ writer.Write(_lastSequenceId);
+ WriteVector2(writer, _targetSyncPosition);
+ WriteVector2(writer, _targetSyncVelocity);
+ return true;
+ }
+
+ public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
+ {
+ var sequenceId = reader.ReadUInt16();
+
+ if (initialState)
+ {
+ _lastSequenceId = sequenceId;
+ _targetSyncPosition = ReadVector2(reader);
+ _targetSyncVelocity = ReadVector2(reader);
+ }
+ else
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client attempted to send unowned {nameof(InnerCustomNetworkTransform)} data");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client attempted to send {nameof(InnerCustomNetworkTransform)} data to a specific player, must be broadcast");
+ }
+
+ if (!SidGreaterThan(sequenceId, _lastSequenceId))
+ {
+ return;
+ }
+
+ _lastSequenceId = sequenceId;
+ _targetSyncPosition = ReadVector2(reader);
+ _targetSyncVelocity = ReadVector2(reader);
+ }
+ }
+
+ private void SnapTo(Vector2 position, ushort minSid)
+ {
+ if (!SidGreaterThan(minSid, _lastSequenceId))
+ {
+ return;
+ }
+
+ _lastSequenceId = minSid;
+ _targetSyncPosition = position;
+ _targetSyncVelocity = Vector2.Zero;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs
new file mode 100644
index 0000000..6af54a0
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs
@@ -0,0 +1,8 @@
+using Impostor.Api.Net.Inner.Objects.Components;
+
+namespace Impostor.Server.Net.Inner.Objects.Components
+{
+ internal partial class InnerPlayerPhysics : IInnerPlayerPhysics
+ {
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs
new file mode 100644
index 0000000..29bc996
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Events.Managers;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Events.Player;
+using Impostor.Server.Net.State;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Inner.Objects.Components
+{
+ internal partial class InnerPlayerPhysics : InnerNetObject
+ {
+ private readonly ILogger<InnerPlayerPhysics> _logger;
+ private readonly InnerPlayerControl _playerControl;
+ private readonly IEventManager _eventManager;
+ private readonly Game _game;
+
+ public InnerPlayerPhysics(ILogger<InnerPlayerPhysics> logger, InnerPlayerControl playerControl, IEventManager eventManager, Game game)
+ {
+ _logger = logger;
+ _playerControl = playerControl;
+ _eventManager = eventManager;
+ _game = game;
+ }
+
+ public override async ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
+ {
+ if (call != RpcCalls.EnterVent && call != RpcCalls.ExitVent)
+ {
+ _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerPlayerPhysics), call);
+ return;
+ }
+
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {call} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {call} to a specific player instead of broadcast");
+ }
+
+ if (!sender.Character.PlayerInfo.IsImpostor)
+ {
+ throw new ImpostorCheatException($"Client sent {call} as crewmate");
+ }
+
+ var ventId = reader.ReadPackedUInt32();
+ var ventEnter = call == RpcCalls.EnterVent;
+
+ await _eventManager.CallAsync(new PlayerVentEvent(_game, sender, _playerControl, (VentLocation)ventId, ventEnter));
+
+ return;
+ }
+
+ public override bool Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs
new file mode 100644
index 0000000..58b9b54
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Net.State;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Inner.Objects.Components
+{
+ internal class InnerVoteBanSystem : InnerNetObject, IInnerVoteBanSystem
+ {
+ private readonly ILogger<InnerVoteBanSystem> _logger;
+ private readonly Dictionary<int, int[]> _votes;
+
+ public InnerVoteBanSystem(ILogger<InnerVoteBanSystem> logger)
+ {
+ _logger = logger;
+ _votes = new Dictionary<int, int[]>();
+ }
+
+ public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
+ {
+ if (call != RpcCalls.AddVote)
+ {
+ _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerVoteBanSystem), call);
+ return default;
+ }
+
+ var clientId = reader.ReadInt32();
+ if (clientId != sender.Client.Id)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.AddVote)} as other client");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to wrong destinition, must be broadcast");
+ }
+
+ var targetClientId = reader.ReadInt32();
+
+ // TODO: Use.
+
+ return default;
+ }
+
+ public override bool Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerShipStatus)} as non-host");
+ }
+
+ var votes = _votes;
+ var unknown = reader.ReadByte();
+ if (unknown != 0)
+ {
+ for (var i = 0; i < unknown; i++)
+ {
+ var v4 = reader.ReadInt32();
+ if (v4 == 0)
+ {
+ break;
+ }
+
+ if (!votes.TryGetValue(v4, out var v12))
+ {
+ v12 = new int[3];
+ votes[v4] = v12;
+ }
+
+ for (var j = 0; j < 3; j++)
+ {
+ v12[j] = reader.ReadPackedInt32();
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs
new file mode 100644
index 0000000..29ca50a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs
@@ -0,0 +1,30 @@
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Inner.Objects;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal partial class InnerGameData
+ {
+ public class TaskInfo : ITaskInfo
+ {
+ public uint Id { get; internal set; }
+
+ public bool Complete { get; internal set; }
+
+ public TaskTypes Type { get; internal set; }
+
+ public void Serialize(IMessageWriter writer)
+ {
+ writer.WritePacked((uint)Id);
+ writer.Write(Complete);
+ }
+
+ public void Deserialize(IMessageReader reader)
+ {
+ Id = reader.ReadPackedUInt32();
+ Complete = reader.ReadBoolean();
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs
new file mode 100644
index 0000000..ed68038
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs
@@ -0,0 +1,187 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Net.Inner.Objects.Components;
+using Impostor.Server.Net.State;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal partial class InnerGameData : InnerNetObject, IInnerGameData
+ {
+ private readonly ILogger<InnerGameData> _logger;
+ private readonly Game _game;
+ private readonly ConcurrentDictionary<byte, InnerPlayerInfo> _allPlayers;
+
+ public InnerGameData(ILogger<InnerGameData> logger, Game game, IServiceProvider serviceProvider)
+ {
+ _logger = logger;
+ _game = game;
+ _allPlayers = new ConcurrentDictionary<byte, InnerPlayerInfo>();
+
+ Components.Add(this);
+ Components.Add(ActivatorUtilities.CreateInstance<InnerVoteBanSystem>(serviceProvider));
+ }
+
+ public int PlayerCount => _allPlayers.Count;
+
+ public IReadOnlyDictionary<byte, InnerPlayerInfo> Players => _allPlayers;
+
+ public InnerPlayerInfo? GetPlayerById(byte id)
+ {
+ if (id == byte.MaxValue)
+ {
+ return null;
+ }
+
+ return _allPlayers.TryGetValue(id, out var player) ? player : null;
+ }
+
+ public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
+ {
+ switch (call)
+ {
+ case RpcCalls.SetTasks:
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} but was not a host");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} to a specific player instead of broadcast");
+ }
+
+ var playerId = reader.ReadByte();
+ var taskTypeIds = reader.ReadBytesAndSize();
+
+ SetTasks(playerId, taskTypeIds);
+ break;
+ }
+
+ case RpcCalls.UpdateGameData:
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} but was not a host");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} to a specific player instead of broadcast");
+ }
+
+ while (reader.Position < reader.Length)
+ {
+ using var message = reader.ReadMessage();
+ var player = GetPlayerById(message.Tag);
+ if (player != null)
+ {
+ player.Deserialize(message);
+ }
+ else
+ {
+ var playerInfo = new InnerPlayerInfo(message.Tag);
+
+ playerInfo.Deserialize(reader);
+
+ if (!_allPlayers.TryAdd(playerInfo.PlayerId, playerInfo))
+ {
+ throw new ImpostorException("Failed to add player to InnerGameData.");
+ }
+ }
+ }
+
+ break;
+ }
+
+ default:
+ {
+ _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerGameData), call);
+ break;
+ }
+ }
+
+ return default;
+ }
+
+ public override bool Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerGameData)} as non-host");
+ }
+
+ if (initialState)
+ {
+ var num = reader.ReadPackedInt32();
+
+ for (var i = 0; i < num; i++)
+ {
+ var playerId = reader.ReadByte();
+ var playerInfo = new InnerPlayerInfo(playerId);
+
+ playerInfo.Deserialize(reader);
+
+ if (!_allPlayers.TryAdd(playerInfo.PlayerId, playerInfo))
+ {
+ throw new ImpostorException("Failed to add player to InnerGameData.");
+ }
+ }
+ }
+ else
+ {
+ throw new NotImplementedException("This shouldn't happen, according to Among Us disassembly.");
+ }
+ }
+
+ internal void AddPlayer(InnerPlayerControl control)
+ {
+ var playerId = control.PlayerId;
+ var playerInfo = new InnerPlayerInfo(control.PlayerId);
+
+ if (_allPlayers.TryAdd(playerId, playerInfo))
+ {
+ control.PlayerInfo = playerInfo;
+ }
+ }
+
+ private void SetTasks(byte playerId, ReadOnlyMemory<byte> taskTypeIds)
+ {
+ var player = GetPlayerById(playerId);
+ if (player == null)
+ {
+ _logger.LogTrace("Could not set tasks for playerId {0}.", playerId);
+ return;
+ }
+
+ if (player.Disconnected)
+ {
+ return;
+ }
+
+ player.Tasks = new List<TaskInfo>(taskTypeIds.Length);
+
+ foreach (var taskId in taskTypeIds.ToArray())
+ {
+ player.Tasks.Add(new TaskInfo
+ {
+ Id = taskId,
+ });
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs
new file mode 100644
index 0000000..63448ed
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs
@@ -0,0 +1,36 @@
+using System.Threading.Tasks;
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Net.State;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal class InnerLobbyBehaviour : InnerNetObject, IInnerLobbyBehaviour
+ {
+ private readonly IGame _game;
+
+ public InnerLobbyBehaviour(IGame game)
+ {
+ _game = game;
+
+ Components.Add(this);
+ }
+
+ public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override bool Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs
new file mode 100644
index 0000000..5d120c5
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs
@@ -0,0 +1,9 @@
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal partial class InnerMeetingHud : IInnerMeetingHud
+ {
+
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs
new file mode 100644
index 0000000..fbf2510
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs
@@ -0,0 +1,49 @@
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal partial class InnerMeetingHud
+ {
+ public class PlayerVoteArea
+ {
+ private const byte VoteMask = 15;
+ private const byte ReportedBit = 32;
+ private const byte VotedBit = 64;
+ private const byte DeadBit = 128;
+
+ public PlayerVoteArea(InnerMeetingHud parent, byte targetPlayerId)
+ {
+ Parent = parent;
+ TargetPlayerId = targetPlayerId;
+ }
+
+ public InnerMeetingHud Parent { get; }
+
+ public byte TargetPlayerId { get; }
+
+ public bool IsDead { get; private set; }
+
+ public bool DidVote { get; private set; }
+
+ public bool DidReport { get; private set; }
+
+ public sbyte VotedFor { get; private set; }
+
+ internal void SetDead(bool didReport, bool isDead)
+ {
+ DidReport = didReport;
+ IsDead = isDead;
+ }
+
+ public void Deserialize(IMessageReader reader)
+ {
+ var num = reader.ReadByte();
+
+ VotedFor = (sbyte)((num & VoteMask) - 1);
+ IsDead = (num & DeadBit) > 0;
+ DidVote = (num & VotedBit) > 0;
+ DidReport = (num & ReportedBit) > 0;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs
new file mode 100644
index 0000000..c2bcb9d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Events.Managers;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Events.Meeting;
+using Impostor.Server.Events.Player;
+using Impostor.Server.Net.State;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal partial class InnerMeetingHud : InnerNetObject
+ {
+ private readonly ILogger<InnerMeetingHud> _logger;
+ private readonly IEventManager _eventManager;
+ private readonly Game _game;
+ private readonly GameNet _gameNet;
+ private PlayerVoteArea[] _playerStates;
+
+ public InnerMeetingHud(ILogger<InnerMeetingHud> logger, IEventManager eventManager, Game game)
+ {
+ _logger = logger;
+ _eventManager = eventManager;
+ _game = game;
+ _gameNet = game.GameNet;
+ _playerStates = null;
+
+ Components.Add(this);
+ }
+
+ public byte ReporterId { get; private set; }
+
+ private void PopulateButtons(byte reporter)
+ {
+ _playerStates = _gameNet.GameData.Players
+ .Select(x =>
+ {
+ var area = new PlayerVoteArea(this, x.Key);
+ area.SetDead(x.Value.PlayerId == reporter, x.Value.Disconnected || x.Value.IsDead);
+ return area;
+ })
+ .ToArray();
+ }
+
+ public override async ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
+ {
+ switch (call)
+ {
+ case RpcCalls.Close:
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Close)} but was not a host");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Close)} to a specific player instead of broadcast");
+ }
+
+ break;
+ }
+
+ case RpcCalls.VotingComplete:
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.VotingComplete)} but was not a host");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.VotingComplete)} to a specific player instead of broadcast");
+ }
+
+ var states = reader.ReadBytesAndSize();
+ var playerId = reader.ReadByte();
+ var tie = reader.ReadBoolean();
+
+ if (playerId != byte.MaxValue)
+ {
+ var player = _game.GameNet.GameData.GetPlayerById(playerId);
+ if (player != null)
+ {
+ player.Controller.Die(DeathReason.Exile);
+ await _eventManager.CallAsync(new PlayerExileEvent(_game, sender, player.Controller));
+ }
+ }
+
+ await _eventManager.CallAsync(new MeetingEndedEvent(_game, this));
+
+ break;
+ }
+
+ case RpcCalls.CastVote:
+ {
+ var srcPlayerId = reader.ReadByte();
+ if (srcPlayerId != sender.Character.PlayerId)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ // Host broadcasts vote to others.
+ if (sender.IsHost && target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to a specific player instead of broadcast");
+ }
+
+ // Player sends vote to host.
+ if (target == null || !target.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to wrong destinition, must be host");
+ }
+
+ var targetPlayerId = reader.ReadByte();
+ break;
+ }
+
+ default:
+ {
+ _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerMeetingHud), call);
+ break;
+ }
+ }
+ }
+
+ public override bool Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerMeetingHud)} as non-host");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client attempted to send {nameof(InnerMeetingHud)} data to a specific player, must be broadcast");
+ }
+
+ if (initialState)
+ {
+ PopulateButtons(0);
+
+ foreach (var playerState in _playerStates)
+ {
+ playerState.Deserialize(reader);
+
+ if (playerState.DidReport)
+ {
+ ReporterId = playerState.TargetPlayerId;
+ }
+ }
+ }
+ else
+ {
+ var num = reader.ReadPackedUInt32();
+
+ for (var i = 0; i < _playerStates.Length; i++)
+ {
+ if ((num & 1 << i) != 0)
+ {
+ _playerStates[i].Deserialize(reader);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs
new file mode 100644
index 0000000..0a7997d
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs
@@ -0,0 +1,138 @@
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Innersloth.Customization;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+using Impostor.Api.Net.Inner.Objects.Components;
+using Impostor.Server.Events.Player;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal partial class InnerPlayerControl : IInnerPlayerControl
+ {
+ IInnerPlayerPhysics IInnerPlayerControl.Physics => Physics;
+
+ IInnerCustomNetworkTransform IInnerPlayerControl.NetworkTransform => NetworkTransform;
+
+ IInnerPlayerInfo IInnerPlayerControl.PlayerInfo => PlayerInfo;
+
+ public async ValueTask SetNameAsync(string name)
+ {
+ PlayerInfo.PlayerName = name;
+
+ using var writer = _game.StartRpc(NetId, RpcCalls.SetName);
+ writer.Write(name);
+ await _game.FinishRpcAsync(writer);
+ }
+
+ public async ValueTask SetColorAsync(byte colorId)
+ {
+ PlayerInfo.ColorId = colorId;
+
+ using var writer = _game.StartRpc(NetId, RpcCalls.SetColor);
+ writer.Write(colorId);
+ await _game.FinishRpcAsync(writer);
+ }
+
+ public ValueTask SetColorAsync(ColorType colorType)
+ {
+ return SetColorAsync((byte)colorType);
+ }
+
+ public async ValueTask SetHatAsync(uint hatId)
+ {
+ PlayerInfo.HatId = hatId;
+
+ using var writer = _game.StartRpc(NetId, RpcCalls.SetHat);
+ writer.WritePacked(hatId);
+ await _game.FinishRpcAsync(writer);
+ }
+
+ public ValueTask SetHatAsync(HatType hatType)
+ {
+ return SetHatAsync((uint)hatType);
+ }
+
+ public async ValueTask SetPetAsync(uint petId)
+ {
+ PlayerInfo.PetId = petId;
+
+ using var writer = _game.StartRpc(NetId, RpcCalls.SetPet);
+ writer.WritePacked(petId);
+ await _game.FinishRpcAsync(writer);
+ }
+
+ public ValueTask SetPetAsync(PetType petType)
+ {
+ return SetPetAsync((uint)petType);
+ }
+
+ public async ValueTask SetSkinAsync(uint skinId)
+ {
+ PlayerInfo.SkinId = skinId;
+
+ using var writer = _game.StartRpc(NetId, RpcCalls.SetSkin);
+ writer.WritePacked(skinId);
+ await _game.FinishRpcAsync(writer);
+ }
+
+ public ValueTask SetSkinAsync(SkinType skinType)
+ {
+ return SetSkinAsync((uint)skinType);
+ }
+
+ public async ValueTask SendChatAsync(string text)
+ {
+ using var writer = _game.StartRpc(NetId, RpcCalls.SendChat);
+ writer.Write(text);
+ await _game.FinishRpcAsync(writer);
+ }
+
+ public async ValueTask SendChatToPlayerAsync(string text, IInnerPlayerControl? player = null)
+ {
+ if (player == null)
+ {
+ player = this;
+ }
+
+ using var writer = _game.StartRpc(NetId, RpcCalls.SendChat);
+ writer.Write(text);
+ await _game.FinishRpcAsync(writer, player.OwnerId);
+ }
+
+ public async ValueTask SetMurderedByAsync(IClientPlayer impostor)
+ {
+ if (impostor.Character == null)
+ {
+ throw new ImpostorException("Character is null.");
+ }
+
+ if (!impostor.Character.PlayerInfo.IsImpostor)
+ {
+ throw new ImpostorProtocolException("Plugin tried to murder a player while the impostor specified was not an impostor.");
+ }
+
+ if (impostor.Character.PlayerInfo.IsDead)
+ {
+ throw new ImpostorProtocolException("Plugin tried to murder a player while the impostor specified was dead.");
+ }
+
+ if (PlayerInfo.IsDead)
+ {
+ return;
+ }
+
+ // Update player.
+ Die(DeathReason.Kill);
+
+ // Send RPC.
+ using var writer = _game.StartRpc(impostor.Character.NetId, RpcCalls.MurderPlayer);
+ writer.WritePacked(NetId);
+ await _game.FinishRpcAsync(writer);
+
+ // Notify plugins.
+ await _eventManager.CallAsync(new PlayerMurderEvent(_game, impostor, impostor.Character, this));
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
new file mode 100644
index 0000000..c71fcf7
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs
@@ -0,0 +1,455 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Events.Managers;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Events.Player;
+using Impostor.Server.Net.Inner.Objects.Components;
+using Impostor.Server.Net.State;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal partial class InnerPlayerControl : InnerNetObject
+ {
+ private readonly ILogger<InnerPlayerControl> _logger;
+ private readonly IEventManager _eventManager;
+ private readonly Game _game;
+
+ public InnerPlayerControl(ILogger<InnerPlayerControl> logger, IServiceProvider serviceProvider, IEventManager eventManager, Game game)
+ {
+ _logger = logger;
+ _eventManager = eventManager;
+ _game = game;
+
+ Physics = ActivatorUtilities.CreateInstance<InnerPlayerPhysics>(serviceProvider, this, _eventManager, _game);
+ NetworkTransform = ActivatorUtilities.CreateInstance<InnerCustomNetworkTransform>(serviceProvider, this, _game);
+
+ Components.Add(this);
+ Components.Add(Physics);
+ Components.Add(NetworkTransform);
+
+ PlayerId = byte.MaxValue;
+ }
+
+ public bool IsNew { get; private set; }
+
+ public byte PlayerId { get; private set; }
+
+ public InnerPlayerPhysics Physics { get; }
+
+ public InnerCustomNetworkTransform NetworkTransform { get; }
+
+ public InnerPlayerInfo PlayerInfo { get; internal set; }
+
+ public override async ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
+ {
+ switch (call)
+ {
+ // Play an animation.
+ case RpcCalls.PlayAnimation:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.PlayAnimation)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.PlayAnimation)} to a specific player instead of broadcast");
+ }
+
+ var animation = reader.ReadByte();
+ break;
+ }
+
+ // Complete a task.
+ case RpcCalls.CompleteTask:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CompleteTask)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CompleteTask)} to a specific player instead of broadcast");
+ }
+
+ var taskId = reader.ReadPackedUInt32();
+ var task = PlayerInfo.Tasks[(int)taskId];
+ if (task == null)
+ {
+ _logger.LogWarning($"Client sent {nameof(RpcCalls.CompleteTask)} with a taskIndex that is not in their {nameof(InnerPlayerInfo)}");
+ }
+ else
+ {
+ task.Complete = true;
+ await _eventManager.CallAsync(new PlayerCompletedTaskEvent(_game, sender, this, task));
+ }
+
+ break;
+ }
+
+ // Update GameOptions.
+ case RpcCalls.SyncSettings:
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SyncSettings)} but was not a host");
+ }
+
+ _game.Options.Deserialize(reader.ReadBytesAndSize());
+ break;
+ }
+
+ // Set Impostors.
+ case RpcCalls.SetInfected:
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetInfected)} but was not a host");
+ }
+
+ var length = reader.ReadPackedInt32();
+
+ for (var i = 0; i < length; i++)
+ {
+ var playerId = reader.ReadByte();
+ var player = _game.GameNet.GameData.GetPlayerById(playerId);
+ if (player != null)
+ {
+ player.IsImpostor = true;
+ }
+ }
+
+ if (_game.GameState == GameStates.Starting)
+ {
+ await _game.StartedAsync();
+ }
+
+ break;
+ }
+
+ // Player was voted out.
+ case RpcCalls.Exiled:
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Exiled)} but was not a host");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Exiled)} to a specific player instead of broadcast");
+ }
+
+ // TODO: Not hit?
+ Die(DeathReason.Exile);
+
+ await _eventManager.CallAsync(new PlayerExileEvent(_game, sender, this));
+ break;
+ }
+
+ // Validates the player name at the host.
+ case RpcCalls.CheckName:
+ {
+ if (target == null || !target.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CheckName)} to the wrong player");
+ }
+
+ var name = reader.ReadString();
+ break;
+ }
+
+ // Update the name of a player.
+ case RpcCalls.SetName:
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetName)} but was not a host");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetName)} to a specific player instead of broadcast");
+ }
+
+ PlayerInfo.PlayerName = reader.ReadString();
+ break;
+ }
+
+ // Validates the color at the host.
+ case RpcCalls.CheckColor:
+ {
+ if (target == null || !target.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CheckColor)} to the wrong player");
+ }
+
+ var color = reader.ReadByte();
+ break;
+ }
+
+ // Update the color of a player.
+ case RpcCalls.SetColor:
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetColor)} but was not a host");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetColor)} to a specific player instead of broadcast");
+ }
+
+ PlayerInfo.ColorId = reader.ReadByte();
+ break;
+ }
+
+ // Update the hat of a player.
+ case RpcCalls.SetHat:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetHat)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetHat)} to a specific player instead of broadcast");
+ }
+
+ PlayerInfo.HatId = reader.ReadPackedUInt32();
+ break;
+ }
+
+ case RpcCalls.SetSkin:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetSkin)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetHat)} to a specific player instead of broadcast");
+ }
+
+ PlayerInfo.SkinId = reader.ReadPackedUInt32();
+ break;
+ }
+
+ // TODO: (ANTICHEAT) Location check?
+ // only called by a non-host player on to start meeting
+ case RpcCalls.ReportDeadBody:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.ReportDeadBody)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.ReportDeadBody)} to a specific player instead of broadcast");
+ }
+
+
+ var deadBodyPlayerId = reader.ReadByte();
+ // deadBodyPlayerId == byte.MaxValue -- means emergency call by button
+
+ break;
+ }
+
+ // TODO: (ANTICHEAT) Cooldown check?
+ case RpcCalls.MurderPlayer:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} to a specific player instead of broadcast");
+ }
+
+ if (!sender.Character.PlayerInfo.IsImpostor)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} as crewmate");
+ }
+
+ if (!sender.Character.PlayerInfo.CanMurder(_game))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} too fast");
+ }
+
+ sender.Character.PlayerInfo.LastMurder = DateTimeOffset.UtcNow;
+
+ var player = reader.ReadNetObject<InnerPlayerControl>(_game);
+ if (!player.PlayerInfo.IsDead)
+ {
+ player.Die(DeathReason.Kill);
+ await _eventManager.CallAsync(new PlayerMurderEvent(_game, sender, this, player));
+ }
+
+ break;
+ }
+
+ case RpcCalls.SendChat:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChat)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChat)} to a specific player instead of broadcast");
+ }
+
+ var chat = reader.ReadString();
+
+ await _eventManager.CallAsync(new PlayerChatEvent(_game, sender, this, chat));
+ break;
+ }
+
+ case RpcCalls.StartMeeting:
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.StartMeeting)} but was not a host");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.StartMeeting)} to a specific player instead of broadcast");
+ }
+
+ // deadBodyPlayerId == byte.MaxValue -- means emergency call by button
+ var deadBodyPlayerId = reader.ReadByte();
+ var deadPlayer = deadBodyPlayerId != byte.MaxValue
+ ? _game.GameNet.GameData.GetPlayerById(deadBodyPlayerId)?.Controller
+ : null;
+
+ await _eventManager.CallAsync(new PlayerStartMeetingEvent(_game, _game.GetClientPlayer(this.OwnerId), this, deadPlayer));
+ break;
+ }
+
+ case RpcCalls.SetScanner:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetScanner)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetScanner)} to a specific player instead of broadcast");
+ }
+
+ var on = reader.ReadBoolean();
+ var count = reader.ReadByte();
+ break;
+ }
+
+ case RpcCalls.SendChatNote:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChatNote)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChatNote)} to a specific player instead of broadcast");
+ }
+
+ var playerId = reader.ReadByte();
+ var chatNote = (ChatNoteType)reader.ReadByte();
+ break;
+ }
+
+ case RpcCalls.SetPet:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetPet)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetPet)} to a specific player instead of broadcast");
+ }
+
+ PlayerInfo.PetId = reader.ReadPackedUInt32();
+ break;
+ }
+
+ // TODO: Understand this RPC
+ case RpcCalls.SetStartCounter:
+ {
+ if (!sender.IsOwner(this))
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetStartCounter)} to an unowned {nameof(InnerPlayerControl)}");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetStartCounter)} to a specific player instead of broadcast");
+ }
+
+ // Used to compare with LastStartCounter.
+ var startCounter = reader.ReadPackedUInt32();
+
+ // Is either start countdown or byte.MaxValue
+ var secondsLeft = reader.ReadByte();
+ if (secondsLeft < byte.MaxValue)
+ {
+ await _eventManager.CallAsync(new PlayerSetStartCounterEvent(_game, sender, this, secondsLeft));
+ }
+
+ break;
+ }
+
+ default:
+ {
+ _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerPlayerControl), call);
+ break;
+ }
+ }
+ }
+
+ public override bool Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerPlayerControl)} as non-host");
+ }
+
+ if (initialState)
+ {
+ IsNew = reader.ReadBoolean();
+ }
+
+ PlayerId = reader.ReadByte();
+ }
+
+ internal void Die(DeathReason reason)
+ {
+ PlayerInfo.IsDead = true;
+ PlayerInfo.LastDeathReason = reason;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs
new file mode 100644
index 0000000..512a4f1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal partial class InnerPlayerInfo : IInnerPlayerInfo
+ {
+ IEnumerable<ITaskInfo> IInnerPlayerInfo.Tasks => Tasks;
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs
new file mode 100644
index 0000000..f248994
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal partial class InnerPlayerInfo
+ {
+ public InnerPlayerInfo(byte playerId)
+ {
+ PlayerId = playerId;
+ }
+
+ public InnerPlayerControl Controller { get; internal set; }
+
+ public byte PlayerId { get; }
+
+ public string PlayerName { get; internal set; }
+
+ public byte ColorId { get; internal set; }
+
+ public uint HatId { get; internal set; }
+
+ public uint PetId { get; internal set; }
+
+ public uint SkinId { get; internal set; }
+
+ public bool Disconnected { get; internal set; }
+
+ public bool IsImpostor { get; internal set; }
+
+ public bool IsDead { get; internal set; }
+
+ public DeathReason LastDeathReason { get; internal set; }
+
+ public List<InnerGameData.TaskInfo> Tasks { get; internal set; }
+
+ public DateTimeOffset LastMurder { get; set; }
+
+ public bool CanMurder(IGame game)
+ {
+ if (!IsImpostor)
+ {
+ return false;
+ }
+
+ return DateTimeOffset.UtcNow.Subtract(LastMurder).TotalSeconds >= game.Options.KillCooldown;
+ }
+
+ public void Serialize(IMessageWriter writer)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Deserialize(IMessageReader reader)
+ {
+ PlayerName = reader.ReadString();
+ ColorId = reader.ReadByte();
+ HatId = reader.ReadPackedUInt32();
+ PetId = reader.ReadPackedUInt32();
+ SkinId = reader.ReadPackedUInt32();
+ var flag = reader.ReadByte();
+ Disconnected = (flag & 1) > 0;
+ IsImpostor = (flag & 2) > 0;
+ IsDead = (flag & 4) > 0;
+ var taskCount = reader.ReadByte();
+ for (var i = 0; i < taskCount; i++)
+ {
+ Tasks[i] ??= new InnerGameData.TaskInfo();
+ Tasks[i].Deserialize(reader);
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs
new file mode 100644
index 0000000..b1a3f18
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Net.Inner.Objects.Systems;
+using Impostor.Server.Net.Inner.Objects.Systems.ShipStatus;
+using Impostor.Server.Net.State;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Inner.Objects
+{
+ internal class InnerShipStatus : InnerNetObject, IInnerShipStatus
+ {
+ private readonly ILogger<InnerShipStatus> _logger;
+ private readonly Game _game;
+ private readonly Dictionary<SystemTypes, ISystemType> _systems;
+
+ public InnerShipStatus(ILogger<InnerShipStatus> logger, Game game)
+ {
+ _logger = logger;
+ _game = game;
+
+ _systems = new Dictionary<SystemTypes, ISystemType>
+ {
+ [SystemTypes.Electrical] = new SwitchSystem(),
+ [SystemTypes.MedBay] = new MedScanSystem(),
+ [SystemTypes.Reactor] = new ReactorSystemType(),
+ [SystemTypes.LifeSupp] = new LifeSuppSystemType(),
+ [SystemTypes.Security] = new SecurityCameraSystemType(),
+ [SystemTypes.Comms] = new HudOverrideSystemType(),
+ [SystemTypes.Doors] = new DoorsSystemType(_game),
+ };
+
+ _systems.Add(SystemTypes.Sabotage, new SabotageSystemType(new[]
+ {
+ (IActivatable)_systems[SystemTypes.Comms],
+ (IActivatable)_systems[SystemTypes.Reactor],
+ (IActivatable)_systems[SystemTypes.LifeSupp],
+ (IActivatable)_systems[SystemTypes.Electrical],
+ }));
+
+ Components.Add(this);
+ }
+
+ public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call,
+ IMessageReader reader)
+ {
+ switch (call)
+ {
+ case RpcCalls.CloseDoorsOfType:
+ {
+ if (target == null || !target.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CloseDoorsOfType)} to wrong destinition, must be host");
+ }
+
+ if (!sender.Character.PlayerInfo.IsImpostor)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CloseDoorsOfType)} as crewmate");
+ }
+
+ var systemType = (SystemTypes)reader.ReadByte();
+
+ break;
+ }
+
+ case RpcCalls.RepairSystem:
+ {
+ if (target == null || !target.IsHost)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.RepairSystem)} to wrong destinition, must be host");
+ }
+
+ var systemType = (SystemTypes)reader.ReadByte();
+ if (systemType == SystemTypes.Sabotage && !sender.Character.PlayerInfo.IsImpostor)
+ {
+ throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.RepairSystem)} for {systemType} as crewmate");
+ }
+
+ var player = reader.ReadNetObject<InnerPlayerControl>(_game);
+ var amount = reader.ReadByte();
+
+ // TODO: Modify data (?)
+ break;
+ }
+
+ default:
+ {
+ _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerShipStatus), call);
+ break;
+ }
+ }
+
+ return default;
+ }
+
+ public override bool Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
+ {
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerShipStatus)} as non-host");
+ }
+
+ if (target != null)
+ {
+ throw new ImpostorCheatException($"Client attempted to send {nameof(InnerShipStatus)} data to a specific player, must be broadcast");
+ }
+
+ if (initialState)
+ {
+ // TODO: (_systems[SystemTypes.Doors] as DoorsSystemType).SetDoors();
+ foreach (var systemType in SystemTypeHelpers.AllTypes)
+ {
+ if (_systems.TryGetValue(systemType, out var system))
+ {
+ system.Deserialize(reader, true);
+ }
+ }
+ }
+ else
+ {
+ var count = reader.ReadPackedUInt32();
+
+ foreach (var systemType in SystemTypeHelpers.AllTypes)
+ {
+ // TODO: Not sure what is going on here, check.
+ if ((count & 1 << (int)(systemType & (SystemTypes.ShipTasks | SystemTypes.Doors))) != 0L)
+ {
+ if (_systems.TryGetValue(systemType, out var system))
+ {
+ system.Deserialize(reader, false);
+ }
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs
new file mode 100644
index 0000000..e595b25
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs
@@ -0,0 +1,7 @@
+namespace Impostor.Server.Net.Inner.Objects.Systems
+{
+ public interface IActivatable
+ {
+ bool IsActive { get; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs
new file mode 100644
index 0000000..a6ef88e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs
@@ -0,0 +1,11 @@
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects.Systems
+{
+ public interface ISystemType
+ {
+ void Serialize(IMessageWriter writer, bool initialState);
+
+ void Deserialize(IMessageReader reader, bool initialState);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs
new file mode 100644
index 0000000..64b1f5f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus
+{
+ public class DoorsSystemType : ISystemType
+ {
+ // TODO: AutoDoors
+ private readonly Dictionary<int, bool> _doors;
+
+ public DoorsSystemType(IGame game)
+ {
+ var doorCount = game.Options.Map switch
+ {
+ MapTypes.Skeld => 13,
+ MapTypes.MiraHQ => 2,
+ MapTypes.Polus => 12,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ _doors = new Dictionary<int, bool>(doorCount);
+
+ for (var i = 0; i < doorCount; i++)
+ {
+ _doors.Add(i, false);
+ }
+ }
+
+ public void Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Deserialize(IMessageReader reader, bool initialState)
+ {
+ if (initialState)
+ {
+ for (var i = 0; i < _doors.Count; i++)
+ {
+ _doors[i] = reader.ReadBoolean();
+ }
+ }
+ else
+ {
+ var num = reader.ReadPackedUInt32();
+
+ for (var i = 0; i < _doors.Count; i++)
+ {
+ if ((num & 1 << i) != 0)
+ {
+ _doors[i] = reader.ReadBoolean();
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs
new file mode 100644
index 0000000..42aa8d3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs
@@ -0,0 +1,19 @@
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus
+{
+ public class HudOverrideSystemType : ISystemType, IActivatable
+ {
+ public bool IsActive { get; private set; }
+
+ public void Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public void Deserialize(IMessageReader reader, bool initialState)
+ {
+ IsActive = reader.ReadBoolean();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs
new file mode 100644
index 0000000..f644024
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus
+{
+ public class LifeSuppSystemType : ISystemType, IActivatable
+ {
+ public LifeSuppSystemType()
+ {
+ Countdown = 10000f;
+ CompletedConsoles = new HashSet<int>();
+ }
+
+ public float Countdown { get; private set; }
+
+ public HashSet<int> CompletedConsoles { get; }
+
+ public bool IsActive => Countdown < 10000.0;
+
+ public void Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public void Deserialize(IMessageReader reader, bool initialState)
+ {
+ Countdown = reader.ReadSingle();
+
+ if (reader.Position >= reader.Length)
+ {
+ return;
+ }
+
+ CompletedConsoles.Clear(); // TODO: Thread safety
+
+ var num = reader.ReadPackedInt32();
+
+ for (var i = 0; i < num; i++)
+ {
+ CompletedConsoles.Add(reader.ReadPackedInt32());
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs
new file mode 100644
index 0000000..007b9d0
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus
+{
+ public class MedScanSystem : ISystemType
+ {
+ public MedScanSystem()
+ {
+ UsersList = new List<byte>();
+ }
+
+ public List<byte> UsersList { get; }
+
+ public void Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public void Deserialize(IMessageReader reader, bool initialState)
+ {
+ UsersList.Clear();
+
+ var num = reader.ReadPackedInt32();
+
+ for (var i = 0; i < num; i++)
+ {
+ UsersList.Add(reader.ReadByte());
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs
new file mode 100644
index 0000000..4380918
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus
+{
+ public class ReactorSystemType : ISystemType, IActivatable
+ {
+ public ReactorSystemType()
+ {
+ Countdown = 10000f;
+ UserConsolePairs = new HashSet<Tuple<byte, byte>>();
+ }
+
+ public float Countdown { get; private set; }
+
+ public HashSet<Tuple<byte, byte>> UserConsolePairs { get; }
+
+ public bool IsActive => Countdown < 10000.0;
+
+ public void Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public void Deserialize(IMessageReader reader, bool initialState)
+ {
+ Countdown = reader.ReadSingle();
+ UserConsolePairs.Clear(); // TODO: Thread safety
+
+ var count = reader.ReadPackedInt32();
+
+ for (var i = 0; i < count; i++)
+ {
+ UserConsolePairs.Add(new Tuple<byte, byte>(reader.ReadByte(), reader.ReadByte()));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs
new file mode 100644
index 0000000..193cfe8
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs
@@ -0,0 +1,26 @@
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus
+{
+ public class SabotageSystemType : ISystemType
+ {
+ private readonly IActivatable[] _specials;
+
+ public SabotageSystemType(IActivatable[] specials)
+ {
+ _specials = specials;
+ }
+
+ public float Timer { get; set; }
+
+ public void Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public void Deserialize(IMessageReader reader, bool initialState)
+ {
+ Timer = reader.ReadSingle();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs
new file mode 100644
index 0000000..df41b68
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs
@@ -0,0 +1,19 @@
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus
+{
+ public class SecurityCameraSystemType : ISystemType
+ {
+ public byte InUse { get; internal set; }
+
+ public void Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public void Deserialize(IMessageReader reader, bool initialState)
+ {
+ InUse = reader.ReadByte();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs
new file mode 100644
index 0000000..925774a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs
@@ -0,0 +1,27 @@
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus
+{
+ public class SwitchSystem : ISystemType, IActivatable
+ {
+ public byte ExpectedSwitches { get; set; }
+
+ public byte ActualSwitches { get; set; }
+
+ public byte Value { get; set; } = byte.MaxValue;
+
+ public bool IsActive { get; }
+
+ public void Serialize(IMessageWriter writer, bool initialState)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public void Deserialize(IMessageReader reader, bool initialState)
+ {
+ ExpectedSwitches = reader.ReadByte();
+ ActualSwitches = reader.ReadByte();
+ Value = reader.ReadByte();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs
new file mode 100644
index 0000000..ce48965
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs
@@ -0,0 +1,37 @@
+namespace Impostor.Server.Net.Inner
+{
+ public enum RpcCalls : byte
+ {
+ PlayAnimation = 0,
+ CompleteTask = 1,
+ SyncSettings = 2,
+ SetInfected = 3,
+ Exiled = 4,
+ CheckName = 5,
+ SetName = 6,
+ CheckColor = 7,
+ SetColor = 8,
+ SetHat = 9,
+ SetSkin = 10,
+ ReportDeadBody = 11,
+ MurderPlayer = 12,
+ SendChat = 13,
+ StartMeeting = 14,
+ SetScanner = 15,
+ SendChatNote = 16,
+ SetPet = 17,
+ SetStartCounter = 18,
+ EnterVent = 19,
+ ExitVent = 20,
+ SnapTo = 21,
+ Close = 22,
+ VotingComplete = 23,
+ CastVote = 24,
+ ClearVote = 25,
+ AddVote = 26,
+ CloseDoorsOfType = 27,
+ RepairSystem = 28,
+ SetTasks = 29,
+ UpdateGameData = 30,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs
new file mode 100644
index 0000000..1860098
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Impostor.Server.Net.Inner
+{
+ [Flags]
+ public enum SpawnFlags : byte
+ {
+ None = 0,
+ IsClientCharacter = 1,
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs
new file mode 100644
index 0000000..6cbe5bf
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Manager;
+
+namespace Impostor.Server.Net.Manager
+{
+ internal partial class ClientManager : IClientManager
+ {
+ IEnumerable<IClient> IClientManager.Clients => _clients.Values;
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs
new file mode 100644
index 0000000..51e22d8
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs
@@ -0,0 +1,103 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Api.Net.Messages.S2C;
+using Impostor.Hazel;
+using Impostor.Server.Config;
+using Impostor.Server.Net.Factories;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Manager
+{
+ internal partial class ClientManager
+ {
+ private static HashSet<int> SupportedVersions { get; } = new HashSet<int>
+ {
+ GameVersion.GetVersion(2020, 09, 07), // 2020.09.07 - 2020.09.22
+ GameVersion.GetVersion(2020, 10, 08), // 2020.10.08
+ GameVersion.GetVersion(2020, 11, 17), // 2020.11.17
+ };
+
+ private readonly ILogger<ClientManager> _logger;
+ private readonly ConcurrentDictionary<int, ClientBase> _clients;
+ private readonly IClientFactory _clientFactory;
+ private int _idLast;
+
+ public ClientManager(ILogger<ClientManager> logger, IClientFactory clientFactory)
+ {
+ _logger = logger;
+ _clientFactory = clientFactory;
+ _clients = new ConcurrentDictionary<int, ClientBase>();
+ }
+
+ public IEnumerable<ClientBase> Clients => _clients.Values;
+
+ public int NextId()
+ {
+ var clientId = Interlocked.Increment(ref _idLast);
+
+ if (clientId < 1)
+ {
+ // Super rare but reset the _idLast because of overflow.
+ _idLast = 0;
+
+ // And get a new id.
+ clientId = Interlocked.Increment(ref _idLast);
+ }
+
+ return clientId;
+ }
+
+ public async ValueTask RegisterConnectionAsync(IHazelConnection connection, string name, int clientVersion)
+ {
+ if (!SupportedVersions.Contains(clientVersion))
+ {
+ using var packet = MessageWriter.Get(MessageType.Reliable);
+ Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.IncorrectVersion);
+ await connection.SendAsync(packet);
+ return;
+ }
+
+ if (name.Length > 10)
+ {
+ using var packet = MessageWriter.Get(MessageType.Reliable);
+ Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.UsernameLength);
+ await connection.SendAsync(packet);
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(name) || !name.All(TextBox.IsCharAllowed))
+ {
+ using var packet = MessageWriter.Get(MessageType.Reliable);
+ Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.UsernameIllegalCharacters);
+ await connection.SendAsync(packet);
+ return;
+ }
+
+ var client = _clientFactory.Create(connection, name, clientVersion);
+ var id = NextId();
+
+ client.Id = id;
+ _logger.LogTrace("Client connected.");
+ _clients.TryAdd(id, client);
+ }
+
+ public void Remove(IClient client)
+ {
+ _logger.LogTrace("Client disconnected.");
+ _clients.TryRemove(client.Id, out _);
+ }
+
+ public bool Validate(IClient client)
+ {
+ return client.Id != 0
+ && _clients.TryGetValue(client.Id, out var registeredClient)
+ && ReferenceEquals(client, registeredClient);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs b/Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs
new file mode 100644
index 0000000..a37829e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Events.Managers;
+using Impostor.Api.Games;
+using Impostor.Api.Games.Managers;
+using Impostor.Api.Innersloth;
+using Impostor.Server.Config;
+using Impostor.Server.Events;
+using Impostor.Server.Net.Redirector;
+using Impostor.Server.Net.State;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Net.Manager
+{
+ internal class GameManager : IGameManager
+ {
+ private readonly ILogger<GameManager> _logger;
+ private readonly INodeLocator _nodeLocator;
+ private readonly IPEndPoint _publicIp;
+ private readonly ConcurrentDictionary<int, Game> _games;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IEventManager _eventManager;
+ private readonly IGameCodeFactory _gameCodeFactory;
+
+ public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config, INodeLocator nodeLocator, IServiceProvider serviceProvider, IEventManager eventManager, IGameCodeFactory gameCodeFactory)
+ {
+ _logger = logger;
+ _nodeLocator = nodeLocator;
+ _serviceProvider = serviceProvider;
+ _eventManager = eventManager;
+ _gameCodeFactory = gameCodeFactory;
+ _publicIp = new IPEndPoint(IPAddress.Parse(config.Value.ResolvePublicIp()), config.Value.PublicPort);
+ _games = new ConcurrentDictionary<int, Game>();
+ }
+
+ IEnumerable<IGame> IGameManager.Games => _games.Select(kv => kv.Value);
+
+ IGame IGameManager.Find(GameCode code) => Find(code);
+
+ public async ValueTask<IGame> CreateAsync(GameOptionsData options)
+ {
+ // TODO: Prevent duplicates when using server redirector using INodeProvider.
+ var (success, game) = await TryCreateAsync(options);
+
+ for (int i = 0; i < 10 && !success; i++)
+ {
+ (success, game) = await TryCreateAsync(options);
+ }
+
+ if (!success)
+ {
+ throw new ImpostorException("Could not create new game"); // TODO: Fix generic exception.
+ }
+
+ return game;
+ }
+
+ private async ValueTask<(bool success, Game game)> TryCreateAsync(GameOptionsData options)
+ {
+ var gameCode = _gameCodeFactory.Create();
+ var gameCodeStr = gameCode.Code;
+ var game = ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options);
+
+ if (await _nodeLocator.ExistsAsync(gameCodeStr) || !_games.TryAdd(gameCode, game))
+ {
+ return (false, null);
+ }
+
+ await _nodeLocator.SaveAsync(gameCodeStr, _publicIp);
+ _logger.LogDebug("Created game with code {0}.", game.Code);
+
+ await _eventManager.CallAsync(new GameCreatedEvent(game));
+
+ return (true, game);
+ }
+
+ public Game Find(GameCode code)
+ {
+ _games.TryGetValue(code, out var game);
+ return game;
+ }
+
+ public IEnumerable<Game> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10)
+ {
+ var results = 0;
+
+ // Find games that have not started yet.
+ foreach (var (_, game) in _games.Where(x =>
+ x.Value.IsPublic &&
+ x.Value.GameState == GameStates.NotStarted &&
+ x.Value.PlayerCount < x.Value.Options.MaxPlayers))
+ {
+ // Check for options.
+ if (!map.HasFlag((MapFlags)(1 << game.Options.MapId)))
+ {
+ continue;
+ }
+
+ if (!language.HasFlag(game.Options.Keywords))
+ {
+ continue;
+ }
+
+ if (impostorCount != 0 && game.Options.NumImpostors != impostorCount)
+ {
+ continue;
+ }
+
+ // Add to result.
+ yield return game;
+
+ // Break out if we have enough.
+ if (++results == count)
+ {
+ yield break;
+ }
+ }
+ }
+
+ public async ValueTask RemoveAsync(GameCode gameCode)
+ {
+ if (_games.TryGetValue(gameCode, out var game) && game.PlayerCount > 0)
+ {
+ foreach (var player in game.Players)
+ {
+ await player.KickAsync();
+ }
+
+ return;
+ }
+
+ if (!_games.TryRemove(gameCode, out game))
+ {
+ return;
+ }
+
+ _logger.LogDebug("Remove game with code {0} ({1}).", GameCodeParser.IntToGameName(gameCode), gameCode);
+ await _nodeLocator.RemoveAsync(GameCodeParser.IntToGameName(gameCode));
+
+ await _eventManager.CallAsync(new GameDestroyedEvent(game));
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs b/Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs
new file mode 100644
index 0000000..64ece55
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using Impostor.Hazel;
+using Impostor.Hazel.Udp;
+using Impostor.Server.Net.Hazel;
+using Impostor.Server.Net.Manager;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Server.Net
+{
+ internal class Matchmaker
+ {
+ private readonly ClientManager _clientManager;
+ private readonly ObjectPool<MessageReader> _readerPool;
+ private readonly ILogger<Matchmaker> _logger;
+ private readonly ILogger<HazelConnection> _connectionLogger;
+ private UdpConnectionListener _connection;
+
+ public Matchmaker(
+ ILogger<Matchmaker> logger,
+ ClientManager clientManager,
+ ObjectPool<MessageReader> readerPool,
+ ILogger<HazelConnection> connectionLogger)
+ {
+ _logger = logger;
+ _clientManager = clientManager;
+ _readerPool = readerPool;
+ _connectionLogger = connectionLogger;
+ }
+
+ public async ValueTask StartAsync(IPEndPoint ipEndPoint)
+ {
+ var mode = ipEndPoint.AddressFamily switch
+ {
+ AddressFamily.InterNetwork => IPMode.IPv4,
+ AddressFamily.InterNetworkV6 => IPMode.IPv6,
+ _ => throw new InvalidOperationException()
+ };
+
+ _connection = new UdpConnectionListener(ipEndPoint, _readerPool, mode);
+ _connection.NewConnection = OnNewConnection;
+
+ await _connection.StartAsync();
+ }
+
+ public async ValueTask StopAsync()
+ {
+ await _connection.DisposeAsync();
+ }
+
+ private async ValueTask OnNewConnection(NewConnectionEventArgs e)
+ {
+ // Handshake.
+ var clientVersion = e.HandshakeData.ReadInt32();
+ var name = e.HandshakeData.ReadString();
+
+ var connection = new HazelConnection(e.Connection, _connectionLogger);
+
+ // Register client
+ await _clientManager.RegisterConnectionAsync(connection, name, clientVersion);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs b/Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs
new file mode 100644
index 0000000..bd9a855
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs
@@ -0,0 +1,57 @@
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Server.Config;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Net
+{
+ internal class MatchmakerService : IHostedService
+ {
+ private readonly ILogger<MatchmakerService> _logger;
+ private readonly ServerConfig _serverConfig;
+ private readonly ServerRedirectorConfig _redirectorConfig;
+ private readonly Matchmaker _matchmaker;
+
+ public MatchmakerService(
+ ILogger<MatchmakerService> logger,
+ IOptions<ServerConfig> serverConfig,
+ IOptions<ServerRedirectorConfig> redirectorConfig,
+ Matchmaker matchmaker)
+ {
+ _logger = logger;
+ _serverConfig = serverConfig.Value;
+ _redirectorConfig = redirectorConfig.Value;
+ _matchmaker = matchmaker;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ var endpoint = new IPEndPoint(IPAddress.Parse(_serverConfig.ResolveListenIp()), _serverConfig.ListenPort);
+
+ await _matchmaker.StartAsync(endpoint);
+
+ _logger.LogInformation(
+ "Matchmaker is listening on {0}:{1}, the public server ip is {2}:{3}.",
+ endpoint.Address,
+ endpoint.Port,
+ _serverConfig.ResolvePublicIp(),
+ _serverConfig.PublicPort);
+
+ if (_redirectorConfig.Enabled)
+ {
+ _logger.LogWarning(_redirectorConfig.Master
+ ? "Server redirection is enabled as master, this instance will redirect clients to other nodes."
+ : "Server redirection is enabled as node, this instance will accept clients.");
+ }
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogWarning("Matchmaker is shutting down!");
+ await _matchmaker.StopAsync();
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs b/Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs
new file mode 100644
index 0000000..707860e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs
@@ -0,0 +1,13 @@
+using Impostor.Api.Net.Messages;
+using Impostor.Hazel;
+
+namespace Impostor.Server.Net.Messages
+{
+ public class MessageWriterProvider : IMessageWriterProvider
+ {
+ public IMessageWriter Get(MessageType sendOption = MessageType.Unreliable)
+ {
+ return MessageWriter.Get(sendOption);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs
new file mode 100644
index 0000000..3dad4b3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs
@@ -0,0 +1,97 @@
+using System.Threading.Tasks;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Messages;
+using Impostor.Api.Net.Messages.C2S;
+using Impostor.Api.Net.Messages.S2C;
+using Impostor.Hazel;
+using Impostor.Server.Config;
+using Impostor.Server.Net.Hazel;
+using Impostor.Server.Net.Manager;
+using Serilog;
+using ILogger = Serilog.ILogger;
+
+namespace Impostor.Server.Net.Redirector
+{
+ internal class ClientRedirector : ClientBase
+ {
+ private static readonly ILogger Logger = Log.ForContext<ClientRedirector>();
+
+ private readonly ClientManager _clientManager;
+ private readonly INodeProvider _nodeProvider;
+ private readonly INodeLocator _nodeLocator;
+
+ public ClientRedirector(
+ string name,
+ HazelConnection connection,
+ ClientManager clientManager,
+ INodeProvider nodeProvider,
+ INodeLocator nodeLocator)
+ : base(name, connection)
+ {
+ _clientManager = clientManager;
+ _nodeProvider = nodeProvider;
+ _nodeLocator = nodeLocator;
+ }
+
+ public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType)
+ {
+ var flag = reader.Tag;
+
+ Logger.Verbose("Server got {0}.", flag);
+
+ switch (flag)
+ {
+ case MessageFlags.HostGame:
+ {
+ using var packet = MessageWriter.Get(MessageType.Reliable);
+ Message13RedirectS2C.Serialize(packet, false, _nodeProvider.Get());
+ await Connection.SendAsync(packet);
+ break;
+ }
+
+ case MessageFlags.JoinGame:
+ {
+ Message01JoinGameC2S.Deserialize(
+ reader,
+ out var gameCode,
+ out _);
+
+ using var packet = MessageWriter.Get(MessageType.Reliable);
+ var endpoint = await _nodeLocator.FindAsync(GameCodeParser.IntToGameName(gameCode));
+ if (endpoint == null)
+ {
+ Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.GameMissing);
+ }
+ else
+ {
+ Message13RedirectS2C.Serialize(packet, false, endpoint);
+ }
+
+ await Connection.SendAsync(packet);
+ break;
+ }
+
+ case MessageFlags.GetGameListV2:
+ {
+ // TODO: Implement.
+ using var packet = MessageWriter.Get(MessageType.Reliable);
+ Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.NotImplemented);
+ await Connection.SendAsync(packet);
+ break;
+ }
+
+ default:
+ {
+ Logger.Warning("Received unsupported message flag on the redirector ({0}).", flag);
+ break;
+ }
+ }
+ }
+
+ public override ValueTask HandleDisconnectAsync(string reason)
+ {
+ _clientManager.Remove(this);
+ return default;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs
new file mode 100644
index 0000000..12563b9
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs
@@ -0,0 +1,14 @@
+using System.Net;
+using System.Threading.Tasks;
+
+namespace Impostor.Server.Net.Redirector
+{
+ public interface INodeLocator
+ {
+ ValueTask<IPEndPoint> FindAsync(string gameCode);
+
+ ValueTask SaveAsync(string gameCode, IPEndPoint endPoint);
+
+ ValueTask RemoveAsync(string gameCode);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs
new file mode 100644
index 0000000..318fcb2
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs
@@ -0,0 +1,9 @@
+using System.Net;
+
+namespace Impostor.Server.Net.Redirector
+{
+ internal interface INodeProvider
+ {
+ IPEndPoint Get();
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs
new file mode 100644
index 0000000..fd4cd56
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs
@@ -0,0 +1,14 @@
+using System.Net;
+using System.Threading.Tasks;
+
+namespace Impostor.Server.Net.Redirector
+{
+ public class NodeLocatorNoOp : INodeLocator
+ {
+ public ValueTask<IPEndPoint> FindAsync(string gameCode) => ValueTask.FromResult(default(IPEndPoint));
+
+ public ValueTask SaveAsync(string gameCode, IPEndPoint endPoint) => ValueTask.CompletedTask;
+
+ public ValueTask RemoveAsync(string gameCode) => ValueTask.CompletedTask;
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs
new file mode 100644
index 0000000..0b6fdff
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Redirector
+{
+ public class NodeLocatorRedis : INodeLocator
+ {
+ private readonly IDistributedCache _cache;
+
+ public NodeLocatorRedis(ILogger<NodeLocatorRedis> logger, IDistributedCache cache)
+ {
+ logger.LogWarning("Using the redis NodeLocator.");
+ _cache = cache;
+ }
+
+ public async ValueTask<IPEndPoint> FindAsync(string gameCode)
+ {
+ var entry = await _cache.GetStringAsync(gameCode);
+ if (entry == null)
+ {
+ return null;
+ }
+
+ return IPEndPoint.Parse(entry);
+ }
+
+ public async ValueTask SaveAsync(string gameCode, IPEndPoint endPoint)
+ {
+ await _cache.SetStringAsync(gameCode, endPoint.ToString(), new DistributedCacheEntryOptions
+ {
+ SlidingExpiration = TimeSpan.FromHours(1),
+ });
+ }
+
+ public async ValueTask RemoveAsync(string gameCode)
+ {
+ await _cache.RemoveAsync(gameCode);
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs
new file mode 100644
index 0000000..2539a8f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Concurrent;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Tasks;
+using Impostor.Server.Config;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Net.Redirector
+{
+ public class NodeLocatorUdp : INodeLocator, IDisposable
+ {
+ private readonly ILogger<NodeLocatorUdp> _logger;
+ private readonly bool _isMaster;
+ private readonly IPEndPoint _server;
+ private readonly UdpClient _client;
+ private readonly ConcurrentDictionary<string, AvailableNode> _availableNodes;
+
+ public NodeLocatorUdp(ILogger<NodeLocatorUdp> logger, IOptions<ServerRedirectorConfig> config)
+ {
+ _logger = logger;
+
+ if (config.Value.Master)
+ {
+ _isMaster = true;
+ _availableNodes = new ConcurrentDictionary<string, AvailableNode>();
+ }
+ else
+ {
+ _isMaster = false;
+
+ if (!IPEndPoint.TryParse(config.Value.Locator.UdpMasterEndpoint, out var endpoint))
+ {
+ throw new ArgumentException("UdpMasterEndpoint should be in the ip:port format.");
+ }
+
+ _logger.LogWarning("Node server will send updates to {0}.", endpoint);
+ _server = endpoint;
+ _client = new UdpClient();
+
+ try
+ {
+ _client.DontFragment = true;
+ }
+ catch (SocketException)
+ {
+ }
+ }
+ }
+
+ public void Update(IPEndPoint ip, string gameCode)
+ {
+ _logger.LogDebug("Received update {0} -> {1}", gameCode, ip);
+
+ _availableNodes.AddOrUpdate(
+ gameCode,
+ s => new AvailableNode
+ {
+ Endpoint = ip,
+ LastUpdated = DateTimeOffset.UtcNow,
+ },
+ (s, node) =>
+ {
+ node.Endpoint = ip;
+ node.LastUpdated = DateTimeOffset.UtcNow;
+
+ return node;
+ });
+
+ foreach (var (key, value) in _availableNodes)
+ {
+ if (value.Expired)
+ {
+ _availableNodes.TryRemove(key, out _);
+ }
+ }
+ }
+
+ public ValueTask<IPEndPoint> FindAsync(string gameCode)
+ {
+ if (!_isMaster)
+ {
+ return ValueTask.FromResult(default(IPEndPoint));
+ }
+
+ if (_availableNodes.TryGetValue(gameCode, out var node))
+ {
+ if (node.Expired)
+ {
+ _availableNodes.TryRemove(gameCode, out _);
+ return ValueTask.FromResult(default(IPEndPoint));
+ }
+
+ return ValueTask.FromResult(node.Endpoint);
+ }
+
+ return ValueTask.FromResult(default(IPEndPoint));
+ }
+
+ public ValueTask RemoveAsync(string gameCode)
+ {
+ if (!_isMaster)
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ _availableNodes.TryRemove(gameCode, out _);
+ return ValueTask.CompletedTask;
+ }
+
+ public ValueTask SaveAsync(string gameCode, IPEndPoint endPoint)
+ {
+ var data = Encoding.UTF8.GetBytes($"{gameCode},{endPoint}");
+ _client.Send(data, data.Length, _server);
+ return ValueTask.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ _client?.Dispose();
+ }
+
+ private class AvailableNode
+ {
+ public IPEndPoint Endpoint { get; set; }
+
+ public DateTimeOffset LastUpdated { get; set; }
+
+ public bool Expired => LastUpdated < DateTimeOffset.UtcNow.AddHours(-1);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs
new file mode 100644
index 0000000..3706bb4
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Server.Config;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Net.Redirector
+{
+ public class NodeLocatorUdpService : BackgroundService
+ {
+ private readonly NodeLocatorUdp _nodeLocator;
+ private readonly ILogger<NodeLocatorUdpService> _logger;
+ private readonly UdpClient _client;
+
+ public NodeLocatorUdpService(
+ INodeLocator nodeLocator,
+ ILogger<NodeLocatorUdpService> logger,
+ IOptions<ServerRedirectorConfig> options)
+ {
+ _nodeLocator = (NodeLocatorUdp)nodeLocator;
+ _logger = logger;
+
+ if (!IPEndPoint.TryParse(options.Value.Locator.UdpMasterEndpoint, out var endpoint))
+ {
+ throw new ArgumentException("UdpMasterEndpoint should be in the ip:port format.");
+ }
+
+ _client = new UdpClient(endpoint);
+
+ try
+ {
+ _client.DontFragment = true;
+ }
+ catch (SocketException)
+ {
+ }
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogWarning("Master server is listening for node updates on {0}.", _client.Client.LocalEndPoint);
+
+ stoppingToken.Register(() =>
+ {
+ _client.Close();
+ _client.Dispose();
+ });
+
+ try
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ // Receive data from a node.
+ UdpReceiveResult data;
+
+ try
+ {
+ data = await _client.ReceiveAsync();
+ }
+ catch (ObjectDisposedException)
+ {
+ break;
+ }
+
+ // Check if data is valid.
+ if (data.Buffer.Length == 0)
+ {
+ break;
+ }
+
+ // Parse the data.
+ var message = Encoding.UTF8.GetString(data.Buffer);
+ var parts = message.Split(',', 2);
+ if (parts.Length != 2)
+ {
+ continue;
+ }
+
+ if (!IPEndPoint.TryParse(parts[1], out var ipEndPoint))
+ {
+ continue;
+ }
+
+ // Update the NodeLocator.
+ _nodeLocator.Update(ipEndPoint, parts[0]);
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error in NodeLocatorUDPService.");
+ }
+
+ _logger.LogWarning("Master server node update listener is stopping.");
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs
new file mode 100644
index 0000000..4e19c43
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using System.Net;
+using Impostor.Server.Config;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Net.Redirector
+{
+ internal class NodeProviderConfig : INodeProvider
+ {
+ private readonly List<IPEndPoint> _nodes;
+ private readonly object _lock;
+ private int _currentIndex;
+
+ public NodeProviderConfig(IOptions<ServerRedirectorConfig> redirectorConfig)
+ {
+ _nodes = new List<IPEndPoint>();
+ _lock = new object();
+
+ if (redirectorConfig.Value.Nodes != null)
+ {
+ foreach (var node in redirectorConfig.Value.Nodes)
+ {
+ _nodes.Add(new IPEndPoint(IPAddress.Parse(node.Ip), node.Port));
+ }
+ }
+ }
+
+ public IPEndPoint Get()
+ {
+ lock (_lock)
+ {
+ var node = _nodes[_currentIndex++];
+
+ if (_currentIndex == _nodes.Count)
+ {
+ _currentIndex = 0;
+ }
+
+ return node;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs
new file mode 100644
index 0000000..2e2467e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs
@@ -0,0 +1,18 @@
+using Impostor.Api.Games;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Net.State
+{
+ internal partial class ClientPlayer
+ {
+ /// <inheritdoc />
+ IClient IClientPlayer.Client => Client;
+
+ /// <inheritdoc />
+ IGame IClientPlayer.Game => Game;
+
+ /// <inheritdoc />
+ IInnerPlayerControl? IClientPlayer.Character => Character;
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs
new file mode 100644
index 0000000..107edbf
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner;
+using Impostor.Server.Net.Inner.Objects;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.State
+{
+ internal partial class ClientPlayer : IClientPlayer
+ {
+ private readonly ILogger<ClientPlayer> _logger;
+ private readonly Timer _spawnTimeout;
+
+ public ClientPlayer(ILogger<ClientPlayer> logger, ClientBase client, Game game)
+ {
+ _logger = logger;
+ _spawnTimeout = new Timer(RunSpawnTimeout, null, -1, -1);
+
+ Game = game;
+ Client = client;
+ Limbo = LimboStates.PreSpawn;
+ }
+
+ public ClientBase Client { get; }
+
+ public Game Game { get; }
+
+ /// <inheritdoc />
+ public LimboStates Limbo { get; set; }
+
+ public InnerPlayerControl? Character { get; internal set; }
+
+ public bool IsHost => Game?.Host == this;
+
+ public string Scene { get; internal set; }
+
+ public void InitializeSpawnTimeout()
+ {
+ _spawnTimeout.Change(Constants.SpawnTimeout, -1);
+ }
+
+ public void DisableSpawnTimeout()
+ {
+ _spawnTimeout.Change(-1, -1);
+ }
+
+ /// <inheritdoc />
+ public bool IsOwner(IInnerNetObject netObject)
+ {
+ return Client.Id == netObject.OwnerId;
+ }
+
+ /// <inheritdoc />
+ public ValueTask KickAsync()
+ {
+ return Game.HandleKickPlayer(Client.Id, false);
+ }
+
+ /// <inheritdoc />
+ public ValueTask BanAsync()
+ {
+ return Game.HandleKickPlayer(Client.Id, true);
+ }
+
+ private async void RunSpawnTimeout(object state)
+ {
+ try
+ {
+ if (Character == null)
+ {
+ _logger.LogInformation("{0} - Player {1} spawn timed out, kicking.", Game.Code, Client.Id);
+
+ await KickAsync();
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Exception caught while kicking player for spawn timeout.");
+ }
+ finally
+ {
+ await _spawnTimeout.DisposeAsync();
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs
new file mode 100644
index 0000000..f395be2
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Inner;
+using Impostor.Api.Net.Inner.Objects;
+using Impostor.Server.Net.Inner;
+
+namespace Impostor.Server.Net.State
+{
+ internal partial class Game : IGame
+ {
+ IClientPlayer IGame.Host => Host;
+
+ IGameNet IGame.GameNet => GameNet;
+
+ public void BanIp(IPAddress ipAddress)
+ {
+ _bannedIps.Add(ipAddress);
+ }
+
+ public async ValueTask SyncSettingsAsync()
+ {
+ if (Host.Character == null)
+ {
+ throw new ImpostorException("Attempted to set infected when the host was not spawned.");
+ }
+
+ using (var writer = StartRpc(Host.Character.NetId, RpcCalls.SyncSettings))
+ {
+ // Someone will probably forget to do this, so we include it here.
+ // If this is not done, the host will overwrite changes later with the defaults.
+ Options.IsDefaults = false;
+
+ await using (var memory = new MemoryStream())
+ await using (var writerBin = new BinaryWriter(memory))
+ {
+ Options.Serialize(writerBin, GameOptionsData.LatestVersion);
+ writer.WriteBytesAndSize(memory.ToArray());
+ }
+
+ await FinishRpcAsync(writer);
+ }
+ }
+
+ public async ValueTask SetInfectedAsync(IEnumerable<IInnerPlayerControl> players)
+ {
+ if (Host.Character == null)
+ {
+ throw new ImpostorException("Attempted to set infected when the host was not spawned.");
+ }
+
+ using (var writer = StartRpc(Host.Character.NetId, RpcCalls.SetInfected))
+ {
+ writer.Write((byte)Host.Character.NetId);
+
+ foreach (var player in players)
+ {
+ writer.Write((byte)player.PlayerId);
+ }
+
+ await FinishRpcAsync(writer);
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs
new file mode 100644
index 0000000..a84d9b5
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs
@@ -0,0 +1,449 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net.Messages;
+using Impostor.Api.Net.Messages.S2C;
+using Impostor.Hazel;
+using Impostor.Server.Events.Meeting;
+using Impostor.Server.Events.Player;
+using Impostor.Server.Net.Inner;
+using Impostor.Server.Net.Inner.Objects;
+using Impostor.Server.Net.Inner.Objects.Components;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.State
+{
+ internal partial class Game
+ {
+ private const int FakeClientId = int.MaxValue - 1;
+
+ /// <summary>
+ /// Used for global object, spawned by the host.
+ /// </summary>
+ private const int InvalidClient = -2;
+
+ /// <summary>
+ /// Used internally to set the OwnerId to the current ClientId.
+ /// i.e: <code>ownerId = ownerId == -3 ? this.ClientId : ownerId;</code>
+ /// </summary>
+ private const int CurrentClient = -3;
+
+ private static readonly Type[] SpawnableObjects =
+ {
+ typeof(InnerShipStatus), // ShipStatus
+ typeof(InnerMeetingHud),
+ typeof(InnerLobbyBehaviour),
+ typeof(InnerGameData),
+ typeof(InnerPlayerControl),
+ typeof(InnerShipStatus), // HeadQuarters
+ typeof(InnerShipStatus), // PlanetMap
+ typeof(InnerShipStatus), // AprilShipStatus
+ };
+
+ private readonly List<InnerNetObject> _allObjects = new List<InnerNetObject>();
+ private readonly Dictionary<uint, InnerNetObject> _allObjectsFast = new Dictionary<uint, InnerNetObject>();
+
+ private int _gamedataInitialized;
+ private bool _gamedataFakeReceived;
+
+ private async ValueTask OnSpawnAsync(InnerNetObject netObj)
+ {
+ switch (netObj)
+ {
+ case InnerLobbyBehaviour lobby:
+ {
+ GameNet.LobbyBehaviour = lobby;
+ break;
+ }
+
+ case InnerGameData data:
+ {
+ GameNet.GameData = data;
+ break;
+ }
+
+ case InnerVoteBanSystem voteBan:
+ {
+ GameNet.VoteBan = voteBan;
+ break;
+ }
+
+ case InnerShipStatus shipStatus:
+ {
+ GameNet.ShipStatus = shipStatus;
+ break;
+ }
+
+ case InnerPlayerControl control:
+ {
+ // Hook up InnerPlayerControl <-> IClientPlayer.
+ if (!TryGetPlayer(control.OwnerId, out var player))
+ {
+ throw new ImpostorException("Failed to find player that spawned the InnerPlayerControl");
+ }
+
+ player.Character = control;
+ player.DisableSpawnTimeout();
+
+ // Hook up InnerPlayerControl <-> InnerPlayerControl.PlayerInfo.
+ control.PlayerInfo = GameNet.GameData.GetPlayerById(control.PlayerId)!;
+
+ if (control.PlayerInfo == null)
+ {
+ GameNet.GameData.AddPlayer(control);
+ }
+
+ if (control.PlayerInfo != null)
+ {
+ control.PlayerInfo!.Controller = control;
+ }
+
+ await _eventManager.CallAsync(new PlayerSpawnedEvent(this, player, control));
+
+ break;
+ }
+
+ case InnerMeetingHud meetingHud:
+ {
+ await _eventManager.CallAsync(new MeetingStartedEvent(this, meetingHud));
+ break;
+ }
+ }
+ }
+
+ private async ValueTask OnDestroyAsync(InnerNetObject netObj)
+ {
+ switch (netObj)
+ {
+ case InnerLobbyBehaviour:
+ {
+ GameNet.LobbyBehaviour = null;
+ break;
+ }
+
+ case InnerGameData:
+ {
+ GameNet.GameData = null;
+ break;
+ }
+
+ case InnerVoteBanSystem:
+ {
+ GameNet.VoteBan = null;
+ break;
+ }
+
+ case InnerShipStatus:
+ {
+ GameNet.ShipStatus = null;
+ break;
+ }
+
+ case InnerPlayerControl control:
+ {
+ // Remove InnerPlayerControl <-> IClientPlayer.
+ if (TryGetPlayer(control.OwnerId, out var player))
+ {
+ player.Character = null;
+ }
+
+ await _eventManager.CallAsync(new PlayerDestroyedEvent(this, player, control));
+
+ break;
+ }
+ }
+ }
+
+ private async ValueTask InitGameDataAsync(ClientPlayer player)
+ {
+ if (Interlocked.Exchange(ref _gamedataInitialized, 1) != 0)
+ {
+ return;
+ }
+
+ /*
+ * The Among Us client on 20.9.22i spawns some components on the host side and
+ * only spawns these on other clients when someone else connects. This means that we can't
+ * parse data until someone connects because we don't know which component belongs to the NetId.
+ *
+ * We solve this by spawning a fake player and removing the player when the spawn GameData
+ * is received in HandleGameDataAsync.
+ */
+ using (var message = MessageWriter.Get(MessageType.Reliable))
+ {
+ // Spawn a fake player.
+ Message01JoinGameS2C.SerializeJoin(message, false, Code, FakeClientId, HostId);
+
+ message.StartMessage(MessageFlags.GameData);
+ message.Write(Code);
+ message.StartMessage(GameDataTag.SceneChangeFlag);
+ message.WritePacked(FakeClientId);
+ message.Write("OnlineGame");
+ message.EndMessage();
+ message.EndMessage();
+
+ await player.Client.Connection.SendAsync(message);
+ }
+ }
+
+ public async ValueTask<bool> HandleGameDataAsync(IMessageReader parent, ClientPlayer sender, bool toPlayer)
+ {
+ // Find target player.
+ ClientPlayer target = null;
+
+ if (toPlayer)
+ {
+ var targetId = parent.ReadPackedInt32();
+ if (targetId == FakeClientId && !_gamedataFakeReceived && sender.IsHost)
+ {
+ _gamedataFakeReceived = true;
+
+ // Remove the fake client, we received the data.
+ using (var message = MessageWriter.Get(MessageType.Reliable))
+ {
+ WriteRemovePlayerMessage(message, false, FakeClientId, (byte)DisconnectReason.ExitGame);
+
+ await sender.Client.Connection.SendAsync(message);
+ }
+ }
+ else if (!TryGetPlayer(targetId, out target))
+ {
+ _logger.LogWarning("Player {0} tried to send GameData to unknown player {1}.", sender.Client.Id, targetId);
+ return false;
+ }
+
+ _logger.LogTrace("Received GameData for target {0}.", targetId);
+ }
+
+ // Parse GameData messages.
+ while (parent.Position < parent.Length)
+ {
+ using var reader = parent.ReadMessage();
+
+ switch (reader.Tag)
+ {
+ case GameDataTag.DataFlag:
+ {
+ var netId = reader.ReadPackedUInt32();
+ if (_allObjectsFast.TryGetValue(netId, out var obj))
+ {
+ obj.Deserialize(sender, target, reader, false);
+ }
+ else
+ {
+ _logger.LogWarning("Received DataFlag for unregistered NetId {0}.", netId);
+ }
+
+ break;
+ }
+
+ case GameDataTag.RpcFlag:
+ {
+ var netId = reader.ReadPackedUInt32();
+ if (_allObjectsFast.TryGetValue(netId, out var obj))
+ {
+ await obj.HandleRpc(sender, target, (RpcCalls) reader.ReadByte(), reader);
+ }
+ else
+ {
+ _logger.LogWarning("Received RpcFlag for unregistered NetId {0}.", netId);
+ }
+
+ break;
+ }
+
+ case GameDataTag.SpawnFlag:
+ {
+ // Only the host is allowed to despawn objects.
+ if (!sender.IsHost)
+ {
+ throw new ImpostorCheatException("Tried to send SpawnFlag as non-host.");
+ }
+
+ var objectId = reader.ReadPackedUInt32();
+ if (objectId < SpawnableObjects.Length)
+ {
+ var innerNetObject = (InnerNetObject) ActivatorUtilities.CreateInstance(_serviceProvider, SpawnableObjects[objectId], this);
+ var ownerClientId = reader.ReadPackedInt32();
+
+ // Prevent fake client from being broadcasted.
+ // TODO: Remove message from stream properly.
+ if (ownerClientId == FakeClientId)
+ {
+ return false;
+ }
+
+ innerNetObject.SpawnFlags = (SpawnFlags) reader.ReadByte();
+
+ var components = innerNetObject.GetComponentsInChildren<InnerNetObject>();
+ var componentsCount = reader.ReadPackedInt32();
+
+ if (componentsCount != components.Count)
+ {
+ _logger.LogError(
+ "Children didn't match for spawnable {0}, name {1} ({2} != {3})",
+ objectId,
+ innerNetObject.GetType().Name,
+ componentsCount,
+ components.Count);
+ continue;
+ }
+
+ _logger.LogDebug(
+ "Spawning {0} components, SpawnFlags {1}",
+ innerNetObject.GetType().Name,
+ innerNetObject.SpawnFlags);
+
+ for (var i = 0; i < componentsCount; i++)
+ {
+ var obj = components[i];
+
+ obj.NetId = reader.ReadPackedUInt32();
+ obj.OwnerId = ownerClientId;
+
+ _logger.LogDebug(
+ "- {0}, NetId {1}, OwnerId {2}",
+ obj.GetType().Name,
+ obj.NetId,
+ obj.OwnerId);
+
+ if (!AddNetObject(obj))
+ {
+ _logger.LogTrace("Failed to AddNetObject, it already exists.");
+
+ obj.NetId = uint.MaxValue;
+ break;
+ }
+
+ using var readerSub = reader.ReadMessage();
+ if (readerSub.Length > 0)
+ {
+ obj.Deserialize(sender, target, readerSub, true);
+ }
+
+ await OnSpawnAsync(obj);
+ }
+
+ continue;
+ }
+
+ _logger.LogError("Couldn't find spawnable object {0}.", objectId);
+ break;
+ }
+
+ // Only the host is allowed to despawn objects.
+ case GameDataTag.DespawnFlag:
+ {
+ var netId = reader.ReadPackedUInt32();
+ if (_allObjectsFast.TryGetValue(netId, out var obj))
+ {
+ if (sender.Client.Id != obj.OwnerId && !sender.IsHost)
+ {
+ _logger.LogWarning(
+ "Player {0} ({1}) tried to send DespawnFlag for {2} but was denied.",
+ sender.Client.Name,
+ sender.Client.Id,
+ netId);
+ return false;
+ }
+
+ RemoveNetObject(obj);
+ await OnDestroyAsync(obj);
+ _logger.LogDebug("Destroyed InnerNetObject {0} ({1}), OwnerId {2}", obj.GetType().Name, netId, obj.OwnerId);
+ }
+ else
+ {
+ _logger.LogDebug(
+ "Player {0} ({1}) sent DespawnFlag for unregistered NetId {2}.",
+ sender.Client.Name,
+ sender.Client.Id,
+ netId);
+ }
+
+ break;
+ }
+
+ case GameDataTag.SceneChangeFlag:
+ {
+ // Sender is only allowed to change his own scene.
+ var clientId = reader.ReadPackedInt32();
+ if (clientId != sender.Client.Id)
+ {
+ _logger.LogWarning(
+ "Player {0} ({1}) tried to send SceneChangeFlag for another player.",
+ sender.Client.Name,
+ sender.Client.Id);
+ return false;
+ }
+
+ sender.Scene = reader.ReadString();
+
+ _logger.LogTrace("> Scene {0} to {1}", clientId, sender.Scene);
+ break;
+ }
+
+ case GameDataTag.ReadyFlag:
+ {
+ var clientId = reader.ReadPackedInt32();
+ _logger.LogTrace("> IsReady {0}", clientId);
+ break;
+ }
+
+ default:
+ {
+ _logger.LogTrace("Bad GameData tag {0}", reader.Tag);
+ break;
+ }
+ }
+
+ if (sender.Client.Player == null)
+ {
+ // Disconnect handler was probably invoked, cancel the rest.
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private bool AddNetObject(InnerNetObject obj)
+ {
+ if (_allObjectsFast.ContainsKey(obj.NetId))
+ {
+ return false;
+ }
+
+ _allObjects.Add(obj);
+ _allObjectsFast.Add(obj.NetId, obj);
+ return true;
+ }
+
+ private void RemoveNetObject(InnerNetObject obj)
+ {
+ var index = _allObjects.IndexOf(obj);
+ if (index > -1)
+ {
+ _allObjects.RemoveAt(index);
+ }
+
+ _allObjectsFast.Remove(obj.NetId);
+
+ obj.NetId = uint.MaxValue;
+ }
+
+ public T FindObjectByNetId<T>(uint netId)
+ where T : InnerNetObject
+ {
+ if (_allObjectsFast.TryGetValue(netId, out var obj))
+ {
+ return (T) obj;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs
new file mode 100644
index 0000000..4bf1c43
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs
@@ -0,0 +1,240 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Hazel;
+using Impostor.Server.Events;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.State
+{
+ internal partial class Game
+ {
+ private readonly SemaphoreSlim _clientAddLock = new SemaphoreSlim(1, 1);
+
+ public async ValueTask HandleStartGame(IMessageReader message)
+ {
+ GameState = GameStates.Starting;
+
+ using var packet = MessageWriter.Get(MessageType.Reliable);
+ message.CopyTo(packet);
+ await SendToAllAsync(packet);
+
+ await _eventManager.CallAsync(new GameStartingEvent(this));
+ }
+
+ public async ValueTask<GameJoinResult> AddClientAsync(ClientBase client)
+ {
+ var hasLock = false;
+
+ try
+ {
+ hasLock = await _clientAddLock.WaitAsync(TimeSpan.FromMinutes(1));
+
+ if (hasLock)
+ {
+ return await AddClientSafeAsync(client);
+ }
+ }
+ finally
+ {
+ if (hasLock)
+ {
+ _clientAddLock.Release();
+ }
+ }
+
+ return GameJoinResult.FromError(GameJoinError.InvalidClient);
+ }
+
+ private async ValueTask<GameJoinResult> AddClientSafeAsync(ClientBase client)
+ {
+ // Check if the IP of the player is banned.
+ if (client.Connection != null && _bannedIps.Contains(client.Connection.EndPoint.Address))
+ {
+ return GameJoinResult.FromError(GameJoinError.Banned);
+ }
+
+ var player = client.Player;
+
+ // Check if;
+ // - The player is already in this game.
+ // - The game is full.
+ if (player?.Game != this && _players.Count >= Options.MaxPlayers)
+ {
+ return GameJoinResult.FromError(GameJoinError.GameFull);
+ }
+
+ if (GameState == GameStates.Starting || GameState == GameStates.Started)
+ {
+ return GameJoinResult.FromError(GameJoinError.GameStarted);
+ }
+
+ if (GameState == GameStates.Destroyed)
+ {
+ return GameJoinResult.FromError(GameJoinError.GameDestroyed);
+ }
+
+ var isNew = false;
+
+ if (player == null || player.Game != this)
+ {
+ var clientPlayer = new ClientPlayer(_serviceProvider.GetRequiredService<ILogger<ClientPlayer>>(), client, this);
+
+ if (!_clientManager.Validate(client))
+ {
+ return GameJoinResult.FromError(GameJoinError.InvalidClient);
+ }
+
+ isNew = true;
+ player = clientPlayer;
+ client.Player = clientPlayer;
+ }
+
+ // Check current player state.
+ if (player.Limbo == LimboStates.NotLimbo)
+ {
+ return GameJoinResult.FromError(GameJoinError.InvalidLimbo);
+ }
+
+ if (GameState == GameStates.Ended)
+ {
+ await HandleJoinGameNext(player, isNew);
+ return GameJoinResult.CreateSuccess(player);
+ }
+
+ await HandleJoinGameNew(player, isNew);
+ return GameJoinResult.CreateSuccess(player);
+ }
+
+ public async ValueTask HandleEndGame(IMessageReader message, GameOverReason gameOverReason)
+ {
+ GameState = GameStates.Ended;
+
+ // Broadcast end of the game.
+ using (var packet = MessageWriter.Get(MessageType.Reliable))
+ {
+ message.CopyTo(packet);
+ await SendToAllAsync(packet);
+ }
+
+ // Put all players in the correct limbo state.
+ foreach (var player in _players)
+ {
+ player.Value.Limbo = LimboStates.PreSpawn;
+ }
+
+ await _eventManager.CallAsync(new GameEndedEvent(this, gameOverReason));
+ }
+
+ public async ValueTask HandleAlterGame(IMessageReader message, IClientPlayer sender, bool isPublic)
+ {
+ IsPublic = isPublic;
+
+ using var packet = MessageWriter.Get(MessageType.Reliable);
+ message.CopyTo(packet);
+ await SendToAllExceptAsync(packet, sender.Client.Id);
+
+ await _eventManager.CallAsync(new GameAlterEvent(this, isPublic));
+ }
+
+ public async ValueTask HandleRemovePlayer(int playerId, DisconnectReason reason)
+ {
+ await PlayerRemove(playerId);
+
+ // It's possible that the last player was removed, so check if the game is still around.
+ if (GameState == GameStates.Destroyed)
+ {
+ return;
+ }
+
+ using var packet = MessageWriter.Get(MessageType.Reliable);
+ WriteRemovePlayerMessage(packet, false, playerId, reason);
+ await SendToAllExceptAsync(packet, playerId);
+ }
+
+ public async ValueTask HandleKickPlayer(int playerId, bool isBan)
+ {
+ _logger.LogInformation("{0} - Player {1} has left.", Code, playerId);
+
+ using var message = MessageWriter.Get(MessageType.Reliable);
+
+ // Send message to everyone that this player was kicked.
+ WriteKickPlayerMessage(message, false, playerId, isBan);
+
+ await SendToAllAsync(message);
+ await PlayerRemove(playerId, isBan);
+
+ // Remove the player from everyone's game.
+ WriteRemovePlayerMessage(
+ message,
+ true,
+ playerId,
+ isBan ? DisconnectReason.Banned : DisconnectReason.Kicked);
+
+ await SendToAllExceptAsync(message, playerId);
+ }
+
+ private async ValueTask HandleJoinGameNew(ClientPlayer sender, bool isNew)
+ {
+ _logger.LogInformation("{0} - Player {1} ({2}) is joining.", Code, sender.Client.Name, sender.Client.Id);
+
+ // Add player to the game.
+ if (isNew)
+ {
+ await PlayerAdd(sender);
+ }
+
+ sender.InitializeSpawnTimeout();
+
+ using (var message = MessageWriter.Get(MessageType.Reliable))
+ {
+ WriteJoinedGameMessage(message, false, sender);
+ WriteAlterGameMessage(message, false, IsPublic);
+
+ sender.Limbo = LimboStates.NotLimbo;
+
+ await SendToAsync(message, sender.Client.Id);
+ await BroadcastJoinMessage(message, true, sender);
+ }
+ }
+
+ private async ValueTask HandleJoinGameNext(ClientPlayer sender, bool isNew)
+ {
+ _logger.LogInformation("{0} - Player {1} ({2}) is rejoining.", Code, sender.Client.Name, sender.Client.Id);
+
+ // Add player to the game.
+ if (isNew)
+ {
+ await PlayerAdd(sender);
+ }
+
+ // Check if the host joined and let everyone join.
+ if (sender.Client.Id == HostId)
+ {
+ GameState = GameStates.NotStarted;
+
+ // Spawn the host.
+ await HandleJoinGameNew(sender, false);
+
+ // Pull players out of limbo.
+ await CheckLimboPlayers();
+ return;
+ }
+
+ sender.Limbo = LimboStates.WaitingForHost;
+
+ using (var packet = MessageWriter.Get(MessageType.Reliable))
+ {
+ WriteWaitForHostMessage(packet, false, sender);
+
+ await SendToAsync(packet, sender.Client.Id);
+ await BroadcastJoinMessage(packet, true, sender);
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs
new file mode 100644
index 0000000..21b6067
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs
@@ -0,0 +1,103 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Api.Net.Messages.S2C;
+using Impostor.Hazel;
+using Impostor.Server.Net.Inner;
+
+namespace Impostor.Server.Net.State
+{
+ internal partial class Game
+ {
+ public async ValueTask SendToAllAsync(IMessageWriter writer, LimboStates states = LimboStates.NotLimbo)
+ {
+ foreach (var connection in GetConnections(x => x.Limbo.HasFlag(states)))
+ {
+ await connection.SendAsync(writer);
+ }
+ }
+
+ public async ValueTask SendToAllExceptAsync(IMessageWriter writer, int senderId, LimboStates states = LimboStates.NotLimbo)
+ {
+ foreach (var connection in GetConnections(x =>
+ x.Limbo.HasFlag(states) &&
+ x.Client.Id != senderId))
+ {
+ await connection.SendAsync(writer);
+ }
+ }
+
+ public async ValueTask SendToAsync(IMessageWriter writer, int id)
+ {
+ if (TryGetPlayer(id, out var player))
+ {
+ await player.Client.Connection.SendAsync(writer);
+ }
+ }
+
+ internal IMessageWriter StartRpc(uint targetNetId, RpcCalls callId, int targetClientId = -1, MessageType type = MessageType.Reliable)
+ {
+ var writer = MessageWriter.Get(type);
+
+ if (targetClientId < 0)
+ {
+ writer.StartMessage(MessageFlags.GameData);
+ writer.Write(Code);
+ }
+ else
+ {
+ writer.StartMessage(MessageFlags.GameDataTo);
+ writer.Write(Code);
+ writer.WritePacked(targetClientId);
+ }
+
+ writer.StartMessage(GameDataTag.RpcFlag);
+ writer.WritePacked(targetNetId);
+ writer.Write((byte) callId);
+
+ return writer;
+ }
+
+ internal ValueTask FinishRpcAsync(IMessageWriter writer, int? targetClientId = null)
+ {
+ writer.EndMessage();
+ writer.EndMessage();
+
+ return targetClientId.HasValue
+ ? SendToAsync(writer, targetClientId.Value)
+ : SendToAllAsync(writer);
+ }
+
+ private void WriteRemovePlayerMessage(IMessageWriter message, bool clear, int playerId, DisconnectReason reason)
+ {
+ Message04RemovePlayerS2C.Serialize(message, clear, Code, playerId, HostId, reason);
+ }
+
+ private void WriteJoinedGameMessage(IMessageWriter message, bool clear, IClientPlayer player)
+ {
+ var playerIds = _players
+ .Where(x => x.Value != player)
+ .Select(x => x.Key)
+ .ToArray();
+
+ Message07JoinedGameS2C.Serialize(message, clear, Code, player.Client.Id, HostId, playerIds);
+ }
+
+ private void WriteAlterGameMessage(IMessageWriter message, bool clear, bool isPublic)
+ {
+ Message10AlterGameS2C.Serialize(message, clear, Code, isPublic);
+ }
+
+ private void WriteKickPlayerMessage(IMessageWriter message, bool clear, int playerId, bool isBan)
+ {
+ Message11KickPlayerS2C.Serialize(message, clear, Code, playerId, isBan);
+ }
+
+ private void WriteWaitForHostMessage(IMessageWriter message, bool clear, IClientPlayer player)
+ {
+ Message12WaitForHostS2C.Serialize(message, clear, Code, player.Client.Id);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs
new file mode 100644
index 0000000..e311776
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs
@@ -0,0 +1,136 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Impostor.Api;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Hazel;
+using Impostor.Server.Events;
+using Impostor.Server.Net.Hazel;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.State
+{
+ internal partial class Game
+ {
+ private async ValueTask PlayerAdd(ClientPlayer player)
+ {
+ // Store player.
+ if (!_players.TryAdd(player.Client.Id, player))
+ {
+ throw new ImpostorException("Failed to add player to game.");
+ }
+
+ // Assign hostId if none is set.
+ if (HostId == -1)
+ {
+ HostId = player.Client.Id;
+ await InitGameDataAsync(player);
+ }
+
+ await _eventManager.CallAsync(new GamePlayerJoinedEvent(this, player));
+ }
+
+ private async ValueTask<bool> PlayerRemove(int playerId, bool isBan = false)
+ {
+ if (!_players.TryRemove(playerId, out var player))
+ {
+ return false;
+ }
+
+ _logger.LogInformation("{0} - Player {1} ({2}) has left.", Code, player.Client.Name, playerId);
+
+ if (GameState == GameStates.Starting || GameState == GameStates.Started)
+ {
+ if (player.Character?.PlayerInfo != null)
+ {
+ player.Character.PlayerInfo.Disconnected = true;
+ player.Character.PlayerInfo.LastDeathReason = DeathReason.Disconnect;
+ }
+ }
+
+ player.Client.Player = null;
+
+ // Game is empty, remove it.
+ if (_players.IsEmpty)
+ {
+ GameState = GameStates.Destroyed;
+
+ // Remove instance reference.
+ await _gameManager.RemoveAsync(Code);
+ return true;
+ }
+
+ // Host migration.
+ if (HostId == playerId)
+ {
+ await MigrateHost();
+ }
+
+ if (isBan && player.Client.Connection != null)
+ {
+ BanIp(player.Client.Connection.EndPoint.Address);
+ }
+
+ await _eventManager.CallAsync(new GamePlayerLeftEvent(this, player, isBan));
+
+ // Player can refuse to be kicked and keep the connection open, check for this.
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(Constants.ConnectionTimeout);
+
+ if (player.Client.Connection.IsConnected && player.Client.Connection is HazelConnection hazel)
+ {
+ _logger.LogInformation("{0} - Player {1} ({2}) kept connection open after leaving, disposing.", Code, player.Client.Name, playerId);
+ hazel.DisposeInnerConnection();
+ }
+ });
+
+ return true;
+ }
+
+ private async ValueTask MigrateHost()
+ {
+ // Pick the first player as new host.
+ var host = _players
+ .Select(p => p.Value)
+ .FirstOrDefault();
+
+ if (host == null)
+ {
+ await EndAsync();
+ return;
+ }
+
+ HostId = host.Client.Id;
+ _logger.LogInformation("{0} - Assigned {1} ({2}) as new host.", Code, host.Client.Name, host.Client.Id);
+
+ // Check our current game state.
+ if (GameState == GameStates.Ended && host.Limbo == LimboStates.WaitingForHost)
+ {
+ GameState = GameStates.NotStarted;
+
+ // Spawn the host.
+ await HandleJoinGameNew(host, false);
+
+ // Pull players out of limbo.
+ await CheckLimboPlayers();
+ }
+ }
+
+ private async ValueTask CheckLimboPlayers()
+ {
+ using var message = MessageWriter.Get(MessageType.Reliable);
+
+ foreach (var (_, player) in _players.Where(x => x.Value.Limbo == LimboStates.WaitingForHost))
+ {
+ WriteJoinedGameMessage(message, true, player);
+ WriteAlterGameMessage(message, false, IsPublic);
+
+ player.Limbo = LimboStates.NotLimbo;
+
+ await SendToAsync(message, player.Client.Id);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.cs
new file mode 100644
index 0000000..1919867
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api.Events.Managers;
+using Impostor.Api.Games;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Api.Net.Messages.S2C;
+using Impostor.Server.Events;
+using Impostor.Server.Net.Manager;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.State
+{
+ internal partial class Game
+ {
+ private readonly ILogger<Game> _logger;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly GameManager _gameManager;
+ private readonly ClientManager _clientManager;
+ private readonly ConcurrentDictionary<int, ClientPlayer> _players;
+ private readonly HashSet<IPAddress> _bannedIps;
+ private readonly IEventManager _eventManager;
+
+ public Game(
+ ILogger<Game> logger,
+ IServiceProvider serviceProvider,
+ GameManager gameManager,
+ IPEndPoint publicIp,
+ GameCode code,
+ GameOptionsData options,
+ ClientManager clientManager,
+ IEventManager eventManager)
+ {
+ _logger = logger;
+ _serviceProvider = serviceProvider;
+ _gameManager = gameManager;
+ _players = new ConcurrentDictionary<int, ClientPlayer>();
+ _bannedIps = new HashSet<IPAddress>();
+
+ PublicIp = publicIp;
+ Code = code;
+ HostId = -1;
+ GameState = GameStates.NotStarted;
+ GameNet = new GameNet();
+ Options = options;
+ _clientManager = clientManager;
+ _eventManager = eventManager;
+ Items = new ConcurrentDictionary<object, object>();
+ }
+
+ public IPEndPoint PublicIp { get; }
+
+ public GameCode Code { get; }
+
+ public bool IsPublic { get; private set; }
+
+ public int HostId { get; private set; }
+
+ public GameStates GameState { get; private set; }
+
+ internal GameNet GameNet { get; }
+
+ public GameOptionsData Options { get; }
+
+ public IDictionary<object, object> Items { get; }
+
+ public int PlayerCount => _players.Count;
+
+ public ClientPlayer Host => _players[HostId];
+
+ public IEnumerable<IClientPlayer> Players => _players.Select(p => p.Value);
+
+ public bool TryGetPlayer(int id, out ClientPlayer player)
+ {
+ if (_players.TryGetValue(id, out var result))
+ {
+ player = result;
+ return true;
+ }
+
+ player = default;
+ return false;
+ }
+
+ public IClientPlayer GetClientPlayer(int clientId)
+ {
+ return _players.TryGetValue(clientId, out var clientPlayer) ? clientPlayer : null;
+ }
+
+ internal ValueTask StartedAsync()
+ {
+ if (GameState == GameStates.Starting)
+ {
+ GameState = GameStates.Started;
+
+ return _eventManager.CallAsync(new GameStartedEvent(this));
+ }
+
+ return default;
+ }
+
+ public ValueTask EndAsync()
+ {
+ return _gameManager.RemoveAsync(Code);
+ }
+
+ private ValueTask BroadcastJoinMessage(IMessageWriter message, bool clear, ClientPlayer player)
+ {
+ Message01JoinGameS2C.SerializeJoin(message, clear, Code, player.Client.Id, HostId);
+
+ return SendToAllExceptAsync(message, player.Client.Id);
+ }
+
+ private IEnumerable<IHazelConnection> GetConnections(Func<IClientPlayer, bool> filter)
+ {
+ return Players
+ .Where(filter)
+ .Select(p => p.Client.Connection);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs
new file mode 100644
index 0000000..34ea0fe
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs
@@ -0,0 +1,17 @@
+using Impostor.Api.Net.Inner;
+using Impostor.Api.Net.Inner.Objects;
+
+namespace Impostor.Server.Net.State
+{
+ /// <inheritdoc />
+ internal partial class GameNet : IGameNet
+ {
+ IInnerLobbyBehaviour IGameNet.LobbyBehaviour => LobbyBehaviour;
+
+ IInnerGameData IGameNet.GameData => GameData;
+
+ IInnerVoteBanSystem IGameNet.VoteBan => VoteBan;
+
+ IInnerShipStatus IGameNet.ShipStatus => ShipStatus;
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs
new file mode 100644
index 0000000..c5542f9
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs
@@ -0,0 +1,16 @@
+using Impostor.Server.Net.Inner.Objects;
+using Impostor.Server.Net.Inner.Objects.Components;
+
+namespace Impostor.Server.Net.State
+{
+ internal partial class GameNet
+ {
+ public InnerLobbyBehaviour LobbyBehaviour { get; internal set; }
+
+ public InnerGameData GameData { get; internal set; }
+
+ public InnerVoteBanSystem VoteBan { get; internal set; }
+
+ public InnerShipStatus ShipStatus { get; internal set; }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs
new file mode 100644
index 0000000..5f6aee1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs
@@ -0,0 +1,38 @@
+using System.IO;
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Impostor.Server.Plugins
+{
+ public class AssemblyInformation : IAssemblyInformation
+ {
+ private Assembly _assembly;
+
+ public AssemblyInformation(AssemblyName assemblyName, string path, bool isPlugin)
+ {
+ AssemblyName = assemblyName;
+ Path = path;
+ IsPlugin = isPlugin;
+ }
+
+ public string Path { get; }
+
+ public bool IsPlugin { get; }
+
+ public AssemblyName AssemblyName { get; }
+
+ public Assembly Load(AssemblyLoadContext context)
+ {
+ if (_assembly != null)
+ {
+ return _assembly;
+ }
+
+ using var stream = File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read);
+
+ _assembly = context.LoadFromStream(stream);
+
+ return _assembly;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs
new file mode 100644
index 0000000..fb36e92
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs
@@ -0,0 +1,14 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Impostor.Server.Plugins
+{
+ public interface IAssemblyInformation
+ {
+ AssemblyName AssemblyName { get; }
+
+ bool IsPlugin { get; }
+
+ Assembly Load(AssemblyLoadContext context);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs
new file mode 100644
index 0000000..720367c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs
@@ -0,0 +1,25 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Impostor.Server.Plugins
+{
+ public class LoadedAssemblyInformation : IAssemblyInformation
+ {
+ private readonly Assembly _assembly;
+
+ public LoadedAssemblyInformation(Assembly assembly)
+ {
+ AssemblyName = assembly.GetName();
+ _assembly = assembly;
+ }
+
+ public AssemblyName AssemblyName { get; }
+
+ public bool IsPlugin => false;
+
+ public Assembly Load(AssemblyLoadContext context)
+ {
+ return _assembly;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs
new file mode 100644
index 0000000..22dc9e9
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace Impostor.Server.Plugins
+{
+ public class PluginConfig
+ {
+ public List<string> Paths { get; set; } = new List<string>();
+
+ public List<string> LibraryPaths { get; set; } = new List<string>();
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs
new file mode 100644
index 0000000..e6a5b6c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Reflection;
+using Impostor.Api.Plugins;
+
+namespace Impostor.Server.Plugins
+{
+ public class PluginInformation
+ {
+ private readonly ImpostorPluginAttribute _attribute;
+
+ public PluginInformation(IPluginStartup startup, Type pluginType)
+ {
+ _attribute = pluginType.GetCustomAttribute<ImpostorPluginAttribute>();
+
+ Startup = startup;
+ PluginType = pluginType;
+ }
+
+ public string Package => _attribute.Package;
+
+ public string Name => _attribute.Name;
+
+ public string Author => _attribute.Author;
+
+ public string Version => _attribute.Version;
+
+ public IPluginStartup Startup { get; }
+
+ public Type PluginType { get; }
+
+ public IPlugin Instance { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Package} {Name} ({Version}) by {Author}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs
new file mode 100644
index 0000000..4e72886
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+using Impostor.Api.Plugins;
+using Impostor.Server.Utils;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileSystemGlobbing;
+using Microsoft.Extensions.Hosting;
+using Serilog;
+
+namespace Impostor.Server.Plugins
+{
+ public static class PluginLoader
+ {
+ private static readonly ILogger Logger = Log.ForContext(typeof(PluginLoader));
+
+ public static IHostBuilder UsePluginLoader(this IHostBuilder builder, PluginConfig config)
+ {
+ var assemblyInfos = new List<IAssemblyInformation>();
+ var context = AssemblyLoadContext.Default;
+
+ // Add the plugins and libraries.
+ var pluginPaths = new List<string>(config.Paths);
+ var libraryPaths = new List<string>(config.LibraryPaths);
+
+ var rootFolder = Directory.GetCurrentDirectory();
+
+ pluginPaths.Add(Path.Combine(rootFolder, "plugins"));
+ libraryPaths.Add(Path.Combine(rootFolder, "libraries"));
+
+ var matcher = new Matcher(StringComparison.OrdinalIgnoreCase);
+ matcher.AddInclude("*.dll");
+ matcher.AddExclude("Impostor.Api.dll");
+
+ RegisterAssemblies(pluginPaths, matcher, assemblyInfos, true);
+ RegisterAssemblies(libraryPaths, matcher, assemblyInfos, false);
+
+ // Register the resolver to the current context.
+ // TODO: Move this to a new context so we can unload/reload plugins.
+ context.Resolving += (loadContext, name) =>
+ {
+ Logger.Verbose("Loading assembly {0} v{1}", name.Name, name.Version);
+
+ // Some plugins may be referencing another Impostor.Api version and try to load it.
+ // We want to only use the one shipped with the server.
+ if (name.Name.Equals("Impostor.Api"))
+ {
+ return typeof(IPlugin).Assembly;
+ }
+
+ var info = assemblyInfos.FirstOrDefault(a => a.AssemblyName.Name == name.Name);
+
+ return info?.Load(loadContext);
+ };
+
+ // TODO: Catch uncaught exceptions.
+ var assemblies = assemblyInfos
+ .Where(a => a.IsPlugin)
+ .Select(a => context.LoadFromAssemblyName(a.AssemblyName))
+ .ToList();
+
+ // Find all plugins.
+ var plugins = new List<PluginInformation>();
+
+ foreach (var assembly in assemblies)
+ {
+ // Find plugin startup.
+ var pluginStartup = assembly
+ .GetTypes()
+ .Where(t => typeof(IPluginStartup).IsAssignableFrom(t) && t.IsClass)
+ .ToList();
+
+ if (pluginStartup.Count > 1)
+ {
+ Logger.Warning("A plugin may only define zero or one IPluginStartup implementation ({0}).", assembly);
+ continue;
+ }
+
+ // Find plugin.
+ var plugin = assembly
+ .GetTypes()
+ .Where(t => typeof(IPlugin).IsAssignableFrom(t)
+ && t.IsClass
+ && !t.IsAbstract
+ && t.GetCustomAttribute<ImpostorPluginAttribute>() != null)
+ .ToList();
+
+ if (plugin.Count != 1)
+ {
+ Logger.Warning("A plugin must define exactly one IPlugin or PluginBase implementation ({0}).", assembly);
+ continue;
+ }
+
+ // Save plugin.
+ plugins.Add(new PluginInformation(
+ pluginStartup
+ .Select(Activator.CreateInstance)
+ .Cast<IPluginStartup>()
+ .FirstOrDefault(),
+ plugin.First()));
+ }
+
+ foreach (var plugin in plugins.Where(plugin => plugin.Startup != null))
+ {
+ plugin.Startup.ConfigureHost(builder);
+ }
+
+ builder.ConfigureServices(services =>
+ {
+ services.AddHostedService(provider => ActivatorUtilities.CreateInstance<PluginLoaderService>(provider, plugins));
+
+ foreach (var plugin in plugins.Where(plugin => plugin.Startup != null))
+ {
+ plugin.Startup.ConfigureServices(services);
+ }
+ });
+
+ return builder;
+ }
+
+ private static void RegisterAssemblies(
+ IEnumerable<string> paths,
+ Matcher matcher,
+ ICollection<IAssemblyInformation> assemblyInfos,
+ bool isPlugin)
+ {
+ foreach (var path in paths.SelectMany(matcher.GetResultsInFullPath))
+ {
+ AssemblyName assemblyName;
+
+ try
+ {
+ assemblyName = AssemblyName.GetAssemblyName(path);
+ }
+ catch (BadImageFormatException)
+ {
+ continue;
+ }
+
+ assemblyInfos.Add(new AssemblyInformation(assemblyName, path, isPlugin));
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs
new file mode 100644
index 0000000..64424a1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Runtime.Serialization;
+using Impostor.Api;
+
+namespace Impostor.Server.Plugins
+{
+ public class PluginLoaderException : ImpostorException
+ {
+ public PluginLoaderException()
+ {
+ }
+
+ protected PluginLoaderException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+
+ public PluginLoaderException(string? message) : base(message)
+ {
+ }
+
+ public PluginLoaderException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs
new file mode 100644
index 0000000..0afbc22
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Api.Plugins;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Plugins
+{
+ public class PluginLoaderService : IHostedService
+ {
+ private readonly ILogger<PluginLoaderService> _logger;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly List<PluginInformation> _plugins;
+
+ public PluginLoaderService(ILogger<PluginLoaderService> logger, IServiceProvider serviceProvider, List<PluginInformation> plugins)
+ {
+ _logger = logger;
+ _serviceProvider = serviceProvider;
+ _plugins = plugins;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Loading plugins.");
+
+ foreach (var plugin in _plugins)
+ {
+ _logger.LogInformation("Enabling plugin {0}.", plugin);
+
+ // Create instance and inject services.
+ plugin.Instance = (IPlugin) ActivatorUtilities.CreateInstance(_serviceProvider, plugin.PluginType);
+
+ // Enable plugin.
+ await plugin.Instance.EnableAsync();
+ }
+
+ _logger.LogInformation(
+ _plugins.Count == 1
+ ? "Loaded {0} plugin."
+ : "Loaded {0} plugins.", _plugins.Count);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ // Disable all plugins with a valid instance set.
+ // In the case of a failed startup, some can be null.
+ foreach (var plugin in _plugins.Where(plugin => plugin.Instance != null))
+ {
+ _logger.LogInformation("Disabling plugin {0}.", plugin);
+
+ // Disable plugin.
+ await plugin.Instance.DisableAsync();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Program.cs b/Impostor-dev/src/Impostor.Server/Program.cs
new file mode 100644
index 0000000..32a28fb
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Program.cs
@@ -0,0 +1,207 @@
+using System;
+using System.IO;
+using System.Linq;
+using Impostor.Api.Events.Managers;
+using Impostor.Api.Games;
+using Impostor.Api.Games.Managers;
+using Impostor.Api.Net.Manager;
+using Impostor.Api.Net.Messages;
+using Impostor.Hazel.Extensions;
+using Impostor.Server.Config;
+using Impostor.Server.Events;
+using Impostor.Server.Net;
+using Impostor.Server.Net.Factories;
+using Impostor.Server.Net.Manager;
+using Impostor.Server.Net.Messages;
+using Impostor.Server.Net.Redirector;
+using Impostor.Server.Plugins;
+using Impostor.Server.Recorder;
+using Impostor.Server.Utils;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.ObjectPool;
+using Serilog;
+using Serilog.Events;
+
+namespace Impostor.Server
+{
+ internal static class Program
+ {
+ private static int Main(string[] args)
+ {
+#if DEBUG
+ var logLevel = LogEventLevel.Debug;
+#else
+ var logLevel = LogEventLevel.Information;
+#endif
+
+ if (args.Contains("--verbose"))
+ {
+ logLevel = LogEventLevel.Verbose;
+ }
+ else if (args.Contains("--errors-only"))
+ {
+ logLevel = LogEventLevel.Error;
+ }
+
+ Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Is(logLevel)
+#if DEBUG
+ .MinimumLevel.Override("Microsoft", LogEventLevel.Debug)
+#else
+ .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
+#endif
+ .Enrich.FromLogContext()
+ .WriteTo.Console()
+ .CreateLogger();
+
+ try
+ {
+ Log.Information("Starting Impostor v{0}", DotnetUtils.GetVersion());
+ CreateHostBuilder(args).Build().Run();
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Log.Fatal(ex, "Impostor terminated unexpectedly");
+ return 1;
+ }
+ finally
+ {
+ Log.CloseAndFlush();
+ }
+ }
+
+ private static IConfiguration CreateConfiguration(string[] args)
+ {
+ var configurationBuilder = new ConfigurationBuilder();
+
+ configurationBuilder.AddJsonFile("config.json", true);
+ configurationBuilder.AddJsonFile("config.Development.json", true);
+ configurationBuilder.AddEnvironmentVariables(prefix: "IMPOSTOR_");
+ configurationBuilder.AddCommandLine(args);
+
+ return configurationBuilder.Build();
+ }
+
+ //c 入口
+ private static IHostBuilder CreateHostBuilder(string[] args)
+ {
+ var configuration = CreateConfiguration(args);
+ var pluginConfig = configuration.GetSection("PluginLoader")
+ .Get<PluginConfig>() ?? new PluginConfig();
+
+ return Host.CreateDefaultBuilder(args)
+ .UseContentRoot(Directory.GetCurrentDirectory())
+#if DEBUG
+ .UseEnvironment(Environment.GetEnvironmentVariable("IMPOSTOR_ENV") ?? "Development")
+#else
+ .UseEnvironment("Production")
+#endif
+ .ConfigureAppConfiguration(builder =>
+ {
+ builder.AddConfiguration(configuration);
+ })
+ .ConfigureServices((host, services) =>
+ {
+ var debug = host.Configuration
+ .GetSection(DebugConfig.Section)
+ .Get<DebugConfig>() ?? new DebugConfig();
+
+ var redirector = host.Configuration
+ .GetSection(ServerRedirectorConfig.Section)
+ .Get<ServerRedirectorConfig>() ?? new ServerRedirectorConfig();
+
+ services.Configure<DebugConfig>(host.Configuration.GetSection(DebugConfig.Section));
+ services.Configure<AntiCheatConfig>(host.Configuration.GetSection(AntiCheatConfig.Section));
+ services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section));
+ services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section));
+
+ if (redirector.Enabled)
+ {
+ if (!string.IsNullOrEmpty(redirector.Locator.Redis))
+ {
+ // When joining a game, it retrieves the game server ip from redis.
+ // When a game has been created on this node, it stores the game code with its ip in redis.
+ services.AddSingleton<INodeLocator, NodeLocatorRedis>();
+
+ // Dependency for the NodeLocatorRedis.
+ services.AddStackExchangeRedisCache(options =>
+ {
+ options.Configuration = redirector.Locator.Redis;
+ options.InstanceName = "ImpostorRedis";
+ });
+ }
+ else if (!string.IsNullOrEmpty(redirector.Locator.UdpMasterEndpoint))
+ {
+ services.AddSingleton<INodeLocator, NodeLocatorUdp>();
+
+ if (redirector.Master)
+ {
+ services.AddHostedService<NodeLocatorUdpService>();
+ }
+ }
+ else
+ {
+ throw new Exception("Missing a valid NodeLocator config.");
+ }
+
+ // Use the configuration as source for the list of nodes to provide
+ // when creating a game.
+ services.AddSingleton<INodeProvider, NodeProviderConfig>();
+ }
+ else
+ {
+ // Redirector is not enabled but the dependency is still required.
+ // So we provide one that ignores all calls.
+ services.AddSingleton<INodeLocator, NodeLocatorNoOp>();
+ }
+
+ services.AddSingleton<ClientManager>();
+ services.AddSingleton<IClientManager>(p => p.GetRequiredService<ClientManager>());
+
+ if (redirector.Enabled && redirector.Master)
+ {
+ services.AddSingleton<IClientFactory, ClientFactory<ClientRedirector>>();
+
+ // For a master server, we don't need a GameManager.
+ }
+ else
+ {
+ if (debug.GameRecorderEnabled)
+ {
+ services.AddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider());
+ services.AddSingleton<ObjectPool<PacketSerializationContext>>(serviceProvider =>
+ {
+ var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
+ var policy = new PacketSerializationContextPooledObjectPolicy();
+ return provider.Create(policy);
+ });
+
+ services.AddSingleton<PacketRecorder>();
+ services.AddHostedService(sp => sp.GetRequiredService<PacketRecorder>());
+ services.AddSingleton<IClientFactory, ClientFactory<ClientRecorder>>();
+ }
+ else
+ {
+ services.AddSingleton<IClientFactory, ClientFactory<Client>>();
+ }
+
+ services.AddSingleton<GameManager>();
+ services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>());
+ }
+
+ services.AddHazel();
+ services.AddSingleton<IMessageWriterProvider, MessageWriterProvider>();
+ services.AddSingleton<IGameCodeFactory, GameCodeFactory>();
+ services.AddSingleton<IEventManager, EventManager>();
+ services.AddSingleton<Matchmaker>();
+ services.AddHostedService<MatchmakerService>();
+ })
+ .UseSerilog()
+ .UseConsoleLifetime()
+ .UsePluginLoader(pluginConfig);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/ProjectRules.ruleset b/Impostor-dev/src/Impostor.Server/ProjectRules.ruleset
new file mode 100644
index 0000000..3654bc3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/ProjectRules.ruleset
@@ -0,0 +1,21 @@
+<RuleSet Name="Rules for Hello World project" Description="These rules focus on critical issues for the Hello World app." ToolsVersion="10.0">
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules">
+ <Rule Id="SA1200" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules">
+ <Rule Id="SA1600" Action="None" />
+ <Rule Id="SA1601" Action="None" />
+ <Rule Id="SA1615" Action="None" />
+ <Rule Id="SA1633" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.ReadabilityRules">
+ <Rule Id="SA1101" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules">
+ <Rule Id="SA1309" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.SpacingRules">
+ <Rule Id="SA1003" Action="None" />
+ <Rule Id="SA1009" Action="None" />
+ </Rules>
+</RuleSet> \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Properties/AssemblyInfo.cs b/Impostor-dev/src/Impostor.Server/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..84c5158
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Properties/AssemblyInfo.cs
@@ -0,0 +1,5 @@
+using System.Runtime.CompilerServices;
+
+[assembly:InternalsVisibleTo("Impostor.Benchmarks")]
+[assembly:InternalsVisibleTo("Impostor.Tests")]
+[assembly:InternalsVisibleTo("Impostor.Tools.ServerReplay")]
diff --git a/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs b/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs
new file mode 100644
index 0000000..5763c70
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs
@@ -0,0 +1,86 @@
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Config;
+using Impostor.Server.Net;
+using Impostor.Server.Net.Hazel;
+using Impostor.Server.Net.Manager;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Recorder
+{
+ internal class ClientRecorder : Client
+ {
+ private readonly PacketRecorder _recorder;
+ private bool _isFirst;
+ private bool _createdGame;
+ private bool _recordAfter;
+
+ public ClientRecorder(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection, PacketRecorder recorder)
+ : base(logger, antiCheatOptions, clientManager, gameManager, name, connection)
+ {
+ _recorder = recorder;
+ _isFirst = true;
+ _createdGame = false;
+ _recordAfter = true;
+ }
+
+ public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType)
+ {
+ using var messageCopy = reader.Copy();
+
+ // Trigger connect event.
+ if (_isFirst)
+ {
+ _isFirst = false;
+
+ await _recorder.WriteConnectAsync(this);
+ }
+
+ // Check if we were in-game before handling the message.
+ var inGame = Player?.Game != null;
+
+ if (!_recordAfter)
+ {
+ // Trigger message event.
+ await _recorder.WriteMessageAsync(this, messageCopy, messageType);
+ }
+
+ // Handle the message.
+ await base.HandleMessageAsync(reader, messageType);
+
+ // Player created a game.
+ if (reader.Tag == MessageFlags.HostGame)
+ {
+ _createdGame = true;
+ }
+ else if (reader.Tag == MessageFlags.JoinGame && _createdGame)
+ {
+ _createdGame = false;
+
+ // We created a game and are now in-game, store that event.
+ if (!inGame && Player?.Game != null)
+ {
+ await _recorder.WriteGameCreatedAsync(this, Player.Game.Code);
+ }
+
+ _recordAfter = false;
+
+ // Trigger message event.
+ await _recorder.WriteMessageAsync(this, messageCopy, messageType);
+ }
+
+ if (_recordAfter)
+ {
+ // Trigger message event.
+ await _recorder.WriteMessageAsync(this, messageCopy, messageType);
+ }
+ }
+
+ public override async ValueTask HandleDisconnectAsync(string reason)
+ {
+ await _recorder.WriteDisconnectAsync(this, reason);
+ await base.HandleDisconnectAsync(reason);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs
new file mode 100644
index 0000000..af2ea4f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs
@@ -0,0 +1,201 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using Impostor.Api.Games;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Config;
+using Impostor.Server.Net;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Recorder
+{
+ /// <summary>
+ /// Records all packets received in <see cref="ClientRecorder.HandleMessageAsync"/>.
+ /// </summary>
+ internal class PacketRecorder : BackgroundService
+ {
+ private readonly string _path;
+ private readonly ILogger<PacketRecorder> _logger;
+ private readonly ObjectPool<PacketSerializationContext> _pool;
+ private readonly Channel<byte[]> _channel;
+
+ public PacketRecorder(ILogger<PacketRecorder> logger, IOptions<DebugConfig> options, ObjectPool<PacketSerializationContext> pool)
+ {
+ var name = $"session_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.dat";
+
+ _path = Path.Combine(options.Value.GameRecorderPath, name);
+ _logger = logger;
+ _pool = pool;
+
+ _channel = Channel.CreateUnbounded<byte[]>(new UnboundedChannelOptions
+ {
+ SingleReader = true,
+ SingleWriter = false,
+ });
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogInformation("PacketRecorder is enabled, writing packets to {0}.", _path);
+
+ var writer = File.Open(_path, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
+
+ // Handle messages.
+ try
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ var result = await _channel.Reader.ReadAsync(stoppingToken);
+
+ await writer.WriteAsync(result, stoppingToken);
+ await writer.FlushAsync(stoppingToken);
+ }
+ }
+ catch (TaskCanceledException)
+ {
+ }
+
+ // Clean up.
+ await writer.DisposeAsync();
+ }
+
+ public async Task WriteConnectAsync(ClientRecorder client)
+ {
+ _logger.LogTrace("Writing Connect.");
+
+ var context = _pool.Get();
+
+ try
+ {
+ WriteHeader(context, RecordedPacketType.Connect);
+ WriteClient(context, client, true);
+ WriteLength(context);
+
+ await WriteAsync(context.Stream);
+ }
+ finally
+ {
+ _pool.Return(context);
+ }
+ }
+
+ public async Task WriteDisconnectAsync(ClientRecorder client, string reason)
+ {
+ _logger.LogTrace("Writing Disconnect.");
+
+ var context = _pool.Get();
+
+ try
+ {
+ WriteHeader(context, RecordedPacketType.Disconnect);
+ WriteClient(context, client, false);
+ context.Writer.Write(reason);
+ WriteLength(context);
+
+ await WriteAsync(context.Stream);
+ }
+ finally
+ {
+ _pool.Return(context);
+ }
+ }
+
+ public async Task WriteMessageAsync(ClientRecorder client, IMessageReader reader, MessageType messageType)
+ {
+ _logger.LogTrace("Writing Message.");
+
+ var context = _pool.Get();
+
+ try
+ {
+ WriteHeader(context, RecordedPacketType.Message);
+ WriteClient(context, client, false);
+ WritePacket(context, reader, messageType);
+ WriteLength(context);
+
+ await WriteAsync(context.Stream);
+ }
+ finally
+ {
+ _pool.Return(context);
+ }
+ }
+
+ public async Task WriteGameCreatedAsync(ClientRecorder client, GameCode gameCode)
+ {
+ _logger.LogTrace("Writing GameCreated {0}.", gameCode);
+
+ var context = _pool.Get();
+
+ try
+ {
+ WriteHeader(context, RecordedPacketType.GameCreated);
+ WriteClient(context, client, false);
+ WriteGameCode(context, gameCode);
+ WriteLength(context);
+
+ await WriteAsync(context.Stream);
+ }
+ finally
+ {
+ _pool.Return(context);
+ }
+ }
+
+ private static void WriteHeader(PacketSerializationContext context, RecordedPacketType type)
+ {
+ // Length placeholder.
+ context.Writer.Write((int) 0);
+ context.Writer.Write((byte) type);
+ }
+
+ private static void WriteClient(PacketSerializationContext context, ClientBase client, bool full)
+ {
+ var address = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 12345);
+ var addressBytes = address.Address.GetAddressBytes();
+
+ context.Writer.Write(client.Id);
+
+ if (full)
+ {
+ context.Writer.Write((byte) addressBytes.Length);
+ context.Writer.Write(addressBytes);
+ context.Writer.Write((ushort) address.Port);
+ context.Writer.Write(client.Name);
+ }
+ }
+
+ private static void WritePacket(PacketSerializationContext context, IMessageReader reader, MessageType messageType)
+ {
+ context.Writer.Write((byte) messageType);
+ context.Writer.Write((byte) reader.Tag);
+ context.Writer.Write((int) reader.Length);
+ context.Writer.Write(reader.Buffer, reader.Offset, reader.Length);
+ }
+
+ private static void WriteGameCode(PacketSerializationContext context, in GameCode gameCode)
+ {
+ context.Writer.Write(gameCode.Code);
+ }
+
+ private static void WriteLength(PacketSerializationContext context)
+ {
+ var length = context.Stream.Position;
+
+ context.Stream.Position = 0;
+ context.Writer.Write((int) length);
+ context.Stream.Position = length;
+ }
+
+ private async Task WriteAsync(MemoryStream data)
+ {
+ await _channel.Writer.WriteAsync(data.ToArray());
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs
new file mode 100644
index 0000000..07755f6
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs
@@ -0,0 +1,56 @@
+using System.IO;
+using System.Text;
+
+namespace Impostor.Server.Recorder
+{
+ public class PacketSerializationContext
+ {
+ private const int InitialStreamSize = 0x100;
+ private const int MaximumStreamSize = 0x100000;
+
+ private MemoryStream _memory;
+ private BinaryWriter _writer;
+
+ public MemoryStream Stream
+ {
+ get
+ {
+ if (_memory == null)
+ {
+ _memory = new MemoryStream(InitialStreamSize);
+ }
+
+ return _memory;
+ }
+ private set => _memory = value;
+ }
+
+ public BinaryWriter Writer
+ {
+ get
+ {
+ if (_writer == null)
+ {
+ _writer = new BinaryWriter(Stream, Encoding.UTF8, true);
+ }
+
+ return _writer;
+ }
+ private set => _writer = value;
+ }
+
+ public void Reset()
+ {
+ if (Stream.Capacity > MaximumStreamSize)
+ {
+ Stream = null;
+ Writer = null;
+ }
+ else
+ {
+ Stream.Position = 0L;
+ Stream.SetLength(0L);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs
new file mode 100644
index 0000000..17b355f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Server.Recorder
+{
+ public class PacketSerializationContextPooledObjectPolicy : IPooledObjectPolicy<PacketSerializationContext>
+ {
+ public PacketSerializationContext Create()
+ {
+ return new PacketSerializationContext();
+ }
+
+ public bool Return(PacketSerializationContext obj)
+ {
+ obj.Reset();
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs b/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs
new file mode 100644
index 0000000..a8a20bc
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs
@@ -0,0 +1,10 @@
+namespace Impostor.Server.Recorder
+{
+ internal enum RecordedPacketType : byte
+ {
+ Connect = 1,
+ Disconnect = 2,
+ Message = 3,
+ GameCreated = 4
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Utils/DotnetUtils.cs b/Impostor-dev/src/Impostor.Server/Utils/DotnetUtils.cs
new file mode 100644
index 0000000..48a0ac0
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Utils/DotnetUtils.cs
@@ -0,0 +1,20 @@
+using System.Reflection;
+
+namespace Impostor.Server.Utils
+{
+ internal static class DotnetUtils
+ {
+ private const string DefaultUnknownBuild = "UNKNOWN";
+
+ public static string GetVersion()
+ {
+ var attribute = typeof(DotnetUtils).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
+ if (attribute != null)
+ {
+ return attribute.InformationalVersion;
+ }
+
+ return DefaultUnknownBuild;
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Utils/IpUtils.cs b/Impostor-dev/src/Impostor.Server/Utils/IpUtils.cs
new file mode 100644
index 0000000..649d45a
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Utils/IpUtils.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using Impostor.Api;
+
+namespace Impostor.Server.Utils
+{
+ internal static class IpUtils
+ {
+ public static string ResolveIp(string ip)
+ {
+ // Check if valid ip was entered.
+ if (!IPAddress.TryParse(ip, out var ipAddress))
+ {
+ // Attempt to resolve DNS.
+ try
+ {
+ var hostAddresses = Dns.GetHostAddresses(ip);
+ if (hostAddresses.Length == 0)
+ {
+ throw new ImpostorConfigException($"Invalid IP Address entered '{ip}'.");
+ }
+
+ // Use first IPv4 result.
+ ipAddress = hostAddresses.First(x => x.AddressFamily == AddressFamily.InterNetwork);
+ }
+ catch (SocketException)
+ {
+ throw new ImpostorConfigException($"Failed to resolve hostname '{ip}'.");
+ }
+ }
+
+ // Only IPv4.
+ if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ throw new ImpostorConfigException($"Invalid IP Address entered '{ipAddress}', only IPv4 is supported by Among Us.");
+ }
+
+ return ipAddress.ToString();
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/config-full.json b/Impostor-dev/src/Impostor.Server/config-full.json
new file mode 100644
index 0000000..dfee1bc
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/config-full.json
@@ -0,0 +1,29 @@
+{
+ "Server": {
+ "PublicIp": "127.0.0.1",
+ "PublicPort": 22023,
+ "ListenIp": "0.0.0.0",
+ "ListenPort": 22023
+ },
+ "AntiCheat": {
+ "BanIpFromGame": true
+ },
+ "ServerRedirector": {
+ "Enabled": false,
+ "Master": true,
+ "Locator": {
+ "Redis": "127.0.0.1.6379",
+ "UdpMasterEndpoint": "127.0.0.1:32320"
+ },
+ "Nodes": [
+ {
+ "Ip": "127.0.0.1",
+ "Port": 22024
+ }
+ ]
+ },
+ "Debug": {
+ "GameRecorderEnabled": true,
+ "GameRecorderPath": ""
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/config.json b/Impostor-dev/src/Impostor.Server/config.json
new file mode 100644
index 0000000..d477c74
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/config.json
@@ -0,0 +1,11 @@
+{
+ "Server": {
+ "PublicIp": "127.0.0.1",
+ "PublicPort": 22023,
+ "ListenIp": "0.0.0.0",
+ "ListenPort": 22023
+ },
+ "AntiCheat": {
+ "BanIpFromGame": true
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/icon.ico b/Impostor-dev/src/Impostor.Server/icon.ico
new file mode 100644
index 0000000..8cc46f3
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/icon.ico
Binary files differ
diff --git a/Impostor-dev/src/Impostor.Tests/Events/EventManagerTests.cs b/Impostor-dev/src/Impostor.Tests/Events/EventManagerTests.cs
new file mode 100644
index 0000000..d222d79
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tests/Events/EventManagerTests.cs
@@ -0,0 +1,175 @@
+using System.Collections.Generic;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using Impostor.Api.Events;
+using Impostor.Api.Events.Managers;
+using Impostor.Server.Events;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Impostor.Tests.Events
+{
+ public class EventManagerTests
+ {
+ public static readonly IEnumerable<object[]> TestModes = new []
+ {
+ new object[] { TestMode.Service },
+ new object[] { TestMode.Temporary }
+ };
+
+ [Theory]
+ [MemberData(nameof(TestModes))]
+ public async ValueTask CallEvent(TestMode mode)
+ {
+ var listener = new EventListener();
+ var eventManager = CreatEventManager(mode, listener);
+
+ await eventManager.CallAsync(new SetValueEvent(1));
+
+ Assert.Equal(1, listener.Value);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestModes))]
+ public async Task CallPriority(TestMode mode)
+ {
+ var listener = new PriorityEventListener();
+ var eventManager = CreatEventManager(mode, listener);
+
+ await eventManager.CallAsync(new SetValueEvent(1));
+
+ Assert.Equal(new []
+ {
+ EventPriority.Monitor,
+ EventPriority.Highest,
+ EventPriority.High,
+ EventPriority.Normal,
+ EventPriority.Low,
+ EventPriority.Lowest
+ }, listener.Priorities);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestModes))]
+ public async ValueTask CancelEvent(TestMode mode)
+ {
+ var listener = new EventListener();
+ var eventManager = CreatEventManager(
+ mode,
+ new CancelAtHighEventListener(),
+ listener
+ );
+
+ await eventManager.CallAsync(new SetValueEvent(1));
+
+ Assert.Equal(0, listener.Value);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestModes))]
+ public async Task CancelPriority(TestMode mode)
+ {
+ var listener = new PriorityEventListener();
+ var eventManager = CreatEventManager(
+ mode,
+ new CancelAtHighEventListener(),
+ listener
+ );
+
+ await eventManager.CallAsync(new SetValueEvent(1));
+
+ Assert.Equal(new []
+ {
+ EventPriority.Monitor,
+ EventPriority.Highest
+ }, listener.Priorities);
+ }
+
+ private static IEventManager CreatEventManager(TestMode mode, params IEventListener[] listeners)
+ {
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddSingleton<IEventManager, EventManager>();
+
+ if (mode == TestMode.Service)
+ {
+ foreach (var listener in listeners)
+ {
+ services.AddSingleton(listener);
+ }
+ }
+
+ var eventManager = services.BuildServiceProvider().GetRequiredService<IEventManager>();
+
+ if (mode == TestMode.Temporary)
+ {
+ foreach (var listener in listeners)
+ {
+ eventManager.RegisterListener(listener);
+ }
+ }
+
+ return eventManager;
+ }
+
+ public enum TestMode
+ {
+ Service,
+ Temporary
+ }
+
+ public interface ISetValueEvent : IEventCancelable
+ {
+ int Value { get; }
+ }
+
+ public class SetValueEvent : ISetValueEvent
+ {
+ public SetValueEvent(int value)
+ {
+ Value = value;
+ }
+
+ public int Value { get; }
+
+ public bool IsCancelled { get; set; }
+ }
+
+ private class CancelAtHighEventListener : IEventListener
+ {
+ [EventListener(Priority = EventPriority.High)]
+ public void OnSetCalled(ISetValueEvent e) => e.IsCancelled = true;
+ }
+
+ private class EventListener : IEventListener
+ {
+ public int Value { get; private set; }
+
+ [EventListener]
+ public void OnSetCalled(ISetValueEvent e) => Value = e.Value;
+ }
+
+ private class PriorityEventListener : IEventListener
+ {
+ public List<EventPriority> Priorities { get; } = new List<EventPriority>();
+
+ [EventListener(EventPriority.Lowest)]
+ public void OnLowest(ISetValueEvent e) => Priorities.Add(EventPriority.Lowest);
+
+ [EventListener(EventPriority.Low)]
+ public void OnLow(ISetValueEvent e) => Priorities.Add(EventPriority.Low);
+
+ [EventListener]
+ public void OnNormal(ISetValueEvent e) => Priorities.Add(EventPriority.Normal);
+
+ [EventListener(EventPriority.High)]
+ public void OnHigh(ISetValueEvent e) => Priorities.Add(EventPriority.High);
+
+ [EventListener(EventPriority.Highest)]
+ public void OnHighest(ISetValueEvent e) => Priorities.Add(EventPriority.Highest);
+
+ [EventListener(EventPriority.Monitor)]
+ public void OnMonitor(ISetValueEvent e) => Priorities.Add(EventPriority.Monitor);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Tests/GameCodeTests.cs b/Impostor-dev/src/Impostor.Tests/GameCodeTests.cs
new file mode 100644
index 0000000..de98123
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tests/GameCodeTests.cs
@@ -0,0 +1,29 @@
+using Impostor.Api.Innersloth;
+
+using Xunit;
+
+namespace Impostor.Tests
+{
+ public class GameCodeTests
+ {
+ [Fact]
+ public void CodeV1()
+ {
+ 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));
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Tests/Hazel/MessageReaderTests.cs b/Impostor-dev/src/Impostor.Tests/Hazel/MessageReaderTests.cs
new file mode 100644
index 0000000..e7fa0df
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tests/Hazel/MessageReaderTests.cs
@@ -0,0 +1,372 @@
+using System;
+using System.Linq;
+using Impostor.Api;
+using Impostor.Hazel;
+using Impostor.Hazel.Extensions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ObjectPool;
+using Xunit;
+
+namespace Impostor.Tests.Hazel
+{
+ public class MessageReaderTests
+ {
+ private ObjectPool<MessageReader> CreateReaderPool()
+ {
+ var services = new ServiceCollection();
+ services.AddHazel();
+ return services.BuildServiceProvider().GetRequiredService<ObjectPool<MessageReader>>();
+ }
+
+ [Fact]
+ public void ReadProperInt()
+ {
+ const int Test1 = int.MaxValue;
+ const int Test2 = int.MinValue;
+
+ var msg = new MessageWriter(128);
+ msg.StartMessage(1);
+ msg.Write(Test1);
+ msg.Write(Test2);
+ msg.EndMessage();
+
+ Assert.Equal(11, msg.Length);
+ Assert.Equal(msg.Length, msg.Position);
+
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(msg.Buffer);
+ Assert.Equal(byte.MaxValue, reader.Tag);
+ var message = reader.ReadMessage();
+ Assert.Equal(1, message.Tag);
+ Assert.Equal(Test1, message.ReadInt32());
+ Assert.Equal(Test2, message.ReadInt32());
+ }
+
+ [Fact]
+ public void ReadProperBool()
+ {
+ const bool Test1 = true;
+ const bool Test2 = false;
+
+ var msg = new MessageWriter(128);
+ msg.StartMessage(1);
+ msg.Write(Test1);
+ msg.Write(Test2);
+ msg.EndMessage();
+
+ Assert.Equal(5, msg.Length);
+ Assert.Equal(msg.Length, msg.Position);
+
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(msg.Buffer);
+ Assert.Equal(byte.MaxValue, reader.Tag);
+ var message = reader.ReadMessage();
+ Assert.Equal(1, message.Tag);
+ Assert.Equal(Test1, message.ReadBoolean());
+ Assert.Equal(Test2, message.ReadBoolean());
+ }
+
+ [Fact]
+ public void ReadProperString()
+ {
+ const string Test1 = "Hello";
+ string Test2 = new string(' ', 1024);
+ var msg = new MessageWriter(2048);
+ msg.StartMessage(1);
+ msg.Write(Test1);
+ msg.Write(Test2);
+ msg.Write(string.Empty);
+ msg.EndMessage();
+
+ Assert.Equal(msg.Length, msg.Position);
+
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(msg.Buffer);
+ Assert.Equal(byte.MaxValue, reader.Tag);
+ var message = reader.ReadMessage();
+ Assert.Equal(1, message.Tag);
+ Assert.Equal(Test1, message.ReadString());
+ Assert.Equal(Test2, message.ReadString());
+ Assert.Equal(string.Empty, message.ReadString());
+ }
+
+ [Fact]
+ public void ReadProperFloat()
+ {
+ const float Test1 = 12.34f;
+
+ var msg = new MessageWriter(2048);
+ msg.StartMessage(1);
+ msg.Write(Test1);
+ msg.EndMessage();
+
+ Assert.Equal(7, msg.Length);
+ Assert.Equal(msg.Length, msg.Position);
+
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(msg.Buffer);
+ Assert.Equal(byte.MaxValue, reader.Tag);
+ var message = reader.ReadMessage();
+ Assert.Equal(1, message.Tag);
+ Assert.Equal(Test1, message.ReadSingle());
+ }
+
+ [Fact]
+ public void CopyMessage()
+ {
+ var readerPool = CreateReaderPool();
+
+ // Create message.
+ const int msgLength = 18;
+ const byte Test1 = 12;
+ const byte Test2 = 146;
+
+ var msg = new MessageWriter(2048);
+
+ msg.StartMessage(1);
+ msg.StartMessage(2);
+ msg.Write(Test1);
+ msg.Write(Test2);
+ msg.StartMessage(2);
+ msg.Write(Test1);
+ msg.Write(Test2);
+ msg.StartMessage(2);
+ msg.Write(Test1);
+ msg.Write(Test2);
+ msg.EndMessage();
+ msg.EndMessage();
+ msg.EndMessage();
+ msg.EndMessage();
+
+ // Read message.
+ using var reader = readerPool.Get();
+
+ reader.Update(msg.Buffer);
+
+ // Read first message.
+ using var messageOne = reader.ReadMessage();
+
+ Assert.Equal(1, messageOne.Tag);
+ Assert.Equal(0, messageOne.Position);
+ Assert.Equal(3, messageOne.Offset);
+ Assert.Equal(msgLength - 3, messageOne.Length);
+
+ using var messageTwo = messageOne.ReadMessage();
+
+ Assert.Equal(2, messageTwo.Tag);
+ Assert.Equal(0, messageTwo.Position);
+ Assert.Equal(6, messageTwo.Offset);
+ Assert.Equal(msgLength - 6, messageTwo.Length);
+ Assert.Equal(Test1, messageTwo.ReadByte());
+ Assert.Equal(Test2, messageTwo.ReadByte());
+
+ using var messageThree = messageTwo.ReadMessage();
+
+ Assert.Equal(2, messageThree.Tag);
+ Assert.Equal(0, messageThree.Position);
+ Assert.Equal(11, messageThree.Offset);
+ Assert.Equal(msgLength - 11, messageThree.Length);
+ Assert.Equal(Test1, messageThree.ReadByte());
+ Assert.Equal(Test2, messageThree.ReadByte());
+ }
+
+ [Fact]
+ public void CopySubMessage()
+ {
+ const byte Test1 = 12;
+ const byte Test2 = 146;
+
+ var msg = new MessageWriter(2048);
+ msg.StartMessage(1);
+
+ msg.StartMessage(2);
+ msg.Write(Test1);
+ msg.Write(Test2);
+ msg.EndMessage();
+
+ msg.EndMessage();
+
+ var readerPool = CreateReaderPool();
+ var handleReader = readerPool.Get();
+ handleReader.Update(msg.Buffer);
+ var handleMessage = handleReader.ReadMessage();
+ Assert.Equal(1, handleMessage.Tag);
+
+ using var parentReader = handleMessage.Copy();
+
+ Assert.Equal(1, parentReader.Tag);
+
+ var reader = parentReader.ReadMessage();
+
+ Assert.Equal(2, reader.Tag);
+ Assert.Equal(Test1, reader.ReadByte());
+ Assert.Equal(Test2, reader.ReadByte());
+ }
+
+ [Fact]
+ public void CopyToMessage()
+ {
+ var expected = new byte[]
+ {
+ 0x2A, 0x00, 0x01, 0x27, 0x00, 0x02, 0x26, 0x54,
+ 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61,
+ 0x20, 0x6C, 0x6F, 0x6E, 0x67, 0x20, 0x70, 0x61,
+ 0x63, 0x6B, 0x65, 0x74, 0x20, 0x74, 0x6F, 0x20,
+ 0x74, 0x65, 0x73, 0x74, 0x20, 0x63, 0x6F, 0x70,
+ 0x79, 0x69, 0x6E, 0x67, 0x2E
+ };
+
+ var readerPool = CreateReaderPool();
+
+ // Create packet.
+ var msg = new MessageWriter(2048);
+ msg.StartMessage(1);
+ msg.StartMessage(2);
+ msg.Write("This is a long packet to test copying.");
+ msg.EndMessage();
+ msg.EndMessage();
+
+ // Create a reader.
+ var reader = readerPool.Get();
+
+ reader.Update(msg.Buffer);
+
+ // Read the initial message.
+ var message = reader.ReadMessage();
+
+ // Copy the message to a new writer.
+ var writer = new MessageWriter(2048);
+
+ message.CopyTo(writer);
+
+ // Compare.
+ Assert.Equal(expected, writer.ToByteArray(true));
+ }
+
+ [Fact]
+ public void ReadMessageLength()
+ {
+ var msg = new MessageWriter(2048);
+ msg.StartMessage(1);
+ msg.Write(65534);
+ msg.StartMessage(2);
+ msg.Write("HO");
+ msg.EndMessage();
+ msg.StartMessage(2);
+ msg.Write("NO");
+ msg.EndMessage();
+ msg.EndMessage();
+
+ Assert.Equal(msg.Length, msg.Position);
+
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(msg.Buffer);
+ Assert.Equal(byte.MaxValue, reader.Tag);
+ var message = reader.ReadMessage();
+ Assert.Equal(1, message.Tag);
+ Assert.Equal(65534, message.ReadInt32()); // Content
+
+ var sub = message.ReadMessage();
+ Assert.Equal(3, sub.Length);
+ Assert.Equal(2, sub.Tag);
+ Assert.Equal("HO", sub.ReadString());
+
+ sub = message.ReadMessage();
+ Assert.Equal(3, sub.Length);
+ Assert.Equal(2, sub.Tag);
+ Assert.Equal("NO", sub.ReadString());
+ }
+
+ [Fact]
+ public void RemoveMessage()
+ {
+ // Create expected message.
+ var messageExpected = new MessageWriter(1024);
+
+ messageExpected.StartMessage(0);
+ messageExpected.StartMessage(1);
+ messageExpected.Write("HiTest1");
+ messageExpected.EndMessage();
+ messageExpected.StartMessage(2);
+ messageExpected.Write("HiTest2");
+ messageExpected.EndMessage();
+ messageExpected.EndMessage();
+
+ // Create message.
+ var messageWriter = new MessageWriter(1024);
+
+ messageWriter.StartMessage(0);
+ messageWriter.StartMessage(1);
+ messageWriter.Write("HiTest1");
+ messageWriter.StartMessage(2);
+ messageWriter.Write("RemoveMe!");
+ messageWriter.EndMessage();
+ messageWriter.EndMessage();
+ messageWriter.StartMessage(2);
+ messageWriter.Write("HiTest2");
+ messageWriter.EndMessage();
+ messageWriter.EndMessage();
+
+ // Copy buffer.
+ var bufferCopy = new byte[messageWriter.Length];
+ Buffer.BlockCopy(messageWriter.Buffer, 0, bufferCopy, 0, bufferCopy.Length);
+
+ var bufferCopyTwo = new byte[messageWriter.Length];
+ Buffer.BlockCopy(messageWriter.Buffer, 0, bufferCopyTwo, 0, bufferCopyTwo.Length);
+
+ // Do the magic.
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(bufferCopy);
+ var inner = reader.ReadMessage();
+
+ while (inner.Position < inner.Length)
+ {
+ var message = inner.ReadMessage();
+ if (message.Tag == 1)
+ {
+ Assert.Equal("HiTest1", message.ReadString());
+
+ var messageSub = message.ReadMessage();
+ if (messageSub.Tag == 2)
+ {
+ Assert.Equal("RemoveMe!", messageSub.ReadString());
+
+ // Remove this message.
+ inner.RemoveMessage(messageSub);
+ }
+ }
+ else if (message.Tag == 2)
+ {
+ Assert.Equal("HiTest2", message.ReadString());
+ }
+ else
+ {
+ Assert.True(false, "Invalid tag was read.");
+ }
+ }
+
+ // Check if the magic was successful.
+ Assert.Equal(messageExpected.Length, reader.Length);
+ Assert.Equal(messageExpected.ToByteArray(true), reader.Buffer.Take(reader.Length).ToArray());
+
+ // Test ownership.
+ var readerTwo = readerPool.Get();
+
+ readerTwo.Update(bufferCopyTwo);
+
+ Assert.Throws<ImpostorProtocolException>(() => reader.RemoveMessage(readerTwo.ReadMessage()));
+ }
+
+ [Fact]
+ public void GetLittleEndian()
+ {
+ Assert.True(MessageWriter.IsLittleEndian());
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Tests/Hazel/MessageWriterTests.cs b/Impostor-dev/src/Impostor.Tests/Hazel/MessageWriterTests.cs
new file mode 100644
index 0000000..83d67da
--- /dev/null
+++ b/Impostor-dev/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
diff --git a/Impostor-dev/src/Impostor.Tests/Impostor.Tests.csproj b/Impostor-dev/src/Impostor.Tests/Impostor.Tests.csproj
new file mode 100644
index 0000000..3f96bf6
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tests/Impostor.Tests.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ <IsPackable>false</IsPackable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.reporters" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
+ <PackageReference Include="coverlet.collector" Version="1.3.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Server\Impostor.Server.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Impostor-dev/src/Impostor.Tools.Proxy/HexUtils.cs b/Impostor-dev/src/Impostor.Tools.Proxy/HexUtils.cs
new file mode 100644
index 0000000..79517b4
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tools.Proxy/HexUtils.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Text;
+
+namespace Impostor.Tools.Proxy
+{
+ public static class HexUtils
+ {
+ public static string HexDump(byte[] bytes, int bytesPerLine = 16)
+ {
+ if (bytes == null) return "<null>";
+ int bytesLength = bytes.Length;
+
+ char[] HexChars = "0123456789ABCDEF".ToCharArray();
+
+ int firstHexColumn =
+ 8 // 8 characters for the address
+ + 3; // 3 spaces
+
+ int firstCharColumn = firstHexColumn
+ + bytesPerLine * 3 // - 2 digit for the hexadecimal value and 1 space
+ + (bytesPerLine - 1) / 8 // - 1 extra space every 8 characters from the 9th
+ + 2; // 2 spaces
+
+ int lineLength = firstCharColumn
+ + bytesPerLine // - characters to show the ascii value
+ + Environment.NewLine.Length; // Carriage return and line feed (should normally be 2)
+
+ char[] line = (new String(' ', lineLength - Environment.NewLine.Length) + Environment.NewLine).ToCharArray();
+ int expectedLines = (bytesLength + bytesPerLine - 1) / bytesPerLine;
+ StringBuilder result = new StringBuilder(expectedLines * lineLength);
+
+ for (int i = 0; i < bytesLength; i += bytesPerLine)
+ {
+ line[0] = HexChars[(i >> 28) & 0xF];
+ line[1] = HexChars[(i >> 24) & 0xF];
+ line[2] = HexChars[(i >> 20) & 0xF];
+ line[3] = HexChars[(i >> 16) & 0xF];
+ line[4] = HexChars[(i >> 12) & 0xF];
+ line[5] = HexChars[(i >> 8) & 0xF];
+ line[6] = HexChars[(i >> 4) & 0xF];
+ line[7] = HexChars[(i >> 0) & 0xF];
+
+ int hexColumn = firstHexColumn;
+ int charColumn = firstCharColumn;
+
+ for (int j = 0; j < bytesPerLine; j++)
+ {
+ if (j > 0 && (j & 7) == 0) hexColumn++;
+ if (i + j >= bytesLength)
+ {
+ line[hexColumn] = ' ';
+ line[hexColumn + 1] = ' ';
+ line[charColumn] = ' ';
+ }
+ else
+ {
+ byte b = bytes[i + j];
+ line[hexColumn] = HexChars[(b >> 4) & 0xF];
+ line[hexColumn + 1] = HexChars[b & 0xF];
+ line[charColumn] = (b < 32 ? '·' : (char)b);
+ }
+ hexColumn += 3;
+ charColumn++;
+ }
+ result.Append(line);
+ }
+ return result.ToString();
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Tools.Proxy/Impostor.Tools.Proxy.csproj b/Impostor-dev/src/Impostor.Tools.Proxy/Impostor.Tools.Proxy.csproj
new file mode 100644
index 0000000..8f523dc
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tools.Proxy/Impostor.Tools.Proxy.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
+ <PackageReference Include="Pcap.Net.x64" Version="1.0.4.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Hazel\Impostor.Hazel.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Impostor-dev/src/Impostor.Tools.Proxy/Program.cs b/Impostor-dev/src/Impostor.Tools.Proxy/Program.cs
new file mode 100644
index 0000000..9e765f0
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tools.Proxy/Program.cs
@@ -0,0 +1,198 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Impostor.Api.Net.Messages;
+using Impostor.Hazel;
+using Impostor.Hazel.Extensions;
+using Impostor.Hazel.Udp;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ObjectPool;
+using PcapDotNet.Core;
+using PcapDotNet.Packets;
+
+namespace Impostor.Tools.Proxy
+{
+ internal static class Program
+ {
+ private const string DeviceName = "Intel(R) I211 Gigabit Network Connection";
+
+ private static readonly Dictionary<byte, string> TagMap = new Dictionary<byte, string>
+ {
+ {0, "HostGame"},
+ {1, "JoinGame"},
+ {2, "StartGame"},
+ {3, "RemoveGame"},
+ {4, "RemovePlayer"},
+ {5, "GameData"},
+ {6, "GameDataTo"},
+ {7, "JoinedGame"},
+ {8, "EndGame"},
+ {9, "GetGameList"},
+ {10, "AlterGame"},
+ {11, "KickPlayer"},
+ {12, "WaitForHost"},
+ {13, "Redirect"},
+ {14, "ReselectServer"},
+ {16, "GetGameListV2"}
+ };
+
+ private static IServiceProvider _serviceProvider;
+ private static ObjectPool<MessageReader> _readerPool;
+
+ //c 服务器入口
+ private static void Main(string[] args)
+ {
+ var services = new ServiceCollection();
+ services.AddHazel();
+
+ _serviceProvider = services.BuildServiceProvider();
+ _readerPool = _serviceProvider.GetRequiredService<ObjectPool<MessageReader>>();
+
+ var devices = LivePacketDevice.AllLocalMachine;
+ if (devices.Count == 0)
+ {
+ Console.WriteLine("No interfaces found! Make sure WinPcap is installed.");
+ return;
+ }
+
+ var device = devices.FirstOrDefault(x => x.Description.Contains(DeviceName));
+ if (device == null)
+ {
+ Console.WriteLine("Unable to find configured device.");
+ return;
+ }
+
+ using (var communicator = device.Open(65536, PacketDeviceOpenAttributes.Promiscuous, 1000))
+ {
+ using (var filter = communicator.CreateFilter("udp and port 22023"))
+ {
+ communicator.SetFilter(filter);
+ }
+
+ communicator.ReceivePackets(0, PacketHandler);
+ }
+ }
+
+ private static void PacketHandler(Packet packet)
+ {
+ var ip = packet.Ethernet.IpV4;
+ var ipSrc = ip.Source.ToString();
+ var udp = ip.Udp;
+
+ // True if this is our own packet.
+ using (var stream = udp.Payload.ToMemoryStream())
+ {
+ using var reader = _readerPool.Get();
+
+ reader.Update(stream.ToArray());
+
+ var option = reader.Buffer[0];
+ if (option == (byte) MessageType.Reliable)
+ {
+ reader.Seek(reader.Position + 3);
+ }
+ else if (option == (byte) UdpSendOption.Acknowledgement ||
+ option == (byte) UdpSendOption.Ping ||
+ option == (byte) UdpSendOption.Hello ||
+ option == (byte) UdpSendOption.Disconnect)
+ {
+ return;
+ }
+ else
+ {
+ reader.Seek(reader.Position + 1);
+ }
+
+ var isSent = ipSrc.StartsWith("192.");
+
+ while (true)
+ {
+ if (reader.Position >= reader.Length)
+ {
+ break;
+ }
+
+ //c 消息
+ using var message = reader.ReadMessage();
+ if (isSent)
+ {
+ HandleToServer(ipSrc, message);
+ }
+ else
+ {
+ HandleToClient(ipSrc, message);
+ }
+
+ if (message.Position < message.Length)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine("- Did not consume all bytes.");
+ }
+ }
+ }
+ }
+
+ private static void HandleToClient(string source, IMessageReader packet)
+ {
+ var tagName = TagMap.ContainsKey(packet.Tag) ? TagMap[packet.Tag] : "Unknown";
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine($"{source,-15} Client received: {packet.Tag,-2} {tagName}");
+
+ switch (packet.Tag)
+ {
+ case 14:
+ case 13:
+ // packet.Position = packet.Length;
+ break;
+ case 0:
+ Console.WriteLine("- GameCode " + packet.ReadInt32());
+ break;
+ case 5:
+ case 6:
+ Console.WriteLine(HexUtils.HexDump(packet.Buffer.ToArray().Take(packet.Length).ToArray()));
+ // packet.Position = packet.Length;
+ break;
+ case 7:
+ Console.WriteLine("- GameCode " + packet.ReadInt32());
+ Console.WriteLine("- PlayerId " + packet.ReadInt32());
+ Console.WriteLine("- Host " + packet.ReadInt32());
+ var playerCount = packet.ReadPackedInt32();
+ Console.WriteLine("- PlayerCount " + playerCount);
+ for (var i = 0; i < playerCount; i++)
+ {
+ Console.WriteLine("- PlayerId " + packet.ReadPackedInt32());
+ }
+ break;
+ case 10:
+ Console.WriteLine("- GameCode " + packet.ReadInt32());
+ Console.WriteLine("- Flag " + packet.ReadSByte());
+ Console.WriteLine("- Value " + packet.ReadBoolean());
+ break;
+ }
+ }
+
+ private static void HandleToServer(string source, IMessageReader packet)
+ {
+ var tagName = TagMap.ContainsKey(packet.Tag) ? TagMap[packet.Tag] : "Unknown";
+ Console.ForegroundColor = ConsoleColor.White;
+ Console.WriteLine($"{source,-15} Server received: {packet.Tag,-2} {tagName}");
+
+ switch (packet.Tag)
+ {
+ case 0:
+ Console.WriteLine("- GameInfo length " + packet.ReadBytesAndSize().Length);
+ break;
+ case 1:
+ Console.WriteLine("- GameCode " + packet.ReadInt32());
+ Console.WriteLine("- Unknown " + packet.ReadByte());
+ break;
+ case 5:
+ case 6:
+ Console.WriteLine("- GameCode " + packet.ReadInt32());
+ Console.WriteLine(HexUtils.HexDump(packet.Buffer.ToArray().Take(packet.Length).ToArray()));
+ // packet.Position = packet.Length;
+ break;
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj b/Impostor-dev/src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj
new file mode 100644
index 0000000..98fa689
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Server\Impostor.Server.csproj"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1"/>
+ </ItemGroup>
+
+</Project>
diff --git a/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs b/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs
new file mode 100644
index 0000000..1111b7e
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs
@@ -0,0 +1,14 @@
+using Impostor.Api.Games;
+
+namespace Impostor.Tools.ServerReplay.Mocks
+{
+ public class MockGameCodeFactory : IGameCodeFactory
+ {
+ public GameCode Result { get; set; }
+
+ public GameCode Create()
+ {
+ return Result;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs b/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs
new file mode 100644
index 0000000..43f0257
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs
@@ -0,0 +1,31 @@
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+
+namespace Impostor.Tools.ServerReplay.Mocks
+{
+ public class MockHazelConnection : IHazelConnection
+ {
+ public MockHazelConnection(IPEndPoint endPoint)
+ {
+ EndPoint = endPoint;
+ IsConnected = true;
+ Client = null;
+ }
+
+ public IPEndPoint EndPoint { get; }
+ public bool IsConnected { get; }
+ public IClient? Client { get; set; }
+
+ public ValueTask SendAsync(IMessageWriter writer)
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ public ValueTask DisconnectAsync(string reason)
+ {
+ return ValueTask.CompletedTask;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Tools.ServerReplay/Program.cs b/Impostor-dev/src/Impostor.Tools.ServerReplay/Program.cs
new file mode 100644
index 0000000..5aaa954
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tools.ServerReplay/Program.cs
@@ -0,0 +1,195 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Api.Events.Managers;
+using Impostor.Api.Games;
+using Impostor.Api.Games.Managers;
+using Impostor.Api.Innersloth;
+using Impostor.Api.Net;
+using Impostor.Api.Net.Messages;
+using Impostor.Api.Net.Messages.C2S;
+using Impostor.Hazel;
+using Impostor.Hazel.Extensions;
+using Impostor.Server.Events;
+using Impostor.Server.Net;
+using Impostor.Server.Net.Factories;
+using Impostor.Server.Net.Manager;
+using Impostor.Server.Net.Redirector;
+using Impostor.Server.Recorder;
+using Impostor.Tools.ServerReplay.Mocks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
+using Serilog;
+using ILogger = Serilog.ILogger;
+
+namespace Impostor.Tools.ServerReplay
+{
+ internal static class Program
+ {
+ private static readonly ILogger Logger = Log.ForContext(typeof(Program));
+ private static readonly Dictionary<int, IHazelConnection> Connections = new Dictionary<int, IHazelConnection>();
+ private static readonly Dictionary<int, GameOptionsData> GameOptions = new Dictionary<int, GameOptionsData>();
+
+ private static ServiceProvider _serviceProvider;
+
+ private static ObjectPool<MessageReader> _readerPool;
+ private static MockGameCodeFactory _gameCodeFactory;
+ private static ClientManager _clientManager;
+ private static GameManager _gameManager;
+
+ private static async Task Main(string[] args)
+ {
+ Log.Logger = new LoggerConfiguration()
+ // .MinimumLevel.Verbose()
+ .MinimumLevel.Debug()
+ .WriteTo.Console()
+ .CreateLogger();
+
+ var stopwatch = Stopwatch.StartNew();
+
+ foreach (var file in Directory.GetFiles(args[0]))
+ {
+ // Clear.
+ Connections.Clear();
+ GameOptions.Clear();
+
+ // Create service provider.
+ _serviceProvider = BuildServices();
+
+ // Create required instances.
+ _readerPool = _serviceProvider.GetRequiredService<ObjectPool<MessageReader>>();
+ _gameCodeFactory = _serviceProvider.GetRequiredService<MockGameCodeFactory>();
+ _clientManager = _serviceProvider.GetRequiredService<ClientManager>();
+ _gameManager = _serviceProvider.GetRequiredService<GameManager>();
+
+ await using (var stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (var reader = new BinaryReader(stream))
+ {
+ await ParseSession(reader);
+ }
+ }
+
+ var elapsedMilliseconds = stopwatch.ElapsedMilliseconds;
+
+ Logger.Information($"Took {elapsedMilliseconds}ms.");
+ }
+
+ private static ServiceProvider BuildServices()
+ {
+ var services = new ServiceCollection();
+
+ services.AddLogging(builder =>
+ {
+ builder.ClearProviders();
+ builder.AddSerilog();
+ });
+
+ services.AddSingleton<GameManager>();
+ services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>());
+
+ services.AddSingleton<MockGameCodeFactory>();
+ services.AddSingleton<IGameCodeFactory>(p => p.GetRequiredService<MockGameCodeFactory>());
+
+ services.AddSingleton<ClientManager>();
+ services.AddSingleton<IClientFactory, ClientFactory<Client>>();
+ services.AddSingleton<INodeLocator, NodeLocatorNoOp>();
+ services.AddSingleton<IEventManager, EventManager>();
+
+ services.AddHazel();
+
+ return services.BuildServiceProvider();
+ }
+
+ private static async Task ParseSession(BinaryReader reader)
+ {
+ while (reader.BaseStream.Position < reader.BaseStream.Length)
+ {
+ var dataLength = reader.ReadInt32();
+ var data = reader.ReadBytes(dataLength - 4);
+
+ await using (var stream = new MemoryStream(data))
+ using (var readerInner = new BinaryReader(stream))
+ {
+ await ParsePacket(readerInner);
+ }
+ }
+ }
+
+ private static async Task ParsePacket(BinaryReader reader)
+ {
+ var dataType = (RecordedPacketType) reader.ReadByte();
+
+ // Read client id.
+ var clientId = reader.ReadInt32();
+
+ switch (dataType)
+ {
+ case RecordedPacketType.Connect:
+ // Read data.
+ var addressLength = reader.ReadByte();
+ var addressBytes = reader.ReadBytes(addressLength);
+ var addressPort = reader.ReadUInt16();
+ var address = new IPEndPoint(new IPAddress(addressBytes), addressPort);
+ var name = reader.ReadString();
+
+ // Create and register connection.
+ var connection = new MockHazelConnection(address);
+
+ await _clientManager.RegisterConnectionAsync(connection, name, 50516550);
+
+ // Store reference for ourselfs.
+ Connections.Add(clientId, connection);
+ break;
+
+ case RecordedPacketType.Disconnect:
+ string reason = null;
+
+ if (reader.BaseStream.Position < reader.BaseStream.Length)
+ {
+ reason = reader.ReadString();
+ }
+
+ await Connections[clientId].Client!.HandleDisconnectAsync(reason);
+ Connections.Remove(clientId);
+ break;
+
+ case RecordedPacketType.Message:
+ {
+ var messageType = (MessageType)reader.ReadByte();
+ var tag = reader.ReadByte();
+ var length = reader.ReadInt32();
+ var buffer = reader.ReadBytes(length);
+ using var message = _readerPool.Get();
+
+ message.Update(buffer, tag: tag);
+
+ if (tag == MessageFlags.HostGame)
+ {
+ GameOptions.Add(clientId, Message00HostGameC2S.Deserialize(message));
+ }
+ else if (Connections.TryGetValue(clientId, out var client))
+ {
+ await client.Client!.HandleMessageAsync(message, messageType);
+ }
+
+ break;
+ }
+
+ case RecordedPacketType.GameCreated:
+ _gameCodeFactory.Result = GameCode.From(reader.ReadString());
+
+ await _gameManager.CreateAsync(GameOptions[clientId]);
+
+ GameOptions.Remove(clientId);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.dat b/Impostor-dev/src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.dat
new file mode 100644
index 0000000..e5f441b
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.dat
Binary files differ
diff --git a/Impostor-dev/src/Impostor.sln b/Impostor-dev/src/Impostor.sln
new file mode 100644
index 0000000..b9706de
--- /dev/null
+++ b/Impostor-dev/src/Impostor.sln
@@ -0,0 +1,177 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30503.244
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Tests", "Impostor.Tests\Impostor.Tests.csproj", "{C1385C67-A7DD-46D2-80F6-FA428B85FB22}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{56DD9707-D811-4056-9E2C-8A9CC2479B07}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Tools.Proxy", "Impostor.Tools.Proxy\Impostor.Tools.Proxy.csproj", "{D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Server", "Impostor.Server\Impostor.Server.csproj", "{1B0390AF-A4F3-4FE4-B093-708B0135C0B3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Api", "Impostor.Api\Impostor.Api.csproj", "{E096A7D7-D693-4A13-A526-38CC574D84F8}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "patcher", "patcher", "{94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.Shared", "Impostor.Patcher\Impostor.Patcher.Shared\Impostor.Patcher.Shared.csproj", "{7C3EB599-2292-4532-B280-D5BED1094DD4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.WinForms", "Impostor.Patcher\Impostor.Patcher.WinForms\Impostor.Patcher.WinForms.csproj", "{804CF172-0C87-4423-9688-BD97D549891E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.Cli", "Impostor.Patcher\Impostor.Patcher.Cli\Impostor.Patcher.Cli.csproj", "{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Plugins.Debugger", "Impostor.Plugins.Debugger\Impostor.Plugins.Debugger.csproj", "{ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Hazel", "Impostor.Hazel\Impostor.Hazel.csproj", "{671B753B-31AE-4C36-AD71-09CF00FA17CA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "client", "client", "{9F1919B0-915B-4749-9944-697DF7E7F67F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client", "Impostor.Client\Impostor.Client.csproj", "{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client.App", "Impostor.Client.App\Impostor.Client.App.csproj", "{3DF86F12-7099-44F6-B98B-A148213D60B1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Tools.ServerReplay", "Impostor.Tools.ServerReplay\Impostor.Tools.ServerReplay.csproj", "{4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Plugins.Example", "Impostor.Plugins.Example\Impostor.Plugins.Example.csproj", "{70F97BB7-12A1-4C7A-B2A5-B962D14B813E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Benchmarks", "Impostor.Benchmarks\Impostor.Benchmarks.csproj", "{EA04E386-6CCB-4C52-8C82-64F32C7EB377}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Debug|x86.Build.0 = Debug|Any CPU
+ {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Release|x86.ActiveCfg = Release|Any CPU
+ {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Release|x86.Build.0 = Release|Any CPU
+ {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Debug|x86.Build.0 = Debug|Any CPU
+ {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Release|x86.ActiveCfg = Release|Any CPU
+ {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Release|x86.Build.0 = Release|Any CPU
+ {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Debug|x86.Build.0 = Debug|Any CPU
+ {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|x86.ActiveCfg = Release|Any CPU
+ {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|x86.Build.0 = Release|Any CPU
+ {804CF172-0C87-4423-9688-BD97D549891E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {804CF172-0C87-4423-9688-BD97D549891E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {804CF172-0C87-4423-9688-BD97D549891E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {804CF172-0C87-4423-9688-BD97D549891E}.Release|x86.ActiveCfg = Release|Any CPU
+ {804CF172-0C87-4423-9688-BD97D549891E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|x86.Build.0 = Debug|Any CPU
+ {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.ActiveCfg = Release|Any CPU
+ {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.Build.0 = Release|Any CPU
+ {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|x86.Build.0 = Debug|Any CPU
+ {7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|x86.ActiveCfg = Release|Any CPU
+ {7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|x86.Build.0 = Release|Any CPU
+ {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|x86.Build.0 = Debug|Any CPU
+ {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|Any CPU.Build.0 = Release|Any CPU
+ {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|x86.ActiveCfg = Release|Any CPU
+ {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|x86.Build.0 = Release|Any CPU
+ {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|x86.Build.0 = Debug|Any CPU
+ {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|x86.ActiveCfg = Release|Any CPU
+ {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|x86.Build.0 = Release|Any CPU
+ {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Debug|x86.Build.0 = Debug|Any CPU
+ {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|x86.ActiveCfg = Release|Any CPU
+ {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|x86.Build.0 = Release|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|x86.Build.0 = Debug|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|x86.ActiveCfg = Release|Any CPU
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|x86.Build.0 = Release|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|x86.Build.0 = Debug|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.ActiveCfg = Release|Any CPU
+ {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.Build.0 = Release|Any CPU
+ {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|x86.Build.0 = Debug|Any CPU
+ {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|x86.ActiveCfg = Release|Any CPU
+ {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|x86.Build.0 = Release|Any CPU
+ {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|x86.Build.0 = Debug|Any CPU
+ {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|x86.ActiveCfg = Release|Any CPU
+ {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|x86.Build.0 = Release|Any CPU
+ {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Debug|x86.Build.0 = Debug|Any CPU
+ {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Release|x86.ActiveCfg = Release|Any CPU
+ {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {06245255-E585-483C-A4AD-71B7B568C6C8}
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D} = {56DD9707-D811-4056-9E2C-8A9CC2479B07}
+ {804CF172-0C87-4423-9688-BD97D549891E} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}
+ {7C3EB599-2292-4532-B280-D5BED1094DD4} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}
+ {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}
+ {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124}
+ {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB} = {9F1919B0-915B-4749-9944-697DF7E7F67F}
+ {3DF86F12-7099-44F6-B98B-A148213D60B1} = {9F1919B0-915B-4749-9944-697DF7E7F67F}
+ {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99} = {56DD9707-D811-4056-9E2C-8A9CC2479B07}
+ {70F97BB7-12A1-4C7A-B2A5-B962D14B813E} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124}
+ EndGlobalSection
+EndGlobal