summaryrefslogtreecommitdiff
path: root/Impostor-dev
diff options
context:
space:
mode:
Diffstat (limited to 'Impostor-dev')
-rw-r--r--Impostor-dev/.config/dotnet-tools.json12
-rw-r--r--Impostor-dev/.dockerignore9
-rw-r--r--Impostor-dev/.github/ISSUE_TEMPLATE/1--bug-reporting.md35
-rw-r--r--Impostor-dev/.github/ISSUE_TEMPLATE/2--feature-request.md19
-rw-r--r--Impostor-dev/.github/ISSUE_TEMPLATE/3--api-suggestion.md20
-rw-r--r--Impostor-dev/.github/ISSUE_TEMPLATE/4--api-invalid-data.md25
-rw-r--r--Impostor-dev/.github/ISSUE_TEMPLATE/5--api-unavailable-data.md20
-rw-r--r--Impostor-dev/.github/ISSUE_TEMPLATE/6--api-other.md10
-rw-r--r--Impostor-dev/.github/ISSUE_TEMPLATE/config.yml1
-rw-r--r--Impostor-dev/.github/PULL_REQUEST_TEMPLATE.md16
-rw-r--r--Impostor-dev/.github/stale.yml17
-rw-r--r--Impostor-dev/.github/workflows/docker.yml70
-rw-r--r--Impostor-dev/.gitignore7
-rw-r--r--Impostor-dev/CONTRIBUTING.md22
-rw-r--r--Impostor-dev/Dockerfile39
-rw-r--r--Impostor-dev/LICENSE674
-rw-r--r--Impostor-dev/README.md72
-rw-r--r--Impostor-dev/appveyor.yml51
-rw-r--r--Impostor-dev/build.cake178
-rw-r--r--Impostor-dev/docs/Building-from-source.md45
-rw-r--r--Impostor-dev/docs/FAQ.md11
-rw-r--r--Impostor-dev/docs/README.md7
-rw-r--r--Impostor-dev/docs/Running-the-server.md101
-rw-r--r--Impostor-dev/docs/Server-configuration.md77
-rw-r--r--Impostor-dev/docs/TROUBLESHOOTING.md28
-rw-r--r--Impostor-dev/docs/Writing-a-plugin.md343
-rw-r--r--Impostor-dev/docs/images/client.jpgbin0 -> 51524 bytes
-rw-r--r--Impostor-dev/docs/images/logo_458.pngbin0 -> 64934 bytes
-rw-r--r--Impostor-dev/docs/images/logo_64.pngbin0 -> 7075 bytes
-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
365 files changed, 22996 insertions, 0 deletions
diff --git a/Impostor-dev/.config/dotnet-tools.json b/Impostor-dev/.config/dotnet-tools.json
new file mode 100644
index 0000000..727dfd7
--- /dev/null
+++ b/Impostor-dev/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "cake.tool": {
+ "version": "0.38.5",
+ "commands": [
+ "dotnet-cake"
+ ]
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/.dockerignore b/Impostor-dev/.dockerignore
new file mode 100644
index 0000000..9163c62
--- /dev/null
+++ b/Impostor-dev/.dockerignore
@@ -0,0 +1,9 @@
+.dockerignore
+.env
+.gitignore
+.vs
+.vscode
+**/.git
+**/.idea
+**/bin
+**/obj \ No newline at end of file
diff --git a/Impostor-dev/.github/ISSUE_TEMPLATE/1--bug-reporting.md b/Impostor-dev/.github/ISSUE_TEMPLATE/1--bug-reporting.md
new file mode 100644
index 0000000..cd4dab5
--- /dev/null
+++ b/Impostor-dev/.github/ISSUE_TEMPLATE/1--bug-reporting.md
@@ -0,0 +1,35 @@
+---
+name: 1. Bug reporting
+about: Bugs within Impostor
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+# Bug Report
+
+## Base Information
+- Operating System
+- Impostor Version
+- Among Us Version
+
+## I confirm:
+- [ ] that I have searched for an existing bug report for this issue.
+
+
+## Symptoms
+
+<!--
+Write symptoms here.
+-->
+
+Enter Symptoms on this line.
+
+## Reproduction
+
+<!--
+ How do you reproduce the bug you have here.
+-->
+
+Enter reproductions steps here.
diff --git a/Impostor-dev/.github/ISSUE_TEMPLATE/2--feature-request.md b/Impostor-dev/.github/ISSUE_TEMPLATE/2--feature-request.md
new file mode 100644
index 0000000..8aa42e6
--- /dev/null
+++ b/Impostor-dev/.github/ISSUE_TEMPLATE/2--feature-request.md
@@ -0,0 +1,19 @@
+---
+name: 2. Feature request
+about: To ask and request new features with Impostor
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+# Feature Request
+
+## Feature Information:
+<!--
+ - One issue per post! Do not try and bring up multiple requests in a single post.
+ - What should it do?
+-->
+
+## I confirm:
+- [ ] that I have searched for an existing feature request matching the description.
diff --git a/Impostor-dev/.github/ISSUE_TEMPLATE/3--api-suggestion.md b/Impostor-dev/.github/ISSUE_TEMPLATE/3--api-suggestion.md
new file mode 100644
index 0000000..bfed62f
--- /dev/null
+++ b/Impostor-dev/.github/ISSUE_TEMPLATE/3--api-suggestion.md
@@ -0,0 +1,20 @@
+---
+name: 3. Api suggestion
+about: To make suggestions for the plugin api
+title: ''
+labels: api
+assignees: ''
+
+---
+
+# Api Suggestion
+
+## Suggestion
+<!--
+Describe your suggestion as detailed as possible.
+-->
+
+## Use case
+<!--
+Describe what you need it for.
+-->
diff --git a/Impostor-dev/.github/ISSUE_TEMPLATE/4--api-invalid-data.md b/Impostor-dev/.github/ISSUE_TEMPLATE/4--api-invalid-data.md
new file mode 100644
index 0000000..6ebe139
--- /dev/null
+++ b/Impostor-dev/.github/ISSUE_TEMPLATE/4--api-invalid-data.md
@@ -0,0 +1,25 @@
+---
+name: 4. Api invalid data
+about: To let us know about invalid data in the api
+title: ''
+labels: api
+assignees: ''
+
+---
+
+# Api missing data
+
+## Data
+<!--
+Describe about the missing data that you need.
+-->
+
+## Expectations
+<!--
+Describe what the api gave you and what you expected.
+-->
+
+## Reproduce
+<!--
+Describe how to reproduce the issue.
+-->
diff --git a/Impostor-dev/.github/ISSUE_TEMPLATE/5--api-unavailable-data.md b/Impostor-dev/.github/ISSUE_TEMPLATE/5--api-unavailable-data.md
new file mode 100644
index 0000000..698c9ab
--- /dev/null
+++ b/Impostor-dev/.github/ISSUE_TEMPLATE/5--api-unavailable-data.md
@@ -0,0 +1,20 @@
+---
+name: 5. Api unavailable data
+about: To let us know about unavailable data from the api that you would like to use
+title: ''
+labels: api
+assignees: ''
+
+---
+
+# Api missing data
+
+## Data
+<!--
+Describe about the unavailable data that you need.
+-->
+
+## Use-case
+<!--
+Describe what you need it for.
+-->
diff --git a/Impostor-dev/.github/ISSUE_TEMPLATE/6--api-other.md b/Impostor-dev/.github/ISSUE_TEMPLATE/6--api-other.md
new file mode 100644
index 0000000..9077852
--- /dev/null
+++ b/Impostor-dev/.github/ISSUE_TEMPLATE/6--api-other.md
@@ -0,0 +1,10 @@
+---
+name: 6. Api other
+about: For anything about the api that does not fit in the other issues
+title: ''
+labels: api
+assignees: ''
+
+---
+
+
diff --git a/Impostor-dev/.github/ISSUE_TEMPLATE/config.yml b/Impostor-dev/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..0086358
--- /dev/null
+++ b/Impostor-dev/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: true
diff --git a/Impostor-dev/.github/PULL_REQUEST_TEMPLATE.md b/Impostor-dev/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..a877a10
--- /dev/null
+++ b/Impostor-dev/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,16 @@
+### Description
+
+
+<!--
+
+If your pull request closes any issues, add them below with the `closes` keyword before them
+
+Example: closes #101
+
+See the following article for more information: https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
+
+-->
+
+### Closes issues
+
+- closes #
diff --git a/Impostor-dev/.github/stale.yml b/Impostor-dev/.github/stale.yml
new file mode 100644
index 0000000..e556fa9
--- /dev/null
+++ b/Impostor-dev/.github/stale.yml
@@ -0,0 +1,17 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 60
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 7
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - pinned
+ - security
+# Label to use when marking an issue as stale
+staleLabel: stale
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your contributions.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/Impostor-dev/.github/workflows/docker.yml b/Impostor-dev/.github/workflows/docker.yml
new file mode 100644
index 0000000..b312075
--- /dev/null
+++ b/Impostor-dev/.github/workflows/docker.yml
@@ -0,0 +1,70 @@
+name: Docker
+
+on:
+ push:
+ branches: dev
+ paths:
+ - 'src/Impostor.Server/**'
+ - 'src/Impostor.Shared/**'
+ - '.gitmodules'
+ - '.github/workflows/docker.yml'
+ - 'Dockerfile'
+ tags:
+ - 'v*.*.*'
+ pull_request:
+ paths:
+ - 'src/Impostor.Server/**'
+ - 'src/Impostor.Shared/**'
+ - '.gitmodules'
+ - '.github/workflows/docker.yml'
+ - 'Dockerfile'
+
+jobs:
+ push_to_registry:
+ name: Push Docker image
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ submodules: true
+ - name: Prepare
+ id: prep
+ run: |
+ DOCKER_IMAGE=aeonlucid/impostor
+ VERSION=noop
+ if [[ $GITHUB_REF == refs/tags/* ]]; then
+ VERSION=${GITHUB_REF#refs/tags/}
+ elif [[ $GITHUB_REF == refs/heads/* ]]; then
+ VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g')
+ if [ "${{ github.event.repository.default_branch }}" = "dev" ]; then
+ VERSION=nightly
+ fi
+ elif [[ $GITHUB_REF == refs/pull/* ]]; then
+ VERSION=pr-${{ github.event.number }}
+ fi
+ TAGS="${DOCKER_IMAGE}:${VERSION}"
+ if [[ $VERSION =~ ^v[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
+ TAGS="$TAGS,${DOCKER_IMAGE}:latest"
+ fi
+ echo ::set-output name=version::${VERSION}
+ echo ::set-output name=tags::${TAGS}
+ echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v1
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v1
+ - name: Login to DockerHub
+ if: github.event_name != 'pull_request'
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Build and push
+ uses: docker/build-push-action@v2
+ with:
+ context: .
+ file: ./Dockerfile
+ platforms: linux/amd64,linux/arm/v7,linux/arm64
+ push: ${{ github.event_name != 'pull_request' }}
+ tags: ${{ steps.prep.outputs.tags }}
diff --git a/Impostor-dev/.gitignore b/Impostor-dev/.gitignore
new file mode 100644
index 0000000..1170bdb
--- /dev/null
+++ b/Impostor-dev/.gitignore
@@ -0,0 +1,7 @@
+/.vscode
+/build
+/.vs
+/src/Impostor.Plugins.Debugger/Properties/launchSettings.json
+/tools
+
+.local/
diff --git a/Impostor-dev/CONTRIBUTING.md b/Impostor-dev/CONTRIBUTING.md
new file mode 100644
index 0000000..16583a3
--- /dev/null
+++ b/Impostor-dev/CONTRIBUTING.md
@@ -0,0 +1,22 @@
+# Contribute guidelines
+
+We are always looking for people to help improve Impostor.
+
+## Code
+
+- If you are implementing or fixing an issue, please comment on the issue so work is not duplicated.
+- If you want to implementing a new feature, create an issue first describing the issue so we know about it. We are always open for discussion or questions on [Discord](https://discord.gg/Mk3w6Tb).
+- Don't commit unnecessary changes to the codebase or debugging code.
+- Write meaningful commits or squash them.
+- Please try to follow the code style of the rest of the codebase. An `.editorconfig` file has been provided to keep consistency.
+
+## Pull requests
+
+- Only make pull requests to the `dev` branch.
+- Only implement one feature per pull request to keep it easy to understand.
+- Expect comments or questions on your pull request from the project maintainers. We try to keep the code as consistent and maintainable as possible.
+- Each pull request should come from a new branch in your fork, it should have a meaningful name.
+- We try to respond to pull requests as fast as possible. If you think we might have missed it, let us know on [Discord](https://discord.gg/Mk3w6Tb).
+
+
+If you have any questions, let us know. \ No newline at end of file
diff --git a/Impostor-dev/Dockerfile b/Impostor-dev/Dockerfile
new file mode 100644
index 0000000..8f386da
--- /dev/null
+++ b/Impostor-dev/Dockerfile
@@ -0,0 +1,39 @@
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:5.0 AS build
+
+# See for all possible platforms
+# https://github.com/containerd/containerd/blob/master/platforms/platforms.go#L17
+ARG TARGETARCH
+
+WORKDIR /source
+
+# Copy csproj and restore.
+COPY src/Impostor.Server/Impostor.Server.csproj ./src/Impostor.Server/Impostor.Server.csproj
+COPY src/Impostor.Api/Impostor.Api.csproj ./src/Impostor.Api/Impostor.Api.csproj
+COPY src/Impostor.Hazel/Impostor.Hazel.csproj ./src/Impostor.Hazel/Impostor.Hazel.csproj
+
+RUN case "$TARGETARCH" in \
+ amd64) NETCORE_PLATFORM='linux-x64';; \
+ arm64) NETCORE_PLATFORM='linux-arm64';; \
+ arm) NETCORE_PLATFORM='linux-arm';; \
+ *) echo "unsupported architecture"; exit 1 ;; \
+ esac && \
+ dotnet restore -r "$NETCORE_PLATFORM" ./src/Impostor.Server/Impostor.Server.csproj && \
+ dotnet restore -r "$NETCORE_PLATFORM" ./src/Impostor.Api/Impostor.Api.csproj && \
+ dotnet restore -r "$NETCORE_PLATFORM" ./src/Impostor.Hazel/Impostor.Hazel.csproj
+
+# Copy everything else.
+COPY src/. ./src/
+RUN case "$TARGETARCH" in \
+ amd64) NETCORE_PLATFORM='linux-x64';; \
+ arm64) NETCORE_PLATFORM='linux-arm64';; \
+ arm) NETCORE_PLATFORM='linux-arm';; \
+ *) echo "unsupported architecture"; exit 1 ;; \
+ esac && \
+ dotnet publish -c release -o /app -r "$NETCORE_PLATFORM" --no-restore ./src/Impostor.Server/Impostor.Server.csproj
+
+# Final image.
+FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/runtime:5.0
+WORKDIR /app
+COPY --from=build /app ./
+EXPOSE 22023/udp
+ENTRYPOINT ["./Impostor.Server"] \ No newline at end of file
diff --git a/Impostor-dev/LICENSE b/Impostor-dev/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/Impostor-dev/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/Impostor-dev/README.md b/Impostor-dev/README.md
new file mode 100644
index 0000000..1ce3f94
--- /dev/null
+++ b/Impostor-dev/README.md
@@ -0,0 +1,72 @@
+# Impostor
+
+[![Discord](https://img.shields.io/badge/Discord-chat-blue?style=flat-square)](https://discord.gg/Mk3w6Tb)
+[![AppVeyor](https://img.shields.io/appveyor/build/Impostor/Impostor/dev?style=flat-square)](https://ci.appveyor.com/project/Impostor/Impostor/branch/dev)
+
+Impostor is one of the first **Among Us** private servers, written in C#.
+
+We support Steam, Itch, Android and iOS. The latest version supported is `2020.9.22`, the `dev` build currently supports `2020.11.17`.
+
+| Impostor version | Among Us version | Experimental | Download |
+|-|-|-|-|
+| 1.1.0 | 2020.09.07 - 2020.09.22 | No | [![Download](https://img.shields.io/badge/Download-v1.1.0-blue?style=flat-square)](https://github.com/Impostor/Impostor/releases/tag/v1.1.0) |
+| 1.2.2 | 2020.09.22 - 2020.11.17 | Yes | [![Download](https://img.shields.io/badge/Download-v1.2.2-blue?style=flat-square)](https://ci.appveyor.com/project/Impostor/Impostor/branch/dev/artifacts) |
+
+## Features
+
+- All Among Us features are implemented. It is a full replacement for the official server.
+- Plugin support.
+- Server-sided anticheat.
+
+## Installation
+
+### Client
+
+If you just want to play on a server hosted by someone else, you need to follow these instructions.
+
+#### Windows
+
+1. Find the [latest release](https://github.com/AeonLucid/Impostor/releases/latest).
+2. Download `Impostor-Client-win-x64.zip`.
+3. Extract the zip.
+4. Run `Impostor.Client.exe`.
+5. Follow the instructions inside the application.
+
+![Client](docs/images/client.jpg)
+
+If you do not wish to execute any programs. Follow the instructions in [this website](https://impostor.github.io/Impostor)
+
+#### Android
+
+##### Android 10 and below.
+1. Go to [this website](https://impostor.github.io/Impostor) **(ON YOUR MOBILE DEVICE)**
+2. Follow the instructions listed there.
+
+##### Android 11.
+1. Connect your phone to a computer. Go to [this website](https://impostor.github.io/Impostor) on the computer and follow the steps 1 and 2 to generate a `regionInfo.dat` file.
+2. Instead of following the next steps, open the phone's internal storage on your computer and navigate to `/sdcard/Android/data/com.innersloth.spacemafia/files`.
+3. Copy the generated `regionInfo.dat` file into the `files` folder you just navigated to.
+
+#### iOS
+
+iOS devices need to be jailbroken in order to connect to Impostor servers.
+
+### Server
+
+See the [docs](docs/Running-the-server.md) for instructions on how to set it up.
+
+## Troubleshooting
+
+See [TROUBLESHOOTING](docs/TROUBLESHOOTING.md) to solve issues the Impostor client or the server.
+
+## Contributing
+
+See [CONTRIBUTING](CONTRIBUTING.md).
+
+## License
+
+This software is distributed under the **GNU GPLv3** License.
+
+## Credits
+
+- [willardf/Hazel-Networking](https://github.com/willardf/Hazel-Networking)
diff --git a/Impostor-dev/appveyor.yml b/Impostor-dev/appveyor.yml
new file mode 100644
index 0000000..e0c82f6
--- /dev/null
+++ b/Impostor-dev/appveyor.yml
@@ -0,0 +1,51 @@
+version: '{build}'
+
+environment:
+ IMPOSTOR_VERSION: '1.2.2'
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+
+branches:
+ except:
+ - gh-pages
+
+pull_requests:
+ do_not_increment_build_number: true
+
+assembly_info:
+ patch: false
+
+dotnet_csproj:
+ patch: false
+
+image: Visual Studio 2019 Preview
+
+install:
+ - git submodule update --init --recursive
+ - ps: dotnet tool restore
+
+build_script:
+ - ps: dotnet cake build.cake --bootstrap
+ - ps: dotnet cake build.cake --pack
+
+test: off
+
+artifacts:
+ - path: ./build/*.zip
+ - path: ./build/*.tar.gz
+ - path: ./build/*.nupkg
+ - path: ./build/*.snupkg
+
+# deploy:
+# - provider: NuGet
+# artifact: /.*(\.|\.s)nupkg/
+# on:
+# branch: dev
+# api_key:
+# secure: 4OHXl+m1KVi60hB1j54MpdP8tVv0UNfkdF4rVP+2AhAjN4a2MVT+Bl90gJZ96xHF
+
+only_commits:
+ files:
+ - appveyor.yml
+ - build.cake
+ - src/**/*
+ - .gitmodules
diff --git a/Impostor-dev/build.cake b/Impostor-dev/build.cake
new file mode 100644
index 0000000..7b28698
--- /dev/null
+++ b/Impostor-dev/build.cake
@@ -0,0 +1,178 @@
+#addin "nuget:?package=SharpZipLib&Version=1.3.0"
+#addin "nuget:?package=Cake.Compression&Version=0.2.4"
+#addin "nuget:?package=Cake.FileHelpers&Version=3.3.0"
+
+
+var buildId = EnvironmentVariable("APPVEYOR_BUILD_VERSION") ?? "0";
+var buildVersion = EnvironmentVariable("IMPOSTOR_VERSION") ?? "1.0.0";
+var buildBranch = EnvironmentVariable("APPVEYOR_REPO_BRANCH") ?? "dev";
+var buildDir = MakeAbsolute(Directory("./build"));
+
+var prNumber = EnvironmentVariable("APPVEYOR_PULL_REQUEST_NUMBER");
+var target = Argument("target", "Deploy");
+var configuration = Argument("configuration", "Release");
+
+// On any branch that is not master, we need to tag the version as prerelease.
+if (buildBranch != "master") {
+ buildVersion = buildVersion + "-ci." + buildId;
+}
+
+//////////////////////////////////////////////////////////////////////
+// UTILS
+//////////////////////////////////////////////////////////////////////
+
+// Remove unnecessary files for packaging.
+private void ImpostorPublish(string name, string project, string runtime, bool isServer = false) {
+ var projBuildDir = buildDir.Combine(name + "_" + runtime);
+ var projBuildName = name + "_" + buildVersion + "_" + runtime;
+
+ DotNetCorePublish(project, new DotNetCorePublishSettings {
+ Configuration = configuration,
+ NoRestore = true,
+ Framework = "net5.0",
+ Runtime = runtime,
+ SelfContained = false,
+ PublishSingleFile = true,
+ PublishTrimmed = false,
+ OutputDirectory = projBuildDir
+ });
+
+ if (isServer) {
+ CreateDirectory(projBuildDir.Combine("plugins"));
+ CreateDirectory(projBuildDir.Combine("libraries"));
+
+ if (runtime == "win-x64") {
+ FileWriteText(projBuildDir.CombineWithFilePath("run.bat"), "@echo off\r\nImpostor.Server.exe\r\npause");
+ }
+ }
+
+ if (runtime == "win-x64") {
+ Zip(projBuildDir, buildDir.CombineWithFilePath(projBuildName + ".zip"));
+ } else {
+ GZipCompress(projBuildDir, buildDir.CombineWithFilePath(projBuildName + ".tar.gz"));
+ }
+}
+
+private void ImpostorPublishNF(string name, string project) {
+ var runtime = "win-x64";
+ var projBuildDir = buildDir.Combine(name + "_" + runtime);
+ var projBuildZip = buildDir.CombineWithFilePath(name + "_" + buildVersion + "_" + runtime + ".zip");
+
+ DotNetCorePublish(project, new DotNetCorePublishSettings {
+ Configuration = configuration,
+ NoRestore = true,
+ Framework = "net472",
+ OutputDirectory = projBuildDir
+ });
+
+ Zip(projBuildDir, projBuildZip);
+}
+
+//////////////////////////////////////////////////////////////////////
+// TASKS
+//////////////////////////////////////////////////////////////////////
+
+Task("Clean")
+ .Does(() => {
+ if (DirectoryExists(buildDir)) {
+ DeleteDirectory(buildDir, new DeleteDirectorySettings {
+ Recursive = true
+ });
+ }
+ });
+
+Task("Restore")
+ .Does(() => {
+ DotNetCoreRestore("./src/Impostor.sln");
+ });
+
+Task("Patch")
+ .WithCriteria(BuildSystem.AppVeyor.IsRunningOnAppVeyor)
+ .Does(() => {
+ ReplaceRegexInFiles("./src/**/*.csproj", @"<Version>.*?<\/Version>", "<Version>" + buildVersion + "</Version>");
+ ReplaceRegexInFiles("./src/**/*.props", @"<Version>.*?<\/Version>", "<Version>" + buildVersion + "</Version>");
+ });
+
+Task("Replay")
+ .Does(() => {
+ // D:\Projects\GitHub\Impostor\Impostor\src\Impostor.Tools.ServerReplay\sessions
+ DotNetCoreRun(
+ "./src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj",
+ "./src/Impostor.Tools.ServerReplay/sessions", new DotNetCoreRunSettings {
+ Configuration = configuration,
+ NoRestore = true,
+ Framework = "net5.0"
+ });
+ });
+
+Task("Build")
+ .IsDependentOn("Clean")
+ .IsDependentOn("Patch")
+ .IsDependentOn("Restore")
+ .IsDependentOn("Replay")
+ .Does(() => {
+ // Tests.
+ DotNetCoreBuild("./src/Impostor.Tests/Impostor.Tests.csproj", new DotNetCoreBuildSettings {
+ Configuration = configuration,
+ });
+
+ // Only build artifacts if;
+ // - buildBranch is master/dev
+ // - it is not a pull request
+ if ((buildBranch == "master" || buildBranch == "dev") && string.IsNullOrEmpty(prNumber)) {
+ // Client.
+ ImpostorPublishNF("Impostor-Patcher", "./src/Impostor.Patcher/Impostor.Patcher.WinForms/Impostor.Patcher.WinForms.csproj");
+
+ ImpostorPublish("Impostor-Patcher-Cli", "./src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj", "win-x64");
+ ImpostorPublish("Impostor-Patcher-Cli", "./src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj", "osx-x64");
+ ImpostorPublish("Impostor-Patcher-Cli", "./src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj", "linux-x64");
+
+ // Server.
+ ImpostorPublish("Impostor-Server", "./src/Impostor.Server/Impostor.Server.csproj", "win-x64", true);
+ ImpostorPublish("Impostor-Server", "./src/Impostor.Server/Impostor.Server.csproj", "osx-x64", true);
+ ImpostorPublish("Impostor-Server", "./src/Impostor.Server/Impostor.Server.csproj", "linux-x64", true);
+ ImpostorPublish("Impostor-Server", "./src/Impostor.Server/Impostor.Server.csproj", "linux-arm", true);
+ ImpostorPublish("Impostor-Server", "./src/Impostor.Server/Impostor.Server.csproj", "linux-arm64", true);
+
+ // API.
+ DotNetCorePack("./src/Impostor.Api/Impostor.Api.csproj", new DotNetCorePackSettings {
+ Configuration = configuration,
+ OutputDirectory = buildDir,
+ IncludeSource = true,
+ IncludeSymbols = true
+ });
+ } else {
+ DotNetCoreBuild("./src/Impostor.Patcher/Impostor.Patcher.WinForms/Impostor.Patcher.WinForms.csproj", new DotNetCoreBuildSettings {
+ Configuration = configuration,
+ NoRestore = true,
+ Framework = "net472"
+ });
+
+ DotNetCoreBuild("./src/Impostor.Server/Impostor.Server.csproj", new DotNetCoreBuildSettings {
+ Configuration = configuration,
+ NoRestore = true,
+ Framework = "net5.0"
+ });
+ }
+ });
+
+Task("Test")
+ .IsDependentOn("Build")
+ .Does(() => {
+ DotNetCoreTest("./src/Impostor.Tests/Impostor.Tests.csproj", new DotNetCoreTestSettings {
+ Configuration = configuration,
+ NoBuild = true
+ });
+ });
+
+Task("Deploy")
+ .IsDependentOn("Test")
+ .Does(() => {
+ Information("Finished.");
+ });
+
+//////////////////////////////////////////////////////////////////////
+// EXECUTION
+//////////////////////////////////////////////////////////////////////
+
+RunTarget(target); \ No newline at end of file
diff --git a/Impostor-dev/docs/Building-from-source.md b/Impostor-dev/docs/Building-from-source.md
new file mode 100644
index 0000000..cf3632f
--- /dev/null
+++ b/Impostor-dev/docs/Building-from-source.md
@@ -0,0 +1,45 @@
+# Building from source
+
+The solution contains two main projects, the Impostor client and server. The client is built using [.NET Framework 4.7.2](https://dotnet.microsoft.com/download/dotnet-framework/net472) and the server with [.NET 5](https://dotnet.microsoft.com/download/dotnet/5.0).
+
+Currently .NET 5 is not yet officially released, so in order to build using Visual Studio, you should have Visual Studio 2019 **Preview** installed.
+This documentation will go over building both the [Server](#building-the-server) and the [Client](#building-the-client) and their requirements.
+
+## Cloning Impostor
+
+You need to clone Impostor with all submodules.
+
+```bash
+git clone --recursive https://github.com/AeonLucid/Impostor.git
+```
+
+If you already have cloned Impostor but have errors related to Hazel, run the following.
+
+```bash
+git submodule update --init
+```
+
+## Building the server
+
+### Dependencies
+- [.NET 5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)
+- [Visual Studio Preview](https://visualstudio.microsoft.com/vs/preview/) (Optional, only if you want the full IDE experience)
+
+### Build using the CLI
+
+```bash
+cd src/Impostor.Server/
+dotnet build
+```
+To setup the server, please look at [Running the server](Running-the-server.md).
+
+## Building the client
+
+### Dependencies
+* [.NET Framework 4.7.2 Developer Pack](https://dotnet.microsoft.com/download/dotnet-framework/thank-you/net472-developer-pack-offline-installer)
+
+### Build using the CLI
+```bash
+cd src/Impostor.Client/Impostor.Client.WinForms
+dotnet build
+``` \ No newline at end of file
diff --git a/Impostor-dev/docs/FAQ.md b/Impostor-dev/docs/FAQ.md
new file mode 100644
index 0000000..14df95f
--- /dev/null
+++ b/Impostor-dev/docs/FAQ.md
@@ -0,0 +1,11 @@
+# Frequently Answered Questions
+
+## What is this?
+The Impostor project is a reverse engineered and open sourced server for the game Among Us. The game itself is developed by [InnerSloth](http://www.innersloth.com/) while this project is maintained by the community. This project was built out of frustration for the lack of server availability in certain regions and the inability for the core developer, [AeonLucid](https://github.com/AeonLucid), to join a public game. As of this time, it has not been officially endorsed by the studio.
+
+## Can this be used with the mobile version of the game?
+Yes, Impostor can be used with both the Android and iOS\* versions of the game.
+###### \* In order to play on an Impostor server with iOS, you _must_ have a jailbroken device.
+
+## How can I get started?
+See [Setting up your Server](Running-the-server.md) for more information on running the server and [Client Setup](https://impostor.github.io/Impostor/) for helping your friends join in!
diff --git a/Impostor-dev/docs/README.md b/Impostor-dev/docs/README.md
new file mode 100644
index 0000000..e32ff8c
--- /dev/null
+++ b/Impostor-dev/docs/README.md
@@ -0,0 +1,7 @@
+# Impostor Documentation
+
+1. [Running the server](Running-the-server.md)
+2. [Server configuration](Server-configuration.md)
+3. [Building from source](Building-from-source.md)
+4. [Writing a plugin](Writing-a-plugin.md)
+5. [Frequently answered questions](FAQ.md) \ No newline at end of file
diff --git a/Impostor-dev/docs/Running-the-server.md b/Impostor-dev/docs/Running-the-server.md
new file mode 100644
index 0000000..db0a618
--- /dev/null
+++ b/Impostor-dev/docs/Running-the-server.md
@@ -0,0 +1,101 @@
+# Running the server
+
+There are currently two modes to run the Impostor server in. The first way is the simplest one and is the one you should probably use. The other way will distribute players across other servers and is a more advanced configuration.
+
+## Single server
+
+### Without docker
+1. Install the **.NET 5.0 runtime**.
+ - [Windows x64](https://dotnet.microsoft.com/download/dotnet/thank-you/runtime-5.0.0-windows-x64-installer)
+ - [Linux x64](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
+ - [macOS x64](https://dotnet.microsoft.com/download/dotnet/thank-you/runtime-5.0.0-macos-x64-installer)
+2. Find the [latest dev release](https://ci.appveyor.com/project/Impostor/Impostor/branch/dev/artifacts).
+3. Download either the Windows or the Linux version.
+4. Extract the zip.
+5. Modify `config.json` to your liking. Documentation can be found [here](Server-configuration.md) *(this step is mandatory if you want to expose this server to other devices)*
+6. Run `Impostor.Server.exe` (Windows) / `Impostor.Server` (Linux)
+
+### Using docker
+
+[![Docker Image](https://img.shields.io/docker/v/aeonlucid/impostor?sort=semver)](https://hub.docker.com/repository/docker/aeonlucid/impostor)
+[![Docker Image](https://img.shields.io/docker/v/aeonlucid/impostor/nightly)](https://hub.docker.com/repository/docker/aeonlucid/impostor)
+
+```
+docker run -p 22023:22023/udp aeonlucid/impostor:nightly
+```
+
+### Using docker-compose
+```
+version: '3.4'
+
+services:
+ impostor:
+ image: aeonlucid/impostor:nightly
+ container_name: impostor
+ ports:
+ - 22023:22023/udp
+ volumes:
+ - /path/to/local/config.json:/app/config.json # For easy editing of the config
+ - /path/to/local/plugins:/app/plugins # Only needed if using plugins
+ - /path/to/local/libraries:/app/libraries # Only needed if using external libraries
+```
+
+## Multiple servers
+
+Follow the steps from the single server on two or more servers.
+
+### Master server
+
+The master server will accept client connections and redirect them to the other servers listed in the configuration. It will not host any games itself.
+
+Example configuration:
+
+```json
+{
+ "Server": {
+ "PublicIp": "127.0.0.1",
+ "PublicPort": 22023,
+ "ListenIp": "0.0.0.0",
+ "ListenPort": 22023
+ },
+ "ServerRedirector": {
+ "Enabled": true,
+ "Master": true,
+ "Locator": {
+ "Redis": "",
+ "UdpMasterEndpoint": "127.0.0.1:22024"
+ },
+ "Nodes": [
+ {
+ "Ip": "127.0.0.1",
+ "Port": 22025
+ }
+ ]
+ }
+}
+```
+
+### Node servers
+
+The node server should have `ServerRedirector` enabled too, but `Master` **must be disabled**. Nodes do not need to be aware of each other.
+
+Example configuration:
+
+```json
+{
+ "Server": {
+ "PublicIp": "127.0.0.1",
+ "PublicPort": 22025,
+ "ListenIp": "0.0.0.0",
+ "ListenPort": 22025
+ },
+ "ServerRedirector": {
+ "Enabled": true,
+ "Master": false,
+ "Locator": {
+ "Redis": "",
+ "UdpMasterEndpoint": "127.0.0.1:22024"
+ }
+ }
+}
+```
diff --git a/Impostor-dev/docs/Server-configuration.md b/Impostor-dev/docs/Server-configuration.md
new file mode 100644
index 0000000..5841ca7
--- /dev/null
+++ b/Impostor-dev/docs/Server-configuration.md
@@ -0,0 +1,77 @@
+# Server configuration
+
+Some information about all the possible configurations. Click [here](https://github.com/AeonLucid/Impostor/blob/master/src/Impostor.Server/config.full.json) to see all the possible config options.
+
+## Options
+
+### Required Server Configuration
+
+| Key | Default | Description |
+|-|-|-|
+| **PublicIp** | `127.0.0.1` | This needs to the public IPv4 address of the server which you give to others to connect. You can find your IPv4 address [on this website](http://whatismyip.host/). Unless you are only planning to use Impostor privately, on your local network, you should change this to your public ip. It is also possible to use hostnames instead of IPv4 addresses, which will be resolved to IPv4 addresses. |
+| **PublicPort** | `22023` | The public port of the server which you give to others to connect. (**This is the external port you configure on your router when port forwarding.**) Usually `22023`. |
+| **ListenIp** | `0.0.0.0` | The network interface to listen on. If you do not know what to put here, use `0.0.0.0`. Since 1.2.2 it is also possible to use hostnames instead of IPv4 addresses, these must resolve to a valid IPv4 address. |
+| **ListenPort** | `22023` | The listen port of the server, usually `22023`. |
+
+### AntiCheat
+
+| Key | Default | Value |
+|-|-|-|
+| **BanIpFromGame** | `true` | When a player is caught hacking, they will be kicked from the server. If this value is set to `true`, the player will be banned instead and will not be able to rejoin that specific game. **(Setting this to false does not disable the anti-cheat!)** |
+
+### ServerRedirector
+In a multi-node setup these need to be specified.
+| Key | Default | Value |
+|-|-|-|
+| **Enabled** | `false` | Whether the server runs in multi-node setup. If this is `false`, all other options in this section do not have any effect. |
+| **Master** | `false` | Whether the current server is a master. A master is responsible for redirecting clients to nodes |
+| **Locator** | | Fill in either `Redis` or `UdpMasterEndpoint` to choose which method to use for locating other nodes. This must be the same across all servers. |
+| **>Redis** | | Format `127.0.0.1.6379`, you can also use a password like so: `127.0.0.1.6379,password=value`. |
+| **>UdpMasterEndpoint** | | On the master, this value acts as a listen ip and port. On a node, this should be the public ip and port of the master. Format `127.0.0.1:32320`. |
+| **Nodes** | | An array containing public ips and ports of nodes. Only needs to be set on the master. See above for an example. |
+
+## Config providers
+
+### File
+
+The simplest option to configure is by using the `config.json` file next to the server executable. For all possible options see the [config-full.json](https://github.com/Impostor/Impostor/blob/dev/src/Impostor.Server/config-full.json) file.
+
+### Command line arguments
+
+TODO
+
+```
+Server:PublicIp=127.0.0.1
+Server:PublicPort=22023
+Server:ListenIp=0.0.0.0
+Server:ListenPort=22023
+AntiCheat:BanIpFromGame=true
+ServerRedirector:Enabled=false
+ServerRedirector:Master=true
+ServerRedirector:Locator:Redis=127.0.0.1.6379
+ServerRedirector:Locator:UdpMasterEndpoint=127.0.0.1:32320
+ServerRedirector:Nodes:0:Ip=127.0.0.1
+ServerRedirector:Nodes:0:Port=22024
+ServerRedirector:Nodes:1:Ip=127.0.0.1
+ServerRedirector:Nodes:1:Port=22025
+```
+
+### Environment variables
+
+TODO
+
+```
+IMPOSTOR_Server__PublicIp=127.0.0.1
+IMPOSTOR_Server__PublicPort=22023
+IMPOSTOR_Server__ListenIp=0.0.0.0
+IMPOSTOR_Server__ListenPort=22023
+IMPOSTOR_AntiCheat__BanIpFromGame=true
+IMPOSTOR_ServerRedirector__Enabled=false
+IMPOSTOR_ServerRedirector__Master=true
+IMPOSTOR_ServerRedirector__Locator__Redis=127.0.0.1.6379
+IMPOSTOR_ServerRedirector__Locator__UdpMasterEndpoint=127.0.0.1:32320
+IMPOSTOR_ServerRedirector__Nodes__0__Ip=127.0.0.1
+IMPOSTOR_ServerRedirector__Nodes__0__Port=22024
+IMPOSTOR_ServerRedirector__Nodes__1__Ip=127.0.0.1
+IMPOSTOR_ServerRedirector__Nodes__1__Port=22025
+```
diff --git a/Impostor-dev/docs/TROUBLESHOOTING.md b/Impostor-dev/docs/TROUBLESHOOTING.md
new file mode 100644
index 0000000..8750159
--- /dev/null
+++ b/Impostor-dev/docs/TROUBLESHOOTING.md
@@ -0,0 +1,28 @@
+# Troubleshooting
+If you're reading this, something went wrong.
+Don't worry though, as this is the most thorough guide to help you!
+
+## `./Impostor.Server: line 1: ELF: not found` (plus other errors)
+No idea where you got that system. But we clearly do **NOT** support it.
+
+## `cannot execute binary file: Exec format error`
+Please check that you have downloaded the right version of Impostor, as we mantain two CPU architectures (x64 and ARM).
+Unless you are running Impostor on a SBC (Single-Board Computer), like the Raspberry Pi, you most likely want to use the x64 version.
+
+## `./Impostor.Server: Permission denied`
+This is an error related to Linux file permissions.
+Some files do not hold their executable bit (the permission that allows them to run) during a download.
+You can solve this by doing: `chmod +x Impostor.Server`
+
+## `You are using an older version of the game`
+You are using an older version of Impostor. The game does not really check who is outdated and blames it on the user.
+Make sure you got the latest working version of Impostor (probably in AppVeyor, not Github).
+
+## `You disconnected from the server. Reliable Packet 1 ...`
+Please double-check that you have followed the [Server Configuration](Server-configuration.md) correctly.
+**NOTE: Your public ip does not start with `127` nor `192`**
+Also check if the port Impostor (ListenPort) is listening on is correctly port-forwarded for UDP (or TCP/UDP).
+
+## `Could not load file or assembly...`
+Please check that you only have **working** plugins in the `plugins` folder.
+This error can be caused by non-plugin files or plugins that are not working correctly.
diff --git a/Impostor-dev/docs/Writing-a-plugin.md b/Impostor-dev/docs/Writing-a-plugin.md
new file mode 100644
index 0000000..a7ad21b
--- /dev/null
+++ b/Impostor-dev/docs/Writing-a-plugin.md
@@ -0,0 +1,343 @@
+# Writing a plugin
+
+Impostor has support for plugins. This document will help you to setup a development environment for writing a plugin.
+
+- [1. Install .NET Core SDK](#1-install-net-core-sdk)
+- [2. Create a C# project](#2-create-a-c-project)
+- [3. Add the Impostor.Api library](#3-add-the-impostorapi-library)
+ - [Quick](#quick)
+ - [Visual Studio](#visual-studio)
+- [4. The plugin class](#4-the-plugin-class)
+- [5. Adding an event listener](#5-adding-an-event-listener)
+- [6. Registering the event listener](#6-registering-the-event-listener)
+- [7. Build and run your plugin](#7-build-and-run-your-plugin)
+- [8. Extra](#8-extra)
+ - [Event listeners](#event-listeners)
+ - [Dependency injection](#dependency-injection)
+ - [Server configuration](#server-configuration)
+ - [Using other libraries](#using-other-libraries)
+ - [Impostor versions](#impostor-versions)
+- [9. Missing/invalid data or want more functions?](#9-missinginvalid-data-or-want-more-functions)
+
+## 1. Install .NET Core SDK
+
+Download and install the latest .NET Core SDK.
+
+https://dotnet.microsoft.com/download
+
+## 2. Create a C# project
+
+The first step is creating a new C# project, it must be a **Class Library (.NET Standard)**. The target framework can be any of those compatible with .NET 5, which includes:
+
+- .NET Standard 2.0
+- .NET Standard 2.1
+- .NET Core 3.1
+- .NET 5
+
+For more information about compatibility, see https://docs.microsoft.com/en-us/dotnet/standard/net-standard.
+
+> At the moment of writing this document, I recommend you to use **.NET Standard 2.1** until .NET 5 is released officially. This should give you enough functionality. If not, upgrade to .NET Core 3.1.
+
+When the project has been created, you should have `Class.cs` and `Project.csproj` files. Your `Project.csproj` should look something like this.
+
+```xml
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>netstandard2.1</TargetFramework>
+ </PropertyGroup>
+</Project>
+```
+
+## 3. Add the Impostor.Api library
+
+You only have to follow the instructions of one below.
+
+### Quick
+
+Install the `Impostor.Api` NuGet package.
+Make sure to get a prerelease if you are writing a plugin for a dev release of the server.
+
+### Visual Studio
+
+1. Right click your project.
+2. Click `Manage NuGet Packages`.
+3. Click `Browse`.
+4. Next to the search bar, enable `Include prerelease`.
+5. Search for `Impostor.Api`.
+6. Click the `Impostor.Api` result and press install on the right side.
+
+### Dotnet CLI
+
+> Make sure to grab the latest (pre-)release version from NuGet [here](https://www.nuget.org/packages/Impostor.Api).
+
+1. Open your project folder in command prompt / bash.
+2. Run `dotnet add package Impostor.Api -v "1.2.0-ci.58"`.
+
+## 4. The plugin class
+
+Now the `Impostor.Api` is installed, you need to create a class for your plugin. A plugin **must** contain exactly one. See the code below for an example.
+
+```csharp
+using System.Threading.Tasks;
+using Impostor.Api.Events.Managers;
+using Impostor.Api.Plugins;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Plugins.Example
+{
+ /// <summary>
+ /// The metadata information of your plugin, this is required.
+ /// </summary>
+ [ImpostorPlugin(
+ package: "gg.impostor.example",
+ name: "Example",
+ author: "AeonLucid",
+ version: "1.0.0")]
+ public class ExamplePlugin : PluginBase // This is also required ": PluginBase".
+ {
+ /// <summary>
+ /// A logger that works seamlessly with the server.
+ /// </summary>
+ private readonly ILogger<ExamplePlugin> _logger;
+
+ /// <summary>
+ /// The constructor of the plugin. There are a few parameters you can add here and they
+ /// will be injected automatically by the server, two examples are used here.
+ ///
+ /// They are not necessary but very recommended.
+ /// </summary>
+ /// <param name="logger">
+ /// A logger to write messages in the console.
+ /// </param>
+ /// <param name="eventManager">
+ /// An event manager to register event listeners.
+ /// Useful if you want your plugin to interact with the game.
+ /// </param>
+ public ExamplePlugin(ILogger<ExamplePlugin> logger, IEventManager eventManager)
+ {
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// This is called when your plugin is enabled by the server.
+ /// </summary>
+ /// <returns></returns>
+ public override ValueTask EnableAsync()
+ {
+ _logger.LogInformation("Example is being enabled.");
+ return default;
+ }
+
+ /// <summary>
+ /// This is called when your plugin is disabled by the server.
+ /// Most likely because it is shutting down, this is the place to clean up any managed resources.
+ /// </summary>
+ /// <returns></returns>
+ public override ValueTask DisableAsync()
+ {
+ _logger.LogInformation("Example is being disabled.");
+ return default;
+ }
+ }
+}
+```
+
+## 5. Adding an event listener
+
+Currently you should have a plugin that loads and does nothing. In order to get some actual functionality, you need to add an event listener.
+
+Create a new class called `GameEventListener`. Example code:
+
+```csharp
+using Impostor.Api.Events;
+using Impostor.Api.Events.Player;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Plugins.Example.Handlers
+{
+ /// <summary>
+ /// A class that listens for two events.
+ /// It may be more but this is just an example.
+ ///
+ /// Make sure your class implements <see cref="IEventListener"/>.
+ /// </summary>
+ public class GameEventListener : IEventListener
+ {
+ private readonly ILogger<ExamplePlugin> _logger;
+
+ public GameEventListener(ILogger<ExamplePlugin> logger)
+ {
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// An example event listener.
+ /// </summary>
+ /// <param name="e">
+ /// The event you want to listen for.
+ /// </param>
+ [EventListener]
+ public void OnGameStarted(IGameStartedEvent e)
+ {
+ _logger.LogInformation($"Game is starting.");
+
+ // This prints out for all players if they are impostor or crewmate.
+ foreach (var player in e.Game.Players)
+ {
+ var info = player.Character.PlayerInfo;
+ var isImpostor = info.IsImpostor;
+ if (isImpostor)
+ {
+ _logger.LogInformation($"- {info.PlayerName} is an impostor.");
+ }
+ else
+ {
+ _logger.LogInformation($"- {info.PlayerName} is a crewmate.");
+ }
+ }
+ }
+
+ [EventListener]
+ public void OnGameEnded(IGameEndedEvent e)
+ {
+ _logger.LogInformation($"Game has ended.");
+ }
+
+ [EventListener]
+ public void OnPlayerChat(IPlayerChatEvent e)
+ {
+ _logger.LogInformation($"{e.PlayerControl.PlayerInfo.PlayerName} said {e.Message}");
+ }
+ }
+}
+```
+
+## 6. Registering the event listener
+
+The last step to get your plugin working is to register the event listener, so the server knows about it. Go back to your plugin class and modify it as below.
+
+```csharp
+using System;
+using System.Threading.Tasks;
+using Impostor.Api.Events.Managers;
+using Impostor.Api.Plugins;
+using Impostor.Plugins.Example.Handlers;
+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;
+ // Add the line below!
+ private readonly IEventManager _eventManager;
+ // Add the line below!
+ private IDisposable _unregister;
+
+ public ExamplePlugin(ILogger<ExamplePlugin> logger, IEventManager eventManager)
+ {
+ _logger = logger;
+ // Add the line below!
+ _eventManager = eventManager;
+ }
+
+ public override ValueTask EnableAsync()
+ {
+ _logger.LogInformation("Example is being enabled.");
+ // Add the line below!
+ _unregister = _eventManager.RegisterListener(new GameEventListener(_logger));
+ return default;
+ }
+
+ public override ValueTask DisableAsync()
+ {
+ _logger.LogInformation("Example is being disabled.");
+ // Add the line below!
+ _unregister.Dispose();
+ return default;
+ }
+ }
+}
+
+```
+
+## 7. Build and run your plugin
+
+Now your plugin is ready to be tested.
+
+1. Right click your project and press `Build`.
+2. Right click your project and press `Open Folder in File Explorer`.
+3. Go to `bin/Debug/netstandard2.1/`.
+4. In this directory, you should find your plugin named `Project.dll`.
+5. Copy the `Project.dll` to the `plugins` directory in your Impostor server directory.
+6. (Re)start your Impostor server.
+7. Open Among Us, create a game and send a chat message. In the console you should see your plugin being loaded and the messages from the example.
+
+## 8. Extra
+
+Some extra information that might be useful for those developing plugins.
+
+### Event listeners
+
+- You can have multiple event listener on the same event.
+- An event listener can be given a priority `[EventListener(EventPriority.Normal)]` and is called in order.
+- It is not recommended to block for a long time inside `EventListener` because the events are called from inside the packet handlers. Blocking too long causes the client to time out. You should create a new `Task` for operations that will take a lot of time.
+
+### Dependency injection
+
+- The main plugin class is constructed by the `IServiceProvider` of the server and can inject everything the server uses. A few examples are:
+ - `ILogger<T>`
+ - `IEventManager`
+ - `IClientManager`
+ - `IOptions<ServerConfig>`
+ - `IOptions<ServerRedirectorConfig>`
+- You can add your own classes and `EventListener` implementation to the `IServiceProvider` by creating a new class and implementing `IPluginStartup`. Make sure to register them as a singleton `services.AddSingleton<IEventListener, GameEventListener>();`.
+
+### Server configuration
+
+Constantly copying the plugin dll to your server directory can be pretty annoying. Luckily we have a solution for that. In your Impostor server open the `config.json` and add the `PluginLoader` from the example below, replace the path with the build destination of your plugin.
+
+```json
+{
+ "Server": {
+ "PublicIp": "127.0.0.1",
+ "PublicPort": 22023,
+ "ListenIp": "0.0.0.0",
+ "ListenPort": 22023
+ },
+ "PluginLoader": {
+ "Paths": [
+ "D:\\Projects\\Impostor\\src\\Impostor.Plugins.Example\\bin\\Debug\\netstandard2.1"
+ ],
+ "Libraries": []
+ }
+}
+```
+
+### Using other libraries
+
+Sometimes you need to use libraries that the original Impostor server does not provide. The dll files of these libraries must be placed in the `libraries` folder next to the server executable. You could also provide them by modifying the `PluginLoader.Libraries` option in the `config.json`, similarly to the `PluginLoader.Paths` option.
+
+### Impostor versions
+
+It is important to use the correct versions when working with `Impostor.Api` prereleases and the `Impostor` dev builds to reduce the chances of mismatching assemblies.
+
+**Example**
+
+The prerelease `Impostor.Api` package `1.2.0-ci.54` belongs to build `54` on AppVeyor, which can be found here https://ci.appveyor.com/project/Impostor/Impostor/build/54. Notice the `54` on the end of the url.
+
+## 9. Missing/invalid data or want more functions?
+
+The `Impostor.Api` is currently in beta. There are a lot of things still missing and we would like to hear from you what you need to develop a plugin.
+
+Create an issue:
+
+- [Suggest a function](https://github.com/Impostor/Impostor/issues/new?template=3--api-suggestion.md)
+- [Data is invalid](https://github.com/Impostor/Impostor/issues/new?template=4--api-invalid.md)
+- [Data is unavailable](https://github.com/Impostor/Impostor/issues/new?template=5--api-missing.md)
+- [Other](https://github.com/Impostor/Impostor/issues/new?template=6--api-other.md) \ No newline at end of file
diff --git a/Impostor-dev/docs/images/client.jpg b/Impostor-dev/docs/images/client.jpg
new file mode 100644
index 0000000..2d82f47
--- /dev/null
+++ b/Impostor-dev/docs/images/client.jpg
Binary files differ
diff --git a/Impostor-dev/docs/images/logo_458.png b/Impostor-dev/docs/images/logo_458.png
new file mode 100644
index 0000000..173f648
--- /dev/null
+++ b/Impostor-dev/docs/images/logo_458.png
Binary files differ
diff --git a/Impostor-dev/docs/images/logo_64.png b/Impostor-dev/docs/images/logo_64.png
new file mode 100644
index 0000000..74c0721
--- /dev/null
+++ b/Impostor-dev/docs/images/logo_64.png
Binary files differ
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
+ 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
+ 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
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACw49DQgF
+ Lq8EAF3/AAC7/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wEAn/8GAjnnCQk0MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAA8bUgMICDJvBQE98QIAjv8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMD/AwB2/wcDMMwKDTsjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADRZJIAgGL58FAUL3AwB+/wEAr/8AAMT/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AQC1/wMAf/8GAz3wCAcvexEgWwgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALDjwbCQgzdwcC
+ LMIGAj/xBABZ/wMAbf8DAHL/AwBw/wQAY/8FAkr8BwMvzwoMMXgLEDYTAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAVLlMNDRY7OgkJLk4LDjRdCxA1WgsPNEIPHUIaGTthAgAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAD//wAAP+MAAP/AAAAA8QAA/gAAAAAIAADgAAAAAAAAAMAAAAAAAgAAwAAAAAADAADAAAAAAAMAAMAA
+ AAAAAwAAwAAAAAADAADAAAAAAAMAAMAAAAAAAwAAwAAAAAADAADAAAAAAAMAAMAAAAAAAwAAwAAAAAAD
+ AADAAAAAAAMAAMAAAAAAAwAAwAAAAAADAADAAAAAAAcAAOAAAAAABwAA4AAAAAAHAADgAAAAAAcAAOAA
+ AAAABwAA4AAAAAAHAADgAAAAAA8AAPAAAAAADwAA8AAAAAAPAADwAAAAAB8AAPAAAAAAHwAA+AAAAAAf
+ AAD4AAAAAD8AAPwAAAAAPwAA/AAAAAA/AAD8AAAAAH8AAP4AAAAAfwAA/gAAAAD/AAD/AAAAAP8AAP8A
+ AAAB/wAA/4AAAAH/AAD/wAAAA/8AAP/AAAAD/wAA/+AAAAf/AAD/8AAAD/8AAP/4AAAf/wAA//wAAD//
+ AAD//wAAf/8AAP//wAH//wAA///4B///AAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEiZMCgoLMCkKDTJLCQovZQgH
+ LHsIBSuJBwMqlAYAJ5QGACeUBwMskgcDKoMIBS1wCQk0XQcCKTUJCzcXEyprAgAAAAAAAAAAAAAAABIa
+ XAAeL40JGBiAAQAAAAAAAAAAAAAAAAAAAAAAAAAAFS1vAQoNODIIBittCAQvpwoDPtcMAkf8DQBQ/w4A
+ Wv8LAEL/CgA8/woAQf8NAE7/DwBf/xIAbf8SAG7/EQBq/xAAZf8PAF7/DgBV/wwAS/8LA0TvCQI0vggH
+ LY0IByxNESJHEgAAAAAeLY4JFxB7AQAAAAAWMHUBCQgzOgcDLZIJAjfiCgA//w0AU/8RAGz/EgBw/xIA
+ cP8SAHD/EgBt/wkAOf8PAF7/DwBc/wsAR/8HAC7/CQA5/xEAa/8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA
+ cP8SAG3/DgBV/wsARP8JATv6CQUzyA0VOygbIocIAAAAAAgFLm8KAD7/DQBO/w0AUf8PAFz/EgBw/xIA
+ cP8SAHD/EgBw/xIAcP8OAGT/DQBR/wsAkP8KAJf/EQBz/woAPf8MAEj/BABs/wIAvP8EALP/CACg/w8A
+ fv8SAHD/EgBw/xIAcP8SAG//DgBY/w0AUf8LAEb/Cg0yVxgVfwAAAAAABgAldgwATP8NAFH/DgBY/xIA
+ b/8SAHD/EgBw/xIAcv8LAJL/AwC2/wMAfP8LAHz/AADG/wAAxv8HAKT/DgBU/wsASP8KAED/AQCm/wAA
+ xv8AAMb/AADE/wkAnP8SAHL/EgBw/xIAcP8RAGz/DQBT/wwASf8JCS9nAAAAAAAAAAAGACVwDABM/w0A
+ Uv8RAGz/EgBw/xIAcP8OAIX/AwC4/wAAxv8AAMb/BQBf/wMAt/8AAMb/AADG/wAAxf8NAF7/CgBA/w4A
+ V/8IAEn/AQCy/wAAxv8AAMb/AADG/wUAsP8RAHb/EgBw/xIAcP8PAGD/DABK/wkKL3QAAAAAAAAAAAYA
+ JVcMAEv/DwBd/xIAcP8SAHD/CgCX/wAAxf8AAMb/AADG/wAAxf8EAGX/AADG/wAAxv8AAMb/AQDD/w0A
+ Vf8JADb/CQA6/w4AV/8GAFP/AADG/wAAxv8AAMb/AADG/wQAsv8SAHL/EgBw/xEAbf8MAEv/CQovdQAA
+ AAAAAAAABwInWQwASv8RAGn/EgBw/wsAk/8AAMb/AADG/wAAxv8AAMb/AQCu/wMAgf8AAMb/AADG/wAA
+ xv8FALD/CgA8/w4AWf8MAE7/CwBD/wgARv8AAMb/AADG/wAAxv8AAMb/AADG/wkAmv8SAHD/EgBw/w0A
+ UP8JCi9oAAAAAAAAAAAIByxdDABL/xIAcP8RAHX/AQDB/wAAxv8AAMb/AADG/wAAxv8CAIr/AQCi/wMA
+ t/8EALT/AADG/wUArP8IAFv/CQB5/woAlP8JAD3/CQA9/wAAvv8AAMb/AADG/wAAxv8AAMb/AQDD/w8A
+ fP8SAHD/DQBT/wgHMVwAAAAAAAAAAAoOM1AMAE3/EgBw/wsAkP8AAMb/AADG/wAAxv8AAMb/AADG/wIA
+ m/8DAGv/AQC8/wcAo/8DALb/AADG/wAAxv8BAMP/CwCT/wYAcv8MAEn/AgCT/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/BgCo/xIAcP8NAFP/CQo2RQAAAAAAAAAACgwyNgwASv8SAHD/BgCp/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wYAPf8GAEr/AQCo/wIAvv8EALH/AQC//wUAr/8AAMP/BgBk/wwAVv8CAJb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMT/EAB4/w0AT/8IBzEmAAAAAAAAAAAJCi8XCwBE/xIAcP8CALz/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/BQBU/xAAYv8LAEv/AwB7/wAAxP8AAMb/AADG/wQAc/8KAIH/BABe/wAA
+ xf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8LAJL/DAFH/AwTRAgAAAAAAAAAABQsUQQLA0D5EQB2/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8EAF7/BwCd/wcApf8JAD7/BABc/wEApv8DAHT/BgBP/wIA
+ nv8CAIX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wcApv8LAkHdAAAAAAAAAAAAAAAAAAAAAAoD
+ OtMPAH3/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAaP8CAK//AADG/wQAYP8AAL3/AgCH/wAA
+ uP8DAG3/BABZ/wAAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BACy/wgBMasAAAAAAAAAAAAA
+ AAAAAAAACAMwqQ8Afv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwBz/wEAqf8AAMb/BABb/wAA
+ xv8AAMb/AADG/wAAxv8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAK3/CAUtfQAA
+ AAAAAAAAAAAAAAAAAAAIBS12DgBw/wAAxv8AAMb/AADG/wAAxf8CALr/BQCt/wgAof8IAGP/AQCn/wAA
+ xv8IAEP/EAB8/xAAef8QAHr/DwB+/w8AgP8OAIT/DACM/wkAmv8FAK3/AQDB/wAAxv8AAMb/AADG/wIA
+ jP8IBi9AAAAAAAAAAAAAAAAAAAAAAAgHMDoMAFr/AADG/wAAxf8JAJv/EAB4/xIAb/8VBF//GAlP/xEG
+ N/8BAKX/AADE/xAILf8rHi//Kx4v/yocMP8mGTH/IRQ0/xwOO/8ZC0f/FgZY/xMBbf8RAHT/CQCc/wAA
+ xf8AAMb/BAFl9BMqWwkAAAAAAAAAAAAAAAAAAAAAEyZkBgoBRO8BAMP/BgCo/xUEYP8gEzb/PjAn/15O
+ NP97akn/UkZB/wEAq/8AALv/QzlE/66dc/+yoXn/sJ51/6mXbP+kkWf/mYZf/4VzUP9oVzr/RDUn/yMW
+ M/8VBV//CACh/wAAxP8HBT+2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAMyqQIAuP8JAJr/Nigl/497
+ WP/OwZf/1MeV/9bKmv9yaGD/AQC1/wEApv9nXVf/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9PH
+ lP/NwJX/l4Rh/zotKv8KAJf/AQCg/wgHLGYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBi9RAgCW/wYA
+ qP9DNjT/uamE/8/CkP+3pnj/p5Rp/0Y7Qv8AAMD/AgCJ/15NQf+Kc0//inNP/4pzT/+LdVH/j3lU/5iD
+ XP+nlGn/u6t7/9bKmf+1pID/Oy0r/wUArP8EAWr7Dhk+FQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkJ
+ NAUEAmTwAQDC/x0RQP+Gck7/lYBa/35mRf9+ZkX/KR4y/wIAjv8aEkP/emJE/35mRf9+ZkX/fmZF/35m
+ Rf9+ZkX/fmZF/35mRf9+ZkX/nYpi/4NuTP8cEET/AQC//wcEPLMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAcEM5IAALj/CwSQ/1NDLP+Bakj/gWpI/4FqSP93YUX/YU4//3tkRv+Bakj/gWpI/4Fq
+ SP+Bakj/gWpI/4FqSP+Bakj/gWpI/4FqSP+Bakj/UkIs/woElf8CAI7/CQkuTgAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAACg4zJgMBdv0AAMX/IBdE/4x4U/+WgVr/j3pV/496Vf+PelX/j3pV/496
+ Vf+PelX/j3pV/496Vf+PelX/j3pV/496Vf+Qe1X/oIxj/4l0Uf8fFkT/AADF/wUCUt0VLVIEAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwQ5pwAAuv8GA6n/VUg2/8O0hP/HuIf/uKh5/7Cf
+ cf+rmW3/qJZq/6aUaf+mk2j/ppNo/6iWa/+wn3H/uqp6/87Aj//Etor/UUQ0/wYDqP8BAKH/CAUtbQAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALDzUmAwFv+AAAxv8TDm3/jn5Z/8/C
+ lf/Tx5X/0cSQ/9XIl//SxpL/0MKO/9DCjv/Qwo7/0MKO/9THlf/YzJ3/1sqg/4l6Vv8SDW//AADG/wUB
+ WOYNFkoKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBzKEAQCn/wEA
+ w/8gGVb/YlY+/25iRP92aUn/e25N/3tuTf99cE//fXBO/3tuTf94a0r/c2dH/21hQ/9hVj3/HhhZ/wAA
+ xP8CAJf/CAUtZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwS
+ NwYGBEnOAAC8/wAAxv8BAcD/AwK1/wUErf8GBKf/BgSn/wYFpP8GBaX/BgSn/wUEqv8EA6//AwK2/wEB
+ wP8AAMb/AAC9/wYDRMcKDDoCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAoLOCEFAVbkAADA/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxf8EAWHwCQs3JQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkKNSsFAVPiAAC4/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMX/AwFy9ggHMUMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkLNxoGA0C5AgCS/wAAxf8AAMb/AADG/wAA
+ xv8AAMb/AADG/wAAxv8AAMb/AQC3/wQBX+gJCDI+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABcyegEJCDJUBgNNyAMA
+ e/8BAKT/AAC8/wAAwf8AALv/AQCg/wQCb/cHBDiWCw49FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAYNn8ACQk1IQkJL18HBCuHCAYvkwgGLIYJCC1ZDBI3EgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAHHwAAAEgAAAAIAAAACAAAABgAAAAYAAAAGAAAABgAAAAYAA
+ AAGAAAABgAAAAYAAAAPAAAADwAAAA8AAAAPAAAADwAAAB+AAAAfgAAAH4AAAD/AAAA/wAAAP+AAAH/gA
+ AB/8AAA//AAAP/4AAH//AAD//4AB///AA///8A//KAAAABAAAAAgAAAAAQAgAAAAAAAABAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAACw46DQgFLUULA0N3DQJOnAoDOrcKAT3HDQBPyg4BVcUOAlOzDAFLkwoD
+ PWwIByw3FSZfBx0oigMIBi4cCgE/sgwASfgRAGj/EgBw/xEAbP8MAF7/DQBr/wkAO/8KAIH/DACN/xEA
+ dP8SAG//DgBY/wsBQvAMEToiBgAlOQ0ATv8RAGn/EQB1/wgAoP8DAJb/AwCw/wIAvf8MAE7/CABi/wAA
+ wf8CALv/DgCC/xIAb/8NAFL/CQovNwYBJiwOAFf/EAB5/wMAuv8AAMb/AgCW/wAAxv8BAMD/CwBI/wsA
+ SP8EAIn/AADG/wEAwf8QAHv/DwBe/wkKLzcJCi8rDwBe/wcAo/8AAMb/AADG/wIAjP8EALP/AgC7/wQA
+ l/8JAHb/BgB2/wAAxv8AAMb/BgCr/xAAYf8JCTMoCgwxEw8AW/8CALz/AADG/wAAxv8DAIf/CABo/wIA
+ q/8CAL7/BQCH/wQAhP8AAMb/AADG/wAAxv8NAGj+CQk1CxQsUQENAV3zAADG/wAAxv8AAMb/AgCV/wQA
+ rv8EAG7/AgCW/wQAbf8BALP/AADG/wAAxv8AAMb/BwF64gAAAAAAAAAADAFdxwAAxv8AAMb/AgC9/wUA
+ j/8BALf/BwB4/wgAoP8HAKL/BgCn/wQAtf8AAMX/AADG/wQBf68AAAAAAAAAAAsCTYwCAL7/EwZq/zEh
+ Sv89MET/AQC0/0s/Rf9tXlP/ZVVO/1VFTP81JUn/FAdp/wIAvP8FA1VtAAAAAAAAAAAIBDE/BQCk/3Bh
+ Tf/KvI3/jYFp/wEAqf+IeF3/rZtv/6+dcP+4p3j/zL+P/3BhTf8FAJP+CQovHwAAAAAAAAAACQk0AQMB
+ juBAMlP/hW5M/2hUQf8+MVX/fmdG/4BoRv+AaEb/gGhG/4dxTv8/MVT/AwKCwAAAAAAAAAAAAAAAAAAA
+ AAAFA1pyCgeb/459Wv+nlWr/noti/5uHX/+bhl7/noph/6qYbf+Pf1z/CgaU/wYERlMAAAAAAAAAAAAA
+ AAAAAAAACw81CQIBj98wKnj/nJBr/6WZb/+mmm//pplt/6SXbf+fk2//Lih4/wMBh9INFkoDAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAHBUY9AQCo+AEBwP8DArj/AwK2/wMCtv8CArn/AQHB/wEAq/sGBEI7AAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYDTEoCAZrtAADG/wAAxv8AAMb/AADG/wEAqvkFA1teAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQgzFQQCZHoDAoe5AwKLxgQCeJgHBjkrAAAAAAAA
+ AAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAgAEAAIABAACAAQAAgAMAAMAD
+ 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