mirror of
https://github.com/Impostor/Impostor.git
synced 2026-10-20 01:46:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a00b9e63d8 | ||
|
|
d7e5594cf6 | ||
|
|
8ba89d9844 | ||
|
|
ebd7f99438 | ||
|
|
2197434bdc | ||
|
|
8f54566245 | ||
|
|
a70e875a4d | ||
|
|
e473e4cdb4 | ||
|
|
9169fab67d | ||
|
|
565bf26ff8 | ||
|
|
8fb996b314 | ||
|
|
46a6005ba5 | ||
|
|
0ac0b1d403 | ||
|
|
e1bd85313a | ||
|
|
fff9713697 | ||
|
|
be0a0b917c | ||
|
|
c672e19e99 | ||
|
|
104376b43f | ||
|
|
92ca5aa9d7 | ||
|
|
67e94e8efc | ||
|
|
da8353655d | ||
|
|
dab8751f1d | ||
|
|
723495e390 | ||
|
|
cd220530ac | ||
|
|
fc2679a8d2 | ||
|
|
28e933092a | ||
|
|
06289d46c0 | ||
|
|
6bd13c0399 | ||
|
|
7e85419b82 | ||
|
|
3e2c3f1525 | ||
|
|
61cbd5c4d8 | ||
|
|
495dc01948 | ||
|
|
a448ece73e | ||
|
|
e7b98f1842 | ||
|
|
d65e99fe45 | ||
|
|
bb980096ff | ||
|
|
58a8bda8e7 | ||
|
|
a7c37b6bcf | ||
|
|
792c9626e4 | ||
|
|
e4f81b4a30 | ||
|
|
2603f34b89 | ||
|
|
9b27edbb11 | ||
|
|
2f9406cb6a | ||
|
|
d5f48aa798 | ||
|
|
ff5501aab7 | ||
|
|
acc4da1210 | ||
|
|
a0f0a65cbf | ||
|
|
6c91280125 | ||
|
|
2ddbb8891c | ||
|
|
8c52b13af4 | ||
|
|
f6318c7040 | ||
|
|
4d6c060c7c | ||
|
|
83bf1c4368 | ||
|
|
ed9f6e3134 | ||
|
|
219b72a3d1 | ||
|
|
01e9cccdf5 | ||
|
|
fc79bd1536 | ||
|
|
9e1abe07ef | ||
|
|
ccd36b6571 | ||
|
|
44e9dfe28d | ||
|
|
09cadcb63b | ||
|
|
b118af12be | ||
|
|
372ed8511f | ||
|
|
85296d2d46 | ||
|
|
c43fdd2a69 | ||
|
|
615469c6c8 | ||
|
|
482b8c2b72 | ||
|
|
2eca9b4e9f | ||
|
|
079f58a7b7 | ||
|
|
c315ef9c28 | ||
|
|
0e7372d9d9 | ||
|
|
bf5a2c2be5 | ||
|
|
066eca09ee | ||
|
|
35c66cda5f | ||
|
|
5d4b3d34f5 | ||
|
|
d94cf78a98 | ||
|
|
cfa66fa5cb | ||
|
|
2d82e1af19 | ||
|
|
c6bad15346 | ||
|
|
b26f451a07 | ||
|
|
4f7ef58b00 | ||
|
|
fe3ea67510 | ||
|
|
c522dd9f88 | ||
|
|
46caeb76c2 | ||
|
|
b218079f9e | ||
|
|
a61524842f | ||
|
|
34c14f69a4 | ||
|
|
da0ee81b5d | ||
|
|
468a71d01e | ||
|
|
6f05946689 | ||
|
|
b2a9bab125 | ||
|
|
9f55fe205d | ||
|
|
734643c8c9 | ||
|
|
661421da5a | ||
|
|
ff2e80d939 | ||
|
|
ddd701320d | ||
|
|
be39c9facf | ||
|
|
6f56e1acaf | ||
|
|
f1f5185f10 | ||
|
|
414303b2c1 | ||
|
|
0ba700ef68 | ||
|
|
c2b5a3e9df | ||
|
|
ac12bacb00 | ||
|
|
2f3e1a620b | ||
|
|
d391b24aad | ||
|
|
5854000220 | ||
|
|
ef022b04e4 | ||
|
|
155c3e69d9 | ||
|
|
3c9b0c305c | ||
|
|
2c0f45c5d1 | ||
|
|
9b7b1e8b26 | ||
|
|
1a11b5f43e | ||
|
|
bdd65e9e9f | ||
|
|
ba9eeb5bb4 | ||
|
|
5dafd9fe5b | ||
|
|
7f943e6b2e | ||
|
|
28f3cb3b2e |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Building from source
|
||||
|
||||
The solution contains the Impostor server and its dependencies, like Hazel and the plugin API. The server is built using [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0).
|
||||
The solution contains the Impostor server and its dependencies, like Hazel and the plugin API. The server is built using [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0).
|
||||
|
||||
## Cloning Impostor
|
||||
|
||||
@@ -14,7 +14,7 @@ git clone https://github.com/Impostor/Impostor.git
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [.NET 7 SDK](https://dotnet.microsoft.com/download/dotnet/7.0)
|
||||
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||
- [Rider](https://www.jetbrains.com/rider/) or [Visual Studio](https://visualstudio.microsoft.com/vs/) (Optional, only if you want the full IDE experience)
|
||||
|
||||
### Build using the CLI
|
||||
|
||||
@@ -8,6 +8,14 @@ The Impostor project is a reverse engineered and open source server for the game
|
||||
|
||||
Yes, Impostor can be used with both the Android and iOS versions of the game. You can add a custom server via a link, you can do this using the [site](https://impostor.github.io/Impostor)
|
||||
|
||||
## Can this be used with the console version of the game?
|
||||
|
||||
Sadly not, as these don't allow adding custom servers.
|
||||
|
||||
## Can this be used with Among Us 3D/VR?
|
||||
|
||||
No, it's an entirely different game with a completely different network stack. So we'd need to develop another server completely from scratch. We're currently not interested in doing this.
|
||||
|
||||
## 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!
|
||||
|
||||
+25
-10
@@ -1,19 +1,13 @@
|
||||
# HTTP Server
|
||||
|
||||
Since Impostor 1.9.0 a HTTP service is included for matchmaking. Recent versions of Among Us require this service in order to connect correctly and not kick players after a round.
|
||||
Since Impostor 1.9.0 a HTTP service is included for matchmaking, which is required for clients to connect to the Impostor server.
|
||||
|
||||
Depending on whether you want to support mobile players, you can set up the HTTP server in one of two ways:
|
||||
|
||||
- Directly expose the HTTP server. Simpler, but only works if you don't want to support mobile players.
|
||||
- Use a reverse proxy to expose the HTTP server. More complex, but allows mobile players to connect.
|
||||
|
||||
## Directly expose the HTTP server.
|
||||
|
||||
In config.json, set "ListenIp" to "0.0.0.0" in the "HttpServer" section. If you don't have this section, look in the example [config.json](https://github.com/Impostor/Impostor/blob/master/src/Impostor.Server/config.json)
|
||||
Starting from Among Us 16.0.5, you need to set up HTTPS for players to connect to your server.
|
||||
Unfortunately Impostor doesn't handle SSL certificates, so you need to set up a reverse proxy.
|
||||
|
||||
## Use a reverse proxy
|
||||
|
||||
A reverse proxy allows you to forward HTTP requests from users to multiple services. If you already have one, you should configure it to add Impostor. This page contains an Nginx configuration you can use for reference.
|
||||
A reverse proxy allows you to forward HTTP requests from users to multiple services. If you already have one, you should configure it to add Impostor.
|
||||
|
||||
If you have never set up a reverse proxy before, we recommend you to set up [Caddy](https://caddyserver.com/). It is easy to set up and comes with support for requesting SSL certificates out of the box.
|
||||
|
||||
@@ -65,3 +59,24 @@ server {
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Stop exposing Impostor's HTTP server to the internet directly
|
||||
|
||||
To only allow connections to Impostor's HTTP servers via the reverse proxy, we recommend to set the ListenIp of the HTTP server to 127.0.0.1:
|
||||
|
||||
```json
|
||||
{
|
||||
// NOTE: merge this snippet into the rest of your configuration
|
||||
"HttpServer": {
|
||||
"ListenIp": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or if you're configuring Impostor with environment variables:
|
||||
|
||||
```sh
|
||||
IMPOSTOR_HttpServer__ListenIp=127.0.0.1
|
||||
```
|
||||
|
||||
For more information on server configuration, see [Server-configuration.md](Server Configuration).
|
||||
+13
-10
@@ -10,19 +10,22 @@ To connect to the server, you need to configure and install a region file on htt
|
||||
|
||||
Among Us connects to the server using two network services: the (TCP) HTTP service points Among Us to the UDP service, then the UDP service hosts the actual game traffic. Because of this, Impostor uses port 22023 using **both** the TCP and UDP protocols.
|
||||
|
||||
As the phone version of Among Us requires this HTTP connection to be secure, we recommend using a HTTP reverse proxy to terminate SSL on the HTTP service. Setup instructions for this are in the [Http Server documentation](Http-server.md).
|
||||
To connect to your Among Us version from another PC, you need to set up a HTTP reverse proxy to support HTTPS. If you're just running a version of Impostor for testing Setup instructions for this are in the [Http Server documentation](Http-server.md).
|
||||
|
||||
If you want to test if your Among Us HTTP server is working, open `http://localhost:22023` (assuming you're using the default settings, change the IP and port respectively) in a browser
|
||||
|
||||
Depending on your host you may also need to port forward Impostor to the internet or pass Impostor traffic by your firewall. Port 22023 UDP needs to be accessible for everyone that wants to play on the server, then you also need to portforward your HTTP reverse proxy or port 22023 TCP if you don't use a reverse proxy. As port forwarding changes per host or router configuration, port forwarding is not covered by this guide.
|
||||
|
||||
## Normal installation
|
||||
|
||||
1. Install [.NET 7.0](https://dotnet.microsoft.com/download/dotnet/7.0). We recommend either the ASP.NET Core Runtime or the SDK. The SDK is necessary in case you want to develop Impostor or Impostor plugins.
|
||||
1. Install [.NET 8.0](https://dotnet.microsoft.com/download/dotnet/8.0). We recommend either the ASP.NET Core Runtime or the SDK. The SDK is necessary in case you want to develop Impostor or Impostor plugins.
|
||||
2. Download the [latest release](https://github.com/Impostor/Impostor/releases) or the [latest CI build](https://nightly.link/Impostor/Impostor/workflows/ci/master). Note that Impostor is built for multiple CPU-architectures and operating systems, you most likely want the x64 version, unless you are running on a Raspberry Pi or another device/VPS with an Arm processor.
|
||||
3. Extract the zip.
|
||||
4. Modify `config.json` to your liking. Documentation can be found [here](Server-configuration.md). You need to at least change `PublicIp` to the address people will connect to your server to.
|
||||
5. Run `Impostor.Server` (Linux/macOS) or `Impostor.Server.exe` (Windows)
|
||||
6. (OPTIONAL - Linux) Configure a systemd definition file and enable the service to start on boot, see [systemd configuration](Server-configuration.md#systemd)
|
||||
7. (OPTIONAL) Set up a reverse proxy to support HTTPS, so mobile phones can connect, see [reverse proxy configuration](Http-server.md)
|
||||
6. Set up a reverse proxy to support HTTPS, so you can connect to your server from another device. See [reverse proxy configuration](Http-server.md)
|
||||
7. (OPTIONAL - Linux) Configure a systemd definition file and enable the service to start on boot, see [systemd configuration](Server-configuration.md#systemd)
|
||||
|
||||
|
||||
## Using Docker
|
||||
|
||||
@@ -34,7 +37,7 @@ Docker is a program that allows you to run programs like Impostor in a container
|
||||
After installing Docker, you can just start a Docker container with `docker run`:
|
||||
|
||||
```
|
||||
docker run -p 22023:22023/tcp -p 22023:22023/udp -e IMPOSTOR_Server__PublicIp=your.public.ip.here aeonlucid/impostor:nightly
|
||||
docker run -p 127.0.0.1:22023:22023/tcp -p 22023:22023/udp -e IMPOSTOR_Server__PublicIp=your.public.ip.here aeonlucid/impostor:nightly
|
||||
```
|
||||
|
||||
Please replace `your.public.ip.here` with the public IP address of your server. This is the address Among Us will try to reach your server at.
|
||||
@@ -43,7 +46,7 @@ To configure the docker container, either use environment variables or mount con
|
||||
|
||||
## Using Docker Compose
|
||||
|
||||
Docker Compose allows you to start a Docker container using a prefined configuration. This is an example configuration you can continue on:
|
||||
Docker Compose allows you to start a Docker container using a predefined configuration. This is an example configuration you can continue on:
|
||||
|
||||
```
|
||||
version: '3.4'
|
||||
@@ -53,12 +56,12 @@ services:
|
||||
image: aeonlucid/impostor:nightly
|
||||
container_name: impostor
|
||||
ports:
|
||||
- 22023:22023/tcp # Add "127.0.0.1:" if you're using a reverse proxy to terminate HTTPS
|
||||
- 127.0.0.1:22023:22023/tcp # Set up your own reverse proxy to terminate HTTPS
|
||||
- 22023:22023/udp
|
||||
environment: # Either configure Impostor using environment variables or mount a copy of config.json
|
||||
environment: # Either configure Impostor using environment variables or mount config.json in your container
|
||||
- IMPOSTOR_Server__PublicIp=your.public.ip.here
|
||||
volumes:
|
||||
- /path/to/local/config.json:/app/config.json # For easy editing of the config
|
||||
- /path/to/local/config.json:/app/config.json # Mount config.json
|
||||
- /path/to/local/plugins:/app/plugins # Only needed if using plugins
|
||||
- /path/to/local/libraries:/app/libraries # Only needed if using external libraries
|
||||
- /path/to/local/libraries:/app/libraries # Only needed if using external libraries (some plugins may need this)
|
||||
```
|
||||
@@ -1,7 +1,5 @@
|
||||
# Server configuration
|
||||
|
||||
Some information about all the possible configurations. Click [here](https://github.com/Impostor/Impostor/blob/master/src/Impostor.Server/config-full.json) to see all the possible config options.
|
||||
|
||||
## Options
|
||||
|
||||
### Required Server Configuration
|
||||
@@ -27,19 +25,30 @@ Impostor has an Http Server that is used by recent versions of Among Us to conne
|
||||
|
||||
Impostor has an Anticheat that makes it possible to kick cheaters from games automatically. Note that the anticheat is tuned on the vanilla version of the game, so client-side modifications could trigger the Anticheat if you're playing with them.
|
||||
|
||||
| Key | Default | Value |
|
||||
| ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Enabled** | `true` | Whether the anticheat should be enabled. |
|
||||
| **BanIpFromGame** | `true` | When anticheat is enabled and 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. |
|
||||
| Key | Default | Value |
|
||||
|-------------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **Enabled** | `true` | Whether the anticheat should be enabled. |
|
||||
| **BanIpFromGame** | `true` | When anticheat is enabled and 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. |
|
||||
| **AllowCheatingHosts** | `"Never"` | Configure whether hosts are allowed to cheat. "Never" forbids it, "Always" allows it. "IfRequested" allows hosts to cheat if they connect with the DisableServerAuthorityFlag set. |
|
||||
| **EnableGameFlowChecks** | `true` | Enable checks that check if certain actions are done in the appropriate order or at the appropriate moment in the game. This includes changing cosmetics while in game or murdering too fast. |
|
||||
| **EnableInvalidObjectChecks** | `true` | Enables checks that check if network objects are spawned properly. Disabling this option also implies disabling EnableRoleChecks. |
|
||||
| **EnableMustBeHostChecks** | `true` | Enables checks that check if players are the host before they can do actions that require them to be host of the game. This includes starting the game and spawning objects. |
|
||||
| **EnableColorLimitChecks** | `true` | Enables checks that checks if players request colors that are already in use. |
|
||||
| **EnableNameLimitChecks** | `true` | Enables checks that checks if player names have a length that is possible to set using the user interface. |
|
||||
| **EnableOwnershipChecks** | `true` | Enables checks that check if players are allowed to perform a certain action on themself or another player. |
|
||||
| **EnableRoleChecks** | `true` | Enables checks that check if players have the correct role when performing certain role abilities like venting or murdering. |
|
||||
| **EnableTargetChecks** | `true` | Enables checks that check if certain packets to everyone that should only have been sent to certain players or vice versa. This includes sending votes or network objects. |
|
||||
| **ForbidProtocolExtensions** | `true` | If disabled allows players to send network packets that go beyond the network packets sent by the vanilla game. This is necessary for most mods that need all players to install it. |
|
||||
|
||||
### Compatibility
|
||||
|
||||
Impostor has two compatibility options which allow some extra flexibility but may not work properly. Enabling either of these options is not recommended.
|
||||
Impostor has some compatibility options which allow some extra flexibility but may not work properly. Enabling any of these options is not recommended. When contacting support, please mention which of these options are enabled.
|
||||
|
||||
| Key | Default | Value |
|
||||
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **AllowNewGameVersions** | `false` | Warning: Setting this option to `true` is unsupported and may cause issues when large updates to Among Us are released. Allow future versions of Among Us to join your server. |
|
||||
| **AllowVersionMixing** | `false` | Allow players using different game versions to play in one lobby. |
|
||||
| Key | Default | Value |
|
||||
|-----------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **AllowFutureGameVersions** | `false` | Warning: Setting this option to `true` is unsupported and may cause issues when large updates to Among Us are released, but can be useful when a small patch is released. Allows future versions of Among Us to join your server. |
|
||||
| **AllowHostAuthority** | `false` | Certain Among Us mods allow disabling some server-authoritative features, which also changes some code paths in the client. These code paths have not undergone as much testing and contain bugs, which can't be fixed from the Impostor side. Use with caution. |
|
||||
| **AllowVersionMixing** | `false` | Allows players using different game versions to play in one lobby that have not been marked by the Impostor developers as compatible. |
|
||||
|
||||
### Debug
|
||||
|
||||
@@ -89,11 +98,11 @@ Other Serilog sinks are also supported, but are out of scope for this documentat
|
||||
|
||||
### 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.
|
||||
The `config.json` file contains all the configurable options.
|
||||
|
||||
### Environment variables
|
||||
|
||||
If you're unable to edit the config.json file, you can instead set Environment variables to configure Impostor. The general pattern for each variable is `IMPOSTOR_SectionName__VariableName`. For example, to disable the anticheat, you can set the environment variable `IMPOSTOR_AntiCheat__Enabled=false`. Here are some more examples:
|
||||
If you're unable to edit the `config.json` file, you can instead set Environment variables to configure Impostor. The general pattern for each variable is `IMPOSTOR_SectionName__VariableName`. For example, to disable the anticheat, you can set the environment variable `IMPOSTOR_AntiCheat__Enabled=false`. Here are some more examples:
|
||||
|
||||
```
|
||||
IMPOSTOR_Server__PublicIp=127.0.0.1
|
||||
|
||||
@@ -47,6 +47,12 @@ Also check if the port Impostor (ListenPort) is listening on is correctly port-f
|
||||
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.
|
||||
|
||||
## Disconnected with `You disconnected from the server. If this happens often, check your network strength. This may also be a server issue`
|
||||
|
||||
Usually this means that Among Us couldn't connect to Impostor's HTTP server.
|
||||
|
||||
Open the address of your server in a web browser, it should show you a small page confirming that Impostor is available.
|
||||
|
||||
## My question is not yet answered and I'm still having problems!
|
||||
|
||||
That's unfortunate. Join the [Impostor Discord](https://discord.gg/Mk3w6Tb), ask your question there and we'll try to help you out. Note that we're not always available, so it may take some time to get an answer. To make answering your question easier, please add the following details:
|
||||
|
||||
@@ -27,18 +27,10 @@ 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 7, which includes:
|
||||
|
||||
- .NET Standard 2.0
|
||||
- .NET Standard 2.1
|
||||
- .NET 5
|
||||
- .NET 6
|
||||
- .NET 7
|
||||
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 8, but we recommend sticking with **.NET 8.0**.
|
||||
|
||||
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 7.0**.
|
||||
|
||||
When the project has been created, you should have `Class.cs` and `Project.csproj` files. Your `Project.csproj` should look something like this.
|
||||
|
||||
```xml
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>11</LangVersion>
|
||||
<VersionPrefix>1.9.1</VersionPrefix>
|
||||
<VersionPrefix>1.10.4</VersionPrefix>
|
||||
<VersionSuffix>dev</VersionSuffix>
|
||||
<EnforceCodeStyleInBuild Condition="$(MSBuildProjectName)!='Hazel'">true</EnforceCodeStyleInBuild>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
|
||||
<PackageReference Include="PolySharp" Version="1.14.1" PrivateAssets="all" />
|
||||
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.0" PrivateAssets="all" GeneratePathProperty="true" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" PrivateAssets="all" GeneratePathProperty="true" />
|
||||
<PackageReference Include="System.Text.Encodings.Web" Version="8.0.0" PrivateAssets="all" GeneratePathProperty="true" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" PrivateAssets="all" GeneratePathProperty="true" />
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ public sealed class SourceGenerator : IIncrementalGenerator
|
||||
enumGenerator.Generate("GameOverReason", underlyingType: CSharpEnumUnderlyingType.Byte);
|
||||
enumGenerator.Generate("Platforms");
|
||||
enumGenerator.Generate("RoleTypes", underlyingType: CSharpEnumUnderlyingType.UnsignedShort);
|
||||
enumGenerator.Generate("RulesPresets", underlyingType: CSharpEnumUnderlyingType.Byte);
|
||||
enumGenerator.Generate("SpecialGameModes", underlyingType: CSharpEnumUnderlyingType.Byte);
|
||||
enumGenerator.Generate("StringNames");
|
||||
enumGenerator.Generate("SystemTypes", underlyingType: CSharpEnumUnderlyingType.Byte);
|
||||
enumGenerator.Generate("Language", sourceName: "SupportedLangs");
|
||||
|
||||
@@ -60,9 +60,9 @@
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==",
|
||||
"requested": "[8.0.5, )",
|
||||
"resolved": "8.0.5",
|
||||
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
|
||||
"System.Buffers": "4.5.1",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace Impostor.Api;
|
||||
|
||||
public enum CheatCategory
|
||||
{
|
||||
/// <summary>A packet used a part of the network protocol that is unknown to Impostor, like a custom RPC.</summary>
|
||||
ProtocolExtension,
|
||||
|
||||
/// <summary>A packet was sent at an inappropriate moment.</summary>
|
||||
GameFlow,
|
||||
|
||||
/// <summary>A packet was sent by a non-host player that should normally only be sent by the host.</summary>
|
||||
MustBeHost,
|
||||
|
||||
/// <summary>A packet was sent that violated limits on the selection of player colors.</summary>
|
||||
ColorLimits,
|
||||
|
||||
/// <summary>A packet was sent that exceeded the limits of possible nicknames to enter ingame.</summary>
|
||||
NameLimits,
|
||||
|
||||
/// <summary>A packet was sent on behalf of another player.</summary>
|
||||
Ownership,
|
||||
|
||||
/// <summary>An ability was used that the current role cannot access.</summary>
|
||||
Role,
|
||||
|
||||
/// <summary>A packet was sent to a player that should be broadcasted, or vice versa.</summary>
|
||||
Target,
|
||||
|
||||
/// <summary>A packet was sent on an invalid network object, like a PlayerControl without PlayerInfo.</summary>
|
||||
InvalidObject,
|
||||
|
||||
/// <summary>Legacy category for unsorted anticheat checks.</summary>
|
||||
Other,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Impostor.Api.Config
|
||||
namespace Impostor.Api.Config
|
||||
{
|
||||
public class AntiCheatConfig
|
||||
{
|
||||
@@ -7,5 +7,25 @@
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public bool BanIpFromGame { get; set; } = true;
|
||||
|
||||
public CheatingHostMode AllowCheatingHosts { get; set; } = CheatingHostMode.Never;
|
||||
|
||||
public bool EnableGameFlowChecks { get; set; } = true;
|
||||
|
||||
public bool EnableMustBeHostChecks { get; set; } = true;
|
||||
|
||||
public bool EnableInvalidObjectChecks { get; set; } = true;
|
||||
|
||||
public bool EnableColorLimitChecks { get; set; } = true;
|
||||
|
||||
public bool EnableNameLimitChecks { get; set; } = true;
|
||||
|
||||
public bool EnableOwnershipChecks { get; set; } = true;
|
||||
|
||||
public bool EnableRoleChecks { get; set; } = true;
|
||||
|
||||
public bool EnableTargetChecks { get; set; } = true;
|
||||
|
||||
public bool ForbidProtocolExtensions { get; set; } = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Impostor.Api.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Details if exceptions are made for hosts that are cheating.
|
||||
/// </summary>
|
||||
public enum CheatingHostMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Hosts follow the same policies as other players.
|
||||
/// </summary>
|
||||
Never,
|
||||
|
||||
/// <summary>
|
||||
/// Hosts are allowed to cheat if they request HostAuthority. If they
|
||||
/// do not request this, the same policies as for other players applies.
|
||||
/// </summary>
|
||||
/// <para>
|
||||
/// HostAuthority can be requested by hosts by adding 25 to their patch
|
||||
/// version when connecting. This flag is used by a lot of (host-only)
|
||||
/// mods and also disable server authority over MurderPlayer packets.
|
||||
/// </para>
|
||||
IfRequested,
|
||||
|
||||
/// <summary>
|
||||
/// Hosts are always allowed to cheat.
|
||||
/// </summary>
|
||||
Always,
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ namespace Impostor.Api.Config
|
||||
|
||||
public bool AllowFutureGameVersions { get; set; } = false;
|
||||
|
||||
public bool AllowHostAuthority { get; set; } = false;
|
||||
|
||||
public bool AllowVersionMixing { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -30,5 +30,7 @@
|
||||
Sorry, UDP Matchmaking is no longer supported.
|
||||
See <link={UpgradingDocsLink}#impostor-190>Impostor documentation</link> on how to migrate to HTTP Matchmaking
|
||||
""";
|
||||
|
||||
public const string HostAuthorityUnsupported = "Your client is requesting host authority, which is not enabled on this Impostor server.";
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace Impostor.Api.Config
|
||||
|
||||
public ushort PublicPort { get; set; } = 22023;
|
||||
|
||||
public string ListenIp { get; set; } = "127.0.0.1";
|
||||
public string ListenIp { get; set; } = "0.0.0.0";
|
||||
|
||||
public ushort ListenPort { get; set; } = 22023;
|
||||
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
using Impostor.Api.Games;
|
||||
using Impostor.Api.Net;
|
||||
|
||||
namespace Impostor.Api.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Called whenever a new <see cref="IGame" /> is created.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that the game just has been created, so no players have joined
|
||||
/// it yet. If you want to know the future host of this game, use the
|
||||
/// <see cref="Host"/> property.
|
||||
/// </remarks>
|
||||
public interface IGameCreatedEvent : IGameEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the client that requested creation of the game.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will be null if game creation was requested by a plugin.
|
||||
/// </remarks>
|
||||
IClient? Host { get; }
|
||||
}
|
||||
}
|
||||
@@ -101,22 +101,41 @@ namespace Impostor.Api.Games
|
||||
/// <returns>A <see cref="ValueTask" /> representing the asynchronous operation.</returns>
|
||||
ValueTask SendToAsync(IMessageWriter writer, int id);
|
||||
|
||||
/// <summary>
|
||||
/// Start a GameData(To) message.
|
||||
/// </summary>
|
||||
/// <remarks>Use with caution, recommended only for advanced developers.</remarks>
|
||||
/// <param name="targetClientId">The client to target if needed, `null` if the message should be sent to all players.</param>
|
||||
/// <param name="type">Message type of the message.</param>
|
||||
/// <returns>A <see cref="IMessageWriter" /> that you can fill and send using <see cref="FinishGameDataAsync"/>.</returns>
|
||||
IMessageWriter StartGameData(int? targetClientId = null, MessageType type = MessageType.Reliable);
|
||||
|
||||
/// <summary>
|
||||
/// Finishes GameData message and sends it to either the target or all players.
|
||||
/// </summary>
|
||||
/// <remarks>Use with caution, recommended only for advanced developers.</remarks>
|
||||
/// <param name="writer">MessageWriter received from <see cref="StartGameData"/>.</param>
|
||||
/// <param name="targetClientId">Same target ClientId passed to StartGameData.</param>
|
||||
/// <returns>Task that sends the packet.</returns>
|
||||
ValueTask FinishGameDataAsync(IMessageWriter writer, int? targetClientId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a MessageWriter with GameData header.
|
||||
/// </summary>
|
||||
/// <remarks>Use with caution, recommended only for advanced developers.</remarks>
|
||||
/// <param name="targetNetId">Net id of the InnerNetObject.</param>
|
||||
/// <param name="callId">Rpc id of the message.</param>
|
||||
/// <param name="targetClientId">Client id of GameDataTo's target.</param>
|
||||
/// <param name="targetClientId">The client to target if needed, `null` if the message should be sent to all players.</param>
|
||||
/// <param name="type">Message type of the message.</param>
|
||||
/// <returns>A <see cref="IMessageWriter" /> that you can fill with rpc data and send using <see cref="FinishRpcAsync"/>.</returns>
|
||||
IMessageWriter StartRpc(uint targetNetId, RpcCalls callId, int? targetClientId, MessageType type = MessageType.Reliable);
|
||||
IMessageWriter StartRpc(uint targetNetId, RpcCalls callId, int? targetClientId = null, MessageType type = MessageType.Reliable);
|
||||
|
||||
/// <summary>
|
||||
/// Finishes rpc message and sends it to the target.
|
||||
/// Finishes rpc message and sends it to either the target or all players.
|
||||
/// </summary>
|
||||
/// <remarks>Use with caution, recommended only for advanced developers.</remarks>
|
||||
/// <param name="writer">Message writer of the rpc.</param>
|
||||
/// <param name="targetClientId">Id of the receiving client.</param>
|
||||
/// <param name="targetClientId">Same target client id passed to <see cref="StartRpc"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask" /> representing the asynchronous operation.</returns>
|
||||
ValueTask FinishRpcAsync(IMessageWriter writer, int? targetClientId = null);
|
||||
}
|
||||
|
||||
@@ -2,42 +2,102 @@ namespace Impostor.Api.Innersloth.Customization
|
||||
{
|
||||
public class PlayerOutfit
|
||||
{
|
||||
public ColorType Color { get; internal set; } = (ColorType)(-1);
|
||||
// Magic value that is assigned by default when the outfit is initially created. This is not
|
||||
// `null` because existing API already exposed this magic value. It's also not 0 (which is
|
||||
// the value exposed when serializing) because SetColor anticheat checks exist.
|
||||
private const ColorType UnknownColor = (ColorType)(-1);
|
||||
|
||||
public string HatId { get; internal set; } = "missing";
|
||||
private string _playerName = string.Empty;
|
||||
private ColorType _color = UnknownColor;
|
||||
private string _hatId = "missing";
|
||||
private string _petId = "missing";
|
||||
private string _skinId = "missing";
|
||||
private string _visorId = "missing";
|
||||
private string _namePlateId = "missing";
|
||||
|
||||
public string PetId { get; internal set; } = "missing";
|
||||
public bool IsDirty { get; internal set; }
|
||||
|
||||
public string SkinId { get; internal set; } = "missing";
|
||||
|
||||
public string VisorId { get; internal set; } = "missing";
|
||||
|
||||
public string NamePlateId { get; internal set; } = "missing";
|
||||
|
||||
public string PlayerName { get; internal set; } = "missing";
|
||||
|
||||
public bool IsIncomplete
|
||||
public string PlayerName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrEmpty(PlayerName) && Color != (ColorType)(-1) && HatId != "missing" && PetId != "missing" && SkinId != "missing" && VisorId != "missing")
|
||||
{
|
||||
return NamePlateId == "missing";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
get => _playerName;
|
||||
internal set => SetField(ref _playerName, value);
|
||||
}
|
||||
|
||||
public ColorType Color
|
||||
{
|
||||
get => _color;
|
||||
internal set => SetField(ref _color, value);
|
||||
}
|
||||
|
||||
public string HatId
|
||||
{
|
||||
get => _hatId;
|
||||
internal set => SetField(ref _hatId, value);
|
||||
}
|
||||
|
||||
public string PetId
|
||||
{
|
||||
get => _petId;
|
||||
internal set => SetField(ref _petId, value);
|
||||
}
|
||||
|
||||
public string SkinId
|
||||
{
|
||||
get => _skinId;
|
||||
internal set => SetField(ref _skinId, value);
|
||||
}
|
||||
|
||||
public string VisorId
|
||||
{
|
||||
get => _visorId;
|
||||
internal set => SetField(ref _visorId, value);
|
||||
}
|
||||
|
||||
public string NamePlateId
|
||||
{
|
||||
get => _namePlateId;
|
||||
internal set => SetField(ref _namePlateId, value);
|
||||
}
|
||||
|
||||
internal byte HatSequenceId { get; set; } = 0;
|
||||
|
||||
internal byte PetSequenceId { get; set; } = 0;
|
||||
|
||||
internal byte SkinSequenceId { get; set; } = 0;
|
||||
|
||||
internal byte VisorSequenceId { get; set; } = 0;
|
||||
|
||||
internal byte NamePlateSequenceId { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a player outfit is complete or whether it needs to receive additional cosmetics.
|
||||
/// </summary>
|
||||
/// <returns>true if the outfit is incomplete, false if the outfit is complete.</returns>
|
||||
public bool IsIncomplete => string.IsNullOrEmpty(PlayerName)
|
||||
|| Color == UnknownColor
|
||||
|| HatId == "missing"
|
||||
|| PetId == "missing"
|
||||
|| SkinId == "missing"
|
||||
|| VisorId == "missing"
|
||||
|| NamePlateId == "missing";
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
{
|
||||
writer.Write(PlayerName);
|
||||
writer.WritePacked((int)Color);
|
||||
|
||||
// Follow officials by not sending a color value that is out of range. Sending -1 can cause exceptions on the client side.
|
||||
writer.WritePacked(Color == UnknownColor ? 0 : (int)Color);
|
||||
|
||||
writer.Write(HatId);
|
||||
writer.Write(PetId);
|
||||
writer.Write(SkinId);
|
||||
writer.Write(VisorId);
|
||||
writer.Write(NamePlateId);
|
||||
writer.Write(HatSequenceId);
|
||||
writer.Write(PetSequenceId);
|
||||
writer.Write(SkinSequenceId);
|
||||
writer.Write(VisorSequenceId);
|
||||
writer.Write(NamePlateSequenceId);
|
||||
}
|
||||
|
||||
public void Deserialize(IMessageReader reader)
|
||||
@@ -49,6 +109,17 @@ namespace Impostor.Api.Innersloth.Customization
|
||||
SkinId = reader.ReadString();
|
||||
VisorId = reader.ReadString();
|
||||
NamePlateId = reader.ReadString();
|
||||
HatSequenceId = reader.ReadByte();
|
||||
PetSequenceId = reader.ReadByte();
|
||||
SkinSequenceId = reader.ReadByte();
|
||||
VisorSequenceId = reader.ReadByte();
|
||||
NamePlateSequenceId = reader.ReadByte();
|
||||
}
|
||||
|
||||
private void SetField<T>(ref T field, T value)
|
||||
{
|
||||
field = value;
|
||||
IsDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,14 @@
|
||||
"0": "SkeldShipStatus",
|
||||
"1": "MeetingHud",
|
||||
"2": "LobbyBehaviour",
|
||||
"3": "GameData",
|
||||
"4": "PlayerControl",
|
||||
"5": "MiraShipStatus",
|
||||
"6": "PolusShipStatus",
|
||||
"7": "ShipStatus",
|
||||
"7": "SkeldShipStatus",
|
||||
"8": "AirshipStatus",
|
||||
"9": "HideAndSeekManager",
|
||||
"10": "NormalGameManager",
|
||||
"11": "NetworkedPlayerInfo",
|
||||
"12": "VoteBanSystem",
|
||||
"13": "FungleShipStatus"
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"HumansByVote": 0,
|
||||
"HumansByTask": 1,
|
||||
"ImpostorByVote": 2,
|
||||
"ImpostorByKill": 3,
|
||||
"ImpostorBySabotage": 4,
|
||||
"CrewmatesByVote": 0,
|
||||
"CrewmatesByTask": 1,
|
||||
"ImpostorsByVote": 2,
|
||||
"ImpostorsByKill": 3,
|
||||
"ImpostorsBySabotage": 4,
|
||||
"ImpostorDisconnect": 5,
|
||||
"HumansDisconnect": 6,
|
||||
"HideAndSeek_ByTimer": 7,
|
||||
"HideAndSeek_ByKills": 8
|
||||
"CrewmateDisconnect": 6,
|
||||
"HideAndSeek_CrewmatesByTimer": 7,
|
||||
"HideAndSeek_ImpostorsByKills": 8
|
||||
}
|
||||
@@ -6,5 +6,10 @@
|
||||
"GuardianAngel": 4,
|
||||
"Shapeshifter": 5,
|
||||
"CrewmateGhost": 6,
|
||||
"ImpostorGhost": 7
|
||||
"ImpostorGhost": 7,
|
||||
"Noisemaker": 8,
|
||||
"Phantom": 9,
|
||||
"Tracker": 10,
|
||||
"Detective": 12,
|
||||
"Viper": 18
|
||||
}
|
||||
@@ -8,15 +8,15 @@
|
||||
"SetName": 6,
|
||||
"CheckColor": 7,
|
||||
"SetColor": 8,
|
||||
"SetHat": 9,
|
||||
"SetSkin": 10,
|
||||
"SetHat_Deprecated": 9,
|
||||
"SetSkin_Deprecated": 10,
|
||||
"ReportDeadBody": 11,
|
||||
"MurderPlayer": 12,
|
||||
"SendChat": 13,
|
||||
"StartMeeting": 14,
|
||||
"SetScanner": 15,
|
||||
"SendChatNote": 16,
|
||||
"SetPet": 17,
|
||||
"SetPet_Deprecated": 17,
|
||||
"SetStartCounter": 18,
|
||||
"EnterVent": 19,
|
||||
"ExitVent": 20,
|
||||
@@ -33,8 +33,8 @@
|
||||
"SendQuickChat": 33,
|
||||
"BootFromVent": 34,
|
||||
"UpdateSystem": 35,
|
||||
"SetVisor": 36,
|
||||
"SetNamePlate": 37,
|
||||
"SetVisor_Deprecated": 36,
|
||||
"SetNamePlate_Deprecated": 37,
|
||||
"SetLevel": 38,
|
||||
"SetHatStr": 39,
|
||||
"SetSkinStr": 40,
|
||||
@@ -55,5 +55,9 @@
|
||||
"CheckShapeshift": 55,
|
||||
"RejectShapeshift": 56,
|
||||
"LobbyTimeExpiring": 60,
|
||||
"ExtendLobbyTimer": 61
|
||||
"ExtendLobbyTimer": 61,
|
||||
"CheckVanish": 62,
|
||||
"StartVanish": 63,
|
||||
"CheckAppear": 64,
|
||||
"StartAppear": 65
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Standard": 0,
|
||||
"StandardRoles": 1,
|
||||
"Flashlight": 2,
|
||||
"Custom": 100
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"None": 0,
|
||||
"ImpersonationCeleb": 1,
|
||||
"Spamming": 2,
|
||||
"InappropriateNameUnsportsmanlike": 3,
|
||||
"UnsportsmanlikeConduct": 4,
|
||||
"ImpersonationDevelopers": 5,
|
||||
"InappropriateChatPersonalInfo": 6,
|
||||
"InappropriateNameDerogatory": 7,
|
||||
"InappropriateNameNsfw": 8,
|
||||
"Bullying": 9,
|
||||
"CheatingHacking": 10,
|
||||
"InappropriateChatDating": 11,
|
||||
"WeaponizingRules": 12,
|
||||
"RepeatOffender3": 13,
|
||||
"SexualMisconduct": 14,
|
||||
"Doxing": 15,
|
||||
"IllegalActivity": 16,
|
||||
"Harassment": 17,
|
||||
"SelfHarmPromotion": 18,
|
||||
"RepeatOffender10": 19,
|
||||
"InvalidUser": 20
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"None": 0,
|
||||
"AprilFools": 1
|
||||
}
|
||||
@@ -570,10 +570,39 @@
|
||||
"RolesHelp_Engineer_02": 1563,
|
||||
"RolesHelp_GuardianAngel_01": 1564,
|
||||
"RolesHelp_GuardianAngel_02": 1565,
|
||||
"RolesHelp_Noisemaker_01": 1566,
|
||||
"RolesHelp_Noisemaker_02": 1567,
|
||||
"RolesHelp_Tracker_01": 1568,
|
||||
"RolesHelp_Tracker_02": 1569,
|
||||
"RolesHelp_Shapeshifter_01": 1600,
|
||||
"RolesHelp_Shapeshifter_02": 1601,
|
||||
"RolesHelp_Phantom_01": 1602,
|
||||
"RolesHelp_Phantom_02": 1603,
|
||||
"RolesHelpOutro_01": 1650,
|
||||
"RolesHelpOutro_02": 1651,
|
||||
"NoisemakerRole": 1660,
|
||||
"NoisemakerBlurb": 1661,
|
||||
"NoisemakerBlurbMed": 1662,
|
||||
"NoisemakerBlurbLong": 1663,
|
||||
"NoisemakerImpostorAlert": 1664,
|
||||
"NoisemakerAlertDuration": 1665,
|
||||
"PhantomAbility": 1670,
|
||||
"PhantomRole": 1671,
|
||||
"PhantomBlurb": 1672,
|
||||
"PhantomBlurbMed": 1673,
|
||||
"PhantomBlurbLong": 1674,
|
||||
"PhantomCooldown": 1675,
|
||||
"PhantomDuration": 1676,
|
||||
"PhantomAbilityUndo": 1677,
|
||||
"TrackerAbility": 1680,
|
||||
"TrackerRole": 1681,
|
||||
"TrackerBlurb": 1682,
|
||||
"TrackerBlurbMed": 1683,
|
||||
"TrackerBlurbLong": 1684,
|
||||
"TrackerCooldown": 1685,
|
||||
"TrackerDuration": 1686,
|
||||
"TrackerAbilityUndo": 1687,
|
||||
"TrackerDelay": 1688,
|
||||
"SanctionDuration": 1700,
|
||||
"SanctionPermanent": 1701,
|
||||
"SanctionConduct": 1702,
|
||||
@@ -1304,6 +1333,24 @@
|
||||
"ConfirmDeleteAccountsEmpty": 3209,
|
||||
"AccountRequestDelete": 3210,
|
||||
"HasBeenKilled": 3211,
|
||||
"RoomSettingsLabel": 3280,
|
||||
"ViewLabel": 3281,
|
||||
"RoomCodeLabel": 3282,
|
||||
"ModeLabel": 3283,
|
||||
"PrivacyLabel": 3284,
|
||||
"CapacityLabel": 3285,
|
||||
"GameSettingsLabel": 3286,
|
||||
"RoleSettingsLabel": 3287,
|
||||
"GamePresetsLabel": 3288,
|
||||
"GamePresetsDescription": 3289,
|
||||
"StandardRulesPresetDescription": 3290,
|
||||
"GameModeStandard": 3291,
|
||||
"GameModeStandardRoles": 3292,
|
||||
"GameModeCustom": 3293,
|
||||
"ReturnToLobby": 3294,
|
||||
"GameModeFlashlight": 3295,
|
||||
"HostNounLabel": 3296,
|
||||
"RoleSettingsAll": 3297,
|
||||
"ScrollList": 3300,
|
||||
"ScrollNews": 3301,
|
||||
"AmongUsAnnouncements": 3302,
|
||||
@@ -1331,6 +1378,40 @@
|
||||
"ReportNotificationBody": 3324,
|
||||
"MissingGuardianEmailText": 3325,
|
||||
"AprilFoolsModeToggle": 3326,
|
||||
"LobbyChangeSettingNotification": 3327,
|
||||
"LoadingBarLoading": 3328,
|
||||
"LoadingBarGameStart": 3329,
|
||||
"LoadingBarGameStartWaitingPlayers": 3330,
|
||||
"ImpostorsCategory": 3350,
|
||||
"RolesCategory": 3351,
|
||||
"MeetingsCategory": 3352,
|
||||
"PlayersCategory": 3353,
|
||||
"TasksCategory": 3354,
|
||||
"GameSettingsDescription": 3355,
|
||||
"RoleSettingsDescription": 3356,
|
||||
"InfoLabel": 3357,
|
||||
"RoleQuotaLabel": 3358,
|
||||
"OverviewCategory": 3359,
|
||||
"CrewmateRolesHeader": 3360,
|
||||
"ImpostorRolesHeader": 3361,
|
||||
"ViewingLabel": 3362,
|
||||
"LocalLobbyLabel": 3363,
|
||||
"NumberRoleHeader": 3364,
|
||||
"ChanceRoleHeader": 3365,
|
||||
"WaitingForPlayers": 3366,
|
||||
"HideAndSeekModeDescription": 3367,
|
||||
"RolesRulesPresetDescription": 3368,
|
||||
"FlashlightRulesPresetDescription": 3369,
|
||||
"RoomCodeCopied": 3370,
|
||||
"FinalHideCategory": 3371,
|
||||
"LobbyChangeSettingNotificationRole": 3372,
|
||||
"StandardRulesPresetDescriptionHideAndSeek": 3373,
|
||||
"GameModeStandardSeek": 3374,
|
||||
"HostYouLabel": 3375,
|
||||
"GamePresetsPopupTitle": 3376,
|
||||
"GamePresetsPopupText": 3377,
|
||||
"GamePresetsPopupContinue": 3378,
|
||||
"GamePresetsPopupBack": 3379,
|
||||
"MiningPit": 3400,
|
||||
"Jungle": 3401,
|
||||
"BuildSandcastle": 3402,
|
||||
@@ -1365,6 +1446,9 @@
|
||||
"CookFish": 3431,
|
||||
"PrepVegetables": 3432,
|
||||
"MushroomMixupName": 3433,
|
||||
"EosAccountSilentMergeFail": 3434,
|
||||
"AskForEosAccount": 3435,
|
||||
"KwsCreateFail": 3436,
|
||||
"GameType": 3500,
|
||||
"GameTypeError": 3501,
|
||||
"GameTypeClassic": 3502,
|
||||
@@ -1399,5 +1483,145 @@
|
||||
"HttpErrorBadGateway": 3531,
|
||||
"HttpErrorServiceUnavailable": 3532,
|
||||
"HttpErrorGatewayTimeout": 3533,
|
||||
"UserVersionError": 3534
|
||||
"UserVersionError": 3534,
|
||||
"QCLobbyRoleNoisemakers": 3535,
|
||||
"QCLobbyRoleTrackers": 3536,
|
||||
"QCLobbyRolePhantoms": 3537,
|
||||
"QCSelfWasTracking": 3538,
|
||||
"QCSelfGoingToNoisemaker": 3539,
|
||||
"QCSelfSawRoleInLocation": 3540,
|
||||
"QCCoreSettings": 3541,
|
||||
"QCRolesGalore": 3542,
|
||||
"QCPitchDark": 3543,
|
||||
"QCFlashlight": 3544,
|
||||
"QCPleaseWait": 3545,
|
||||
"QCWaitMinute": 3546,
|
||||
"QCLobbyClosing": 3547,
|
||||
"QCChangeCosmetics": 3548,
|
||||
"QCWaitingPlayers": 3549,
|
||||
"QCLetsWaitPlayers": 3550,
|
||||
"ServerLabel": 3551,
|
||||
"JoinLobby": 3552,
|
||||
"ChatLabel": 3553,
|
||||
"ToolTipDefault": 3554,
|
||||
"LevelLabel": 3555,
|
||||
"NoneLowercaseLabel": 3556,
|
||||
"ToolTipClassic": 3557,
|
||||
"ToolTipHide": 3558,
|
||||
"ToolTipMap": 3559,
|
||||
"ToolTipCapacity": 3560,
|
||||
"ToolTipLevel": 3561,
|
||||
"ToolTipServer": 3562,
|
||||
"ToolTipCrossplay": 3563,
|
||||
"ToolTipNoCrossplay": 3564,
|
||||
"ToolTipLanguage": 3565,
|
||||
"ConfirmSettings": 3566,
|
||||
"ConfirmSettingsTooltip": 3567,
|
||||
"EnterCodeElipsis": 3568,
|
||||
"ToolTipChatTab": 3569,
|
||||
"DropshipsLabel": 3570,
|
||||
"FiltersActive": 3571,
|
||||
"ClearFilters": 3572,
|
||||
"MoreLabel": 3573,
|
||||
"HostNounEmpty": 3574,
|
||||
"ToolTipSkeld": 3575,
|
||||
"ToolTipMIRA": 3576,
|
||||
"ToolTipPolus": 3577,
|
||||
"ToolTipAirship": 3578,
|
||||
"ToolTipFungle": 3579,
|
||||
"FilterLow": 3580,
|
||||
"FilterMed": 3581,
|
||||
"FilterHigh": 3582,
|
||||
"FilterVeryHigh": 3583,
|
||||
"FilterLong": 3584,
|
||||
"FilterNone": 3585,
|
||||
"FilterPlayerLevel": 3586,
|
||||
"RefreshLabel": 3587,
|
||||
"JoinGameLabel": 3588,
|
||||
"FilterClearText": 3589,
|
||||
"MatchesFoundLabel": 3590,
|
||||
"FilterGameTooltip": 3591,
|
||||
"FilterMapTooltip": 3592,
|
||||
"FilterImpostorTooltip": 3593,
|
||||
"FilterRoleTooltip": 3594,
|
||||
"FilterSpeedTooltip": 3595,
|
||||
"FilterKillCDTooltip": 3596,
|
||||
"FilterVotingTooltip": 3597,
|
||||
"FilterVisualTaskTooltip": 3598,
|
||||
"FilterAnonVotesTooltip": 3599,
|
||||
"FilterConfirmEjectTooltip": 3600,
|
||||
"FilterLevelTooltip": 3601,
|
||||
"FilterCrossplayTooltip": 3602,
|
||||
"FilterDefaultTooltip": 3603,
|
||||
"FilterFlashlightTooltip": 3604,
|
||||
"FilterHidingTimeTooltip": 3605,
|
||||
"FilterFinalTimeTooltip": 3606,
|
||||
"FilterMaxVentTooltip": 3607,
|
||||
"FilterBeginner": 3608,
|
||||
"FilterIntermediate": 3609,
|
||||
"FilterExpert": 3610,
|
||||
"FilterTag": 3611,
|
||||
"FilterTagTooltip": 3612,
|
||||
"TooltipTag": 3613,
|
||||
"SelectedLabel": 3614,
|
||||
"TooltipSpeedInfo": 3615,
|
||||
"TooltipKillCDInfo": 3616,
|
||||
"TooltipVotingInfo": 3617,
|
||||
"TooptipHidingTimeInfo": 3618,
|
||||
"TooltipFinalTimeInfo": 3619,
|
||||
"FilterNoMaps": 3620,
|
||||
"MaxFilters": 3621,
|
||||
"XboxCommsMultiplayerError": 3700,
|
||||
"XboxCommsUGError": 3701,
|
||||
"XboxCommsCreateGameError": 3702,
|
||||
"XboxCommsSearchError": 3703,
|
||||
"XboxCommsJoinError": 3704,
|
||||
"DetectiveRole": 4000,
|
||||
"DetectiveAbility": 4001,
|
||||
"DetectiveAbilityNotes": 4002,
|
||||
"DetectiveBlurb": 4003,
|
||||
"DetectiveBlurbMed": 4004,
|
||||
"DetectiveBlurbLong": 4005,
|
||||
"DetectiveNotesEmpty": 4006,
|
||||
"DetectiveNotesFoundDead": 4007,
|
||||
"DetectiveNotesSuspectInfo": 4008,
|
||||
"DetectiveNearLocation": 4009,
|
||||
"DetectiveNotesKiller": 4010,
|
||||
"StatsDetectiveQuestioned": 4011,
|
||||
"StatsDetectiveCasesFilled": 4012,
|
||||
"DetectiveSuspectLimit": 4013,
|
||||
"RolesHelp_Detective_01": 4014,
|
||||
"RolesHelp_Detective_02": 4015,
|
||||
"ViperRole": 4037,
|
||||
"ViperBlurb": 4038,
|
||||
"ViperBlurbMed": 4039,
|
||||
"ViperBlurbLong": 4040,
|
||||
"ViperDissolveTime": 4041,
|
||||
"ViperAbility": 4042,
|
||||
"StatsViperBodiesDissolved": 4043,
|
||||
"RolesHelp_Viper_01": 4044,
|
||||
"RolesHelp_Viper_02": 4045,
|
||||
"QCWhereWasDead": 4060,
|
||||
"QCInterrogateCrew": 4061,
|
||||
"QCInterrogated": 4062,
|
||||
"QCBodyDissolved": 4063,
|
||||
"QCBodyVeryDissolved": 4064,
|
||||
"QCBodyAlmostDissolved": 4065,
|
||||
"QCDetectives": 4066,
|
||||
"QCVipers": 4067,
|
||||
"DetectiveNotesTitleVictim": 4068,
|
||||
"DetectiveNotesTitleFoundDead": 4069,
|
||||
"DetectiveNotesTitleKiller": 4070,
|
||||
"DetectiveNotesTitleSuspects": 4071,
|
||||
"DetectiveNotesTitleLocationsAtTimeOfDeath": 4072,
|
||||
"DetectiveNotesPromptPreposition": 4073,
|
||||
"DetectiveNotesPromptLocation": 4074,
|
||||
"DetectiveNotesPromptImpostor": 4075,
|
||||
"DetectiveNotesPromptSuspect": 4076,
|
||||
"DetectiveNotesPrepositionIn": 4077,
|
||||
"DetectiveNotesPrepositionBy": 4078,
|
||||
"DetectiveNotesPrepositionMaybe": 4079,
|
||||
"DetectiveNotesTitlePostIt": 4080,
|
||||
"DetectiveNotesPostIt": 4081,
|
||||
"DetectiveNotesSuspectNumber": 4082
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dumpostorVersion": "1.0.0",
|
||||
"gameVersion": "2024.3.5",
|
||||
"dumpostorVersion": "1.0.1-dev",
|
||||
"gameVersion": "2025.9.9",
|
||||
"platformType": "StandaloneItch"
|
||||
}
|
||||
}
|
||||
@@ -1,93 +1,93 @@
|
||||
{
|
||||
"0": {
|
||||
"room": "Cafeteria",
|
||||
"position": {
|
||||
"x": 6.3936,
|
||||
"y": 1.33848
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"room": "Storage",
|
||||
"position": {
|
||||
"x": 5.268,
|
||||
"y": -14.2884
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"room": "UpperEngine",
|
||||
"position": {
|
||||
"x": 16.930801,
|
||||
"y": -2.1276002
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"room": "Cafeteria",
|
||||
"position": {
|
||||
"x": 0.708,
|
||||
"y": -4.992
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"room": "LowerEngine",
|
||||
"position": {
|
||||
"x": 14.6808,
|
||||
"y": -11.472
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"room": "UpperEngine",
|
||||
"position": {
|
||||
"x": 14.676002,
|
||||
"y": 1.3406401
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"room": "Security",
|
||||
"position": {
|
||||
"x": 14.734801,
|
||||
"y": -5.1417603
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
"room": "Storage",
|
||||
"position": {
|
||||
"x": -1.1316001,
|
||||
"y": -11.98128
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"room": "Cafeteria",
|
||||
"position": {
|
||||
"x": -5.1432004,
|
||||
"y": 1.3355999
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
"1": {
|
||||
"room": "Cafeteria",
|
||||
"position": {
|
||||
"x": 0.708,
|
||||
"y": -4.992
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"room": "Cafeteria",
|
||||
"position": {
|
||||
"x": 6.3936,
|
||||
"y": 1.33848
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"room": "Electrical",
|
||||
"position": {
|
||||
"x": 9.53616,
|
||||
"y": -13.416001
|
||||
}
|
||||
},
|
||||
"10": {
|
||||
"room": "MedBay",
|
||||
"4": {
|
||||
"room": "Storage",
|
||||
"position": {
|
||||
"x": 9.1452,
|
||||
"y": -0.4439999
|
||||
"x": -1.1316001,
|
||||
"y": -11.98128
|
||||
}
|
||||
},
|
||||
"11": {
|
||||
"5": {
|
||||
"room": "Storage",
|
||||
"position": {
|
||||
"x": 0.70919997,
|
||||
"y": -8.560801
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"room": "Storage",
|
||||
"position": {
|
||||
"x": 5.268,
|
||||
"y": -14.2884
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
"room": "LowerEngine",
|
||||
"position": {
|
||||
"x": 14.6808,
|
||||
"y": -11.472
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"room": "LowerEngine",
|
||||
"position": {
|
||||
"x": 16.930801,
|
||||
"y": -8.9664
|
||||
}
|
||||
},
|
||||
"12": {
|
||||
"room": "Storage",
|
||||
"9": {
|
||||
"room": "UpperEngine",
|
||||
"position": {
|
||||
"x": 0.70919997,
|
||||
"y": -8.560801
|
||||
"x": 14.676002,
|
||||
"y": 1.3406401
|
||||
}
|
||||
},
|
||||
"10": {
|
||||
"room": "UpperEngine",
|
||||
"position": {
|
||||
"x": 16.930801,
|
||||
"y": -2.1276002
|
||||
}
|
||||
},
|
||||
"11": {
|
||||
"room": "Security",
|
||||
"position": {
|
||||
"x": 14.734801,
|
||||
"y": -5.1417603
|
||||
}
|
||||
},
|
||||
"12": {
|
||||
"room": "MedBay",
|
||||
"position": {
|
||||
"x": 9.1452,
|
||||
"y": -0.4439999
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"initialSpawnCenter": {
|
||||
"x": -0.72,
|
||||
"y": 0.62
|
||||
"x": 0.87,
|
||||
"y": 0.69
|
||||
},
|
||||
"meetingSpawnCenter": {
|
||||
"x": -0.72,
|
||||
"y": 0.62
|
||||
"x": 0.87,
|
||||
"y": 0.69
|
||||
},
|
||||
"meetingSpawnCenter2": {
|
||||
"x": 0,
|
||||
|
||||
@@ -476,5 +476,138 @@
|
||||
"usableDistance": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"29": {
|
||||
"type": "NormalPlayerTask",
|
||||
"taskType": "VentCleaning",
|
||||
"length": "Short",
|
||||
"consoles": [
|
||||
{
|
||||
"id": 0,
|
||||
"room": "Admin",
|
||||
"position": {
|
||||
"x": -2.544,
|
||||
"y": -9.955201
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"room": "Cafeteria",
|
||||
"position": {
|
||||
"x": -4.2588,
|
||||
"y": -0.27600002
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"room": "Nav",
|
||||
"position": {
|
||||
"x": -16.008001,
|
||||
"y": -6.3840003
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"room": "Nav",
|
||||
"position": {
|
||||
"x": -16.008001,
|
||||
"y": -3.1680002
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"room": "Weapons",
|
||||
"position": {
|
||||
"x": -8.820001,
|
||||
"y": 3.3240001
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"room": "Shields",
|
||||
"position": {
|
||||
"x": -9.5232,
|
||||
"y": -14.337601
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"room": "Hallway",
|
||||
"position": {
|
||||
"x": -9.384,
|
||||
"y": -6.438
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"room": "Electrical",
|
||||
"position": {
|
||||
"x": 9.7764,
|
||||
"y": -8.034
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"room": "Reactor",
|
||||
"position": {
|
||||
"x": 20.796001,
|
||||
"y": -6.9528003
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"room": "Reactor",
|
||||
"position": {
|
||||
"x": 21.876,
|
||||
"y": -3.0516002
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"room": "LowerEngine",
|
||||
"position": {
|
||||
"x": 15.2508,
|
||||
"y": -13.656001
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"room": "UpperEngine",
|
||||
"position": {
|
||||
"x": 15.288,
|
||||
"y": 2.52
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"room": "Security",
|
||||
"position": {
|
||||
"x": 12.534,
|
||||
"y": -6.9492
|
||||
},
|
||||
"usableDistance": 1
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"room": "MedBay",
|
||||
"position": {
|
||||
"x": 10.608001,
|
||||
"y": -4.176
|
||||
},
|
||||
"usableDistance": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"0": {
|
||||
"name": "AdminVent",
|
||||
"name": "AdminVent (1)",
|
||||
"position": {
|
||||
"x": -2.544,
|
||||
"y": -9.955201
|
||||
@@ -10,7 +10,7 @@
|
||||
"right": 1
|
||||
},
|
||||
"1": {
|
||||
"name": "BigYVent",
|
||||
"name": "BigYVent (1)",
|
||||
"position": {
|
||||
"x": -9.384,
|
||||
"y": -6.438
|
||||
@@ -20,7 +20,7 @@
|
||||
"right": 2
|
||||
},
|
||||
"2": {
|
||||
"name": "CafeVent",
|
||||
"name": "CafeVent (1)",
|
||||
"position": {
|
||||
"x": -4.2588,
|
||||
"y": -0.27600002
|
||||
@@ -30,7 +30,7 @@
|
||||
"right": 1
|
||||
},
|
||||
"3": {
|
||||
"name": "ElecVent",
|
||||
"name": "ElecVent (1)",
|
||||
"position": {
|
||||
"x": 9.7764,
|
||||
"y": -8.034
|
||||
@@ -40,7 +40,7 @@
|
||||
"right": 6
|
||||
},
|
||||
"4": {
|
||||
"name": "LEngineVent",
|
||||
"name": "LEngineVent (1)",
|
||||
"position": {
|
||||
"x": 15.288,
|
||||
"y": 2.52
|
||||
@@ -50,7 +50,7 @@
|
||||
"right": null
|
||||
},
|
||||
"5": {
|
||||
"name": "LifeSuppVent",
|
||||
"name": "SecurityVent",
|
||||
"position": {
|
||||
"x": 12.534,
|
||||
"y": -6.9492
|
||||
@@ -60,7 +60,7 @@
|
||||
"right": 3
|
||||
},
|
||||
"6": {
|
||||
"name": "MedVent",
|
||||
"name": "MedVent (1)",
|
||||
"position": {
|
||||
"x": 10.608001,
|
||||
"y": -4.176
|
||||
@@ -70,7 +70,7 @@
|
||||
"right": 3
|
||||
},
|
||||
"7": {
|
||||
"name": "WeaponsVent",
|
||||
"name": "WeaponsVent (1)",
|
||||
"position": {
|
||||
"x": -8.820001,
|
||||
"y": 3.3240001
|
||||
@@ -80,7 +80,7 @@
|
||||
"right": 12
|
||||
},
|
||||
"8": {
|
||||
"name": "ReactorVent",
|
||||
"name": "ReactorVent (1)",
|
||||
"position": {
|
||||
"x": 20.796001,
|
||||
"y": -6.9528003
|
||||
@@ -90,7 +90,7 @@
|
||||
"right": null
|
||||
},
|
||||
"9": {
|
||||
"name": "REngineVent",
|
||||
"name": "REngineVent (1)",
|
||||
"position": {
|
||||
"x": 15.2508,
|
||||
"y": -13.656001
|
||||
@@ -100,7 +100,7 @@
|
||||
"right": null
|
||||
},
|
||||
"10": {
|
||||
"name": "ShieldsVent",
|
||||
"name": "ShieldsVent (1)",
|
||||
"position": {
|
||||
"x": -9.5232,
|
||||
"y": -14.337601
|
||||
@@ -110,7 +110,7 @@
|
||||
"right": null
|
||||
},
|
||||
"11": {
|
||||
"name": "UpperReactorVent",
|
||||
"name": "UpperReactorVent (1)",
|
||||
"position": {
|
||||
"x": 21.876,
|
||||
"y": -3.0516002
|
||||
@@ -120,7 +120,7 @@
|
||||
"right": null
|
||||
},
|
||||
"12": {
|
||||
"name": "NavVentNorth",
|
||||
"name": "NavVentNorth (1)",
|
||||
"position": {
|
||||
"x": -16.008001,
|
||||
"y": -3.1680002
|
||||
@@ -130,7 +130,7 @@
|
||||
"right": 7
|
||||
},
|
||||
"13": {
|
||||
"name": "NavVentSouth",
|
||||
"name": "NavVentSouth (1)",
|
||||
"position": {
|
||||
"x": -16.008001,
|
||||
"y": -6.3840003
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Impostor.Api.Innersloth.GameOptions;
|
||||
|
||||
// TODO: generate this enum based on game data
|
||||
|
||||
/// <summary>
|
||||
/// Tag that can be added to indicate the expected skill level of players in this lobby.
|
||||
/// </summary>
|
||||
public enum GameTags
|
||||
{
|
||||
None = 0,
|
||||
Beginner = 1,
|
||||
Intermediate = 2,
|
||||
Expert = 3,
|
||||
}
|
||||
@@ -2,7 +2,7 @@ namespace Impostor.Api.Innersloth.GameOptions;
|
||||
|
||||
public class HideNSeekGameOptions : IGameOptions
|
||||
{
|
||||
public const int LatestVersion = 7;
|
||||
public const int LatestVersion = 10;
|
||||
|
||||
public HideNSeekGameOptions(byte version = LatestVersion)
|
||||
{
|
||||
@@ -16,6 +16,12 @@ public class HideNSeekGameOptions : IGameOptions
|
||||
/// <inheritdoc />
|
||||
public GameModes GameMode => GameModes.HideNSeek;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SpecialGameModes SpecialMode { get; set; } = SpecialGameModes.None;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RulesPresets RulesPreset { get; set; } = RulesPresets.Custom;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte MaxPlayers { get; set; } = 15;
|
||||
|
||||
@@ -87,6 +93,11 @@ public class HideNSeekGameOptions : IGameOptions
|
||||
|
||||
public float MaxPingTime { get; set; } = 6f;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the experience level of people in the lobby: Beginner, Intermediate or Expert.
|
||||
/// </summary>
|
||||
public GameTags Tag { get; set; } = 0;
|
||||
|
||||
public static HideNSeekGameOptions Deserialize(IMessageReader reader, byte version)
|
||||
{
|
||||
var options = new HideNSeekGameOptions(version);
|
||||
@@ -96,6 +107,12 @@ public class HideNSeekGameOptions : IGameOptions
|
||||
|
||||
public void Deserialize(IMessageReader reader)
|
||||
{
|
||||
if (Version >= 8)
|
||||
{
|
||||
SpecialMode = (SpecialGameModes)reader.ReadByte();
|
||||
RulesPreset = (RulesPresets)reader.ReadByte();
|
||||
}
|
||||
|
||||
MaxPlayers = reader.ReadByte();
|
||||
Keywords = (GameKeywords)reader.ReadInt32();
|
||||
Map = (MapTypes)reader.ReadByte();
|
||||
@@ -121,7 +138,12 @@ public class HideNSeekGameOptions : IGameOptions
|
||||
MaxPingTime = reader.ReadSingle();
|
||||
CrewmateTimeInVent = reader.ReadSingle();
|
||||
|
||||
if (Version > 7)
|
||||
if (Version >= 9)
|
||||
{
|
||||
Tag = (GameTags)reader.ReadByte();
|
||||
}
|
||||
|
||||
if (Version > LatestVersion)
|
||||
{
|
||||
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
|
||||
}
|
||||
@@ -129,6 +151,12 @@ public class HideNSeekGameOptions : IGameOptions
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
{
|
||||
if (Version >= 8)
|
||||
{
|
||||
writer.Write((byte)SpecialMode);
|
||||
writer.Write((byte)RulesPreset);
|
||||
}
|
||||
|
||||
writer.Write(MaxPlayers);
|
||||
writer.Write((uint)Keywords);
|
||||
writer.Write((byte)Map);
|
||||
@@ -154,7 +182,12 @@ public class HideNSeekGameOptions : IGameOptions
|
||||
writer.Write(MaxPingTime);
|
||||
writer.Write(CrewmateTimeInVent);
|
||||
|
||||
if (Version > 7)
|
||||
if (Version >= 9)
|
||||
{
|
||||
writer.Write((byte)Tag);
|
||||
}
|
||||
|
||||
if (Version > LatestVersion)
|
||||
{
|
||||
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,20 @@ public interface IGameOptions
|
||||
public byte Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently active gamemode.
|
||||
/// Gets the currently active gamemode. This is currently used for the Normal and HideAndSeek gamemodes.
|
||||
/// </summary>
|
||||
public GameModes GameMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently active special gamemode. This is currently used for the AprilFools gamemode.
|
||||
/// </summary>
|
||||
public SpecialGameModes SpecialMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rule preset.
|
||||
/// </summary>
|
||||
public RulesPresets RulesPreset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum amount of players for this lobby.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,6 +21,12 @@ public class LegacyGameOptionsData : IGameOptions
|
||||
|
||||
public GameModes GameMode => GameModes.Normal;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SpecialGameModes SpecialMode { get; } = SpecialGameModes.None;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RulesPresets RulesPreset { get; } = RulesPresets.Custom;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum amount of players for this lobby.
|
||||
/// </summary>
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Impostor.Api.Innersloth.GameOptions;
|
||||
|
||||
public class NormalGameOptions : IGameOptions
|
||||
{
|
||||
public const int LatestVersion = 7;
|
||||
public const int LatestVersion = 10;
|
||||
|
||||
public NormalGameOptions(byte version = LatestVersion)
|
||||
{
|
||||
@@ -19,6 +19,12 @@ public class NormalGameOptions : IGameOptions
|
||||
/// <inheritdoc />
|
||||
public GameModes GameMode => GameModes.Normal;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SpecialGameModes SpecialMode { get; set; } = SpecialGameModes.None;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RulesPresets RulesPreset { get; set; } = RulesPresets.Custom;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte MaxPlayers { get; set; } = 10;
|
||||
|
||||
@@ -122,6 +128,11 @@ public class NormalGameOptions : IGameOptions
|
||||
/// </summary>
|
||||
public TaskBarUpdate TaskBarUpdate { get; set; } = TaskBarUpdate.Always;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the experience level of people in the lobby: Beginner, Intermediate or Expert.
|
||||
/// </summary>
|
||||
public GameTags Tag { get; set; } = 0;
|
||||
|
||||
public RoleOptionsCollection RoleOptions { get; set; }
|
||||
|
||||
public static NormalGameOptions Deserialize(IMessageReader reader, byte version)
|
||||
@@ -133,6 +144,12 @@ public class NormalGameOptions : IGameOptions
|
||||
|
||||
public void Deserialize(IMessageReader reader)
|
||||
{
|
||||
if (Version >= 8)
|
||||
{
|
||||
SpecialMode = (SpecialGameModes)reader.ReadByte();
|
||||
RulesPreset = (RulesPresets)reader.ReadByte();
|
||||
}
|
||||
|
||||
MaxPlayers = reader.ReadByte();
|
||||
Keywords = (GameKeywords)reader.ReadUInt32();
|
||||
Map = (MapTypes)reader.ReadByte();
|
||||
@@ -161,9 +178,14 @@ public class NormalGameOptions : IGameOptions
|
||||
AnonymousVotes = reader.ReadBoolean();
|
||||
TaskBarUpdate = (TaskBarUpdate)reader.ReadByte();
|
||||
|
||||
if (Version >= 9)
|
||||
{
|
||||
Tag = (GameTags)reader.ReadByte();
|
||||
}
|
||||
|
||||
RoleOptions.Deserialize(reader);
|
||||
|
||||
if (Version > 7)
|
||||
if (Version > LatestVersion)
|
||||
{
|
||||
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
|
||||
}
|
||||
@@ -171,6 +193,12 @@ public class NormalGameOptions : IGameOptions
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
{
|
||||
if (Version >= 8)
|
||||
{
|
||||
writer.Write((byte)SpecialMode);
|
||||
writer.Write((byte)RulesPreset);
|
||||
}
|
||||
|
||||
writer.Write(MaxPlayers);
|
||||
writer.Write((uint)Keywords);
|
||||
writer.Write((byte)Map);
|
||||
@@ -199,9 +227,14 @@ public class NormalGameOptions : IGameOptions
|
||||
writer.Write(AnonymousVotes);
|
||||
writer.Write((byte)TaskBarUpdate);
|
||||
|
||||
if (Version >= 9)
|
||||
{
|
||||
writer.Write((byte)Tag);
|
||||
}
|
||||
|
||||
RoleOptions.Serialize(writer);
|
||||
|
||||
if (Version > 7)
|
||||
if (Version > LatestVersion)
|
||||
{
|
||||
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
|
||||
|
||||
public class DetectiveRoleOptions : IRoleOptions
|
||||
{
|
||||
public DetectiveRoleOptions(byte version)
|
||||
{
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public byte Version { get; }
|
||||
|
||||
public RoleTypes Type => RoleTypes.Detective;
|
||||
|
||||
public byte DetectiveSuspectLimit { get; set; } = 3;
|
||||
|
||||
public static DetectiveRoleOptions Deserialize(IMessageReader reader, byte version)
|
||||
{
|
||||
var options = new DetectiveRoleOptions(version);
|
||||
|
||||
options.DetectiveSuspectLimit = reader.ReadByte();
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
{
|
||||
writer.Write((byte)DetectiveSuspectLimit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
|
||||
|
||||
public class NoisemakerRoleOptions : IRoleOptions
|
||||
{
|
||||
public NoisemakerRoleOptions(byte version)
|
||||
{
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public byte Version { get; }
|
||||
|
||||
public RoleTypes Type => RoleTypes.Noisemaker;
|
||||
|
||||
public bool ImpostorAlert { get; set; } = true;
|
||||
|
||||
public byte AlertDuration { get; set; } = 10;
|
||||
|
||||
public static NoisemakerRoleOptions Deserialize(IMessageReader reader, byte version)
|
||||
{
|
||||
var options = new NoisemakerRoleOptions(version);
|
||||
|
||||
options.AlertDuration = reader.ReadByte();
|
||||
options.ImpostorAlert = reader.ReadBoolean();
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
{
|
||||
writer.Write(AlertDuration);
|
||||
writer.Write(ImpostorAlert);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
|
||||
|
||||
public class PhantomRoleOptions : IRoleOptions
|
||||
{
|
||||
public PhantomRoleOptions(byte version)
|
||||
{
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public byte Version { get; }
|
||||
|
||||
public RoleTypes Type => RoleTypes.Phantom;
|
||||
|
||||
public byte Cooldown { get; set; } = 15;
|
||||
|
||||
public byte Duration { get; set; } = 30;
|
||||
|
||||
public static PhantomRoleOptions Deserialize(IMessageReader reader, byte version)
|
||||
{
|
||||
var options = new PhantomRoleOptions(version);
|
||||
|
||||
options.Cooldown = reader.ReadByte();
|
||||
options.Duration = reader.ReadByte();
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
{
|
||||
writer.Write(Cooldown);
|
||||
writer.Write(Duration);
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@ public class RoleOptionsCollection
|
||||
|
||||
public void Deserialize(IMessageReader reader)
|
||||
{
|
||||
Roles.Clear();
|
||||
|
||||
var count = reader.ReadPackedInt32();
|
||||
Roles.EnsureCapacity(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
@@ -31,10 +29,15 @@ public class RoleOptionsCollection
|
||||
RoleTypes.Engineer => EngineerRoleOptions.Deserialize(roleOptionsReader, Version),
|
||||
RoleTypes.GuardianAngel => GuardianAngelRoleOptions.Deserialize(roleOptionsReader, Version),
|
||||
RoleTypes.Shapeshifter => ShapeshifterRoleOptions.Deserialize(roleOptionsReader, Version),
|
||||
RoleTypes.Noisemaker => NoisemakerRoleOptions.Deserialize(roleOptionsReader, Version),
|
||||
RoleTypes.Phantom => PhantomRoleOptions.Deserialize(roleOptionsReader, Version),
|
||||
RoleTypes.Tracker => TrackerRoleOptions.Deserialize(roleOptionsReader, Version),
|
||||
RoleTypes.Detective => DetectiveRoleOptions.Deserialize(roleOptionsReader, Version),
|
||||
RoleTypes.Viper => ViperRoleOptions.Deserialize(roleOptionsReader, Version),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(roleType), roleType, null),
|
||||
};
|
||||
|
||||
Roles.Add(roleType, new RoleData(roleType, roleOptions, roleRate));
|
||||
Roles[roleType] = new RoleData(roleType, roleOptions, roleRate);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
|
||||
|
||||
public class TrackerRoleOptions : IRoleOptions
|
||||
{
|
||||
public TrackerRoleOptions(byte version)
|
||||
{
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public byte Version { get; }
|
||||
|
||||
public RoleTypes Type => RoleTypes.Tracker;
|
||||
|
||||
public byte Cooldown { get; set; } = 15;
|
||||
|
||||
public byte Duration { get; set; } = 30;
|
||||
|
||||
public byte Delay { get; set; } = 1;
|
||||
|
||||
public static TrackerRoleOptions Deserialize(IMessageReader reader, byte version)
|
||||
{
|
||||
var options = new TrackerRoleOptions(version);
|
||||
|
||||
options.Cooldown = reader.ReadByte();
|
||||
options.Duration = reader.ReadByte();
|
||||
options.Delay = reader.ReadByte();
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
{
|
||||
writer.Write(Cooldown);
|
||||
writer.Write(Duration);
|
||||
writer.Write(Delay);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
|
||||
|
||||
public class ViperRoleOptions : IRoleOptions
|
||||
{
|
||||
public ViperRoleOptions(byte version)
|
||||
{
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public byte Version { get; }
|
||||
|
||||
public RoleTypes Type => RoleTypes.Viper;
|
||||
|
||||
public byte ViperDissolveTime { get; set; } = 15;
|
||||
|
||||
public static ViperRoleOptions Deserialize(IMessageReader reader, byte version)
|
||||
{
|
||||
var options = new ViperRoleOptions(version);
|
||||
|
||||
options.ViperDissolveTime = reader.ReadByte();
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
{
|
||||
writer.Write((byte)ViperDissolveTime);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api.Innersloth;
|
||||
@@ -7,7 +8,7 @@ namespace Impostor.Api.Net
|
||||
/// <summary>
|
||||
/// Represents a connected game client.
|
||||
/// </summary>
|
||||
public interface IClient
|
||||
public interface IClient : IEquatable<IClient>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique ID of the client.
|
||||
@@ -74,6 +75,9 @@ namespace Impostor.Api.Net
|
||||
/// </summary>
|
||||
PlatformSpecificData PlatformSpecificData { get; }
|
||||
|
||||
ValueTask<bool> ReportCheatAsync(CheatContext context, CheatCategory category, string message);
|
||||
|
||||
[Obsolete("Please use the overload that adds a cheat category")]
|
||||
ValueTask<bool> ReportCheatAsync(CheatContext context, string message);
|
||||
|
||||
ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Impostor.Api.Net.Inner.Objects
|
||||
{
|
||||
public interface IInnerGameData : IInnerNetObject
|
||||
public interface IInnerGameData
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api.Innersloth;
|
||||
using Impostor.Api.Innersloth.Customization;
|
||||
@@ -28,13 +29,14 @@ namespace Impostor.Api.Net.Inner.Objects
|
||||
/// Gets the <see cref="IInnerPlayerInfo" /> of the <see cref="IInnerPlayerControl" />.
|
||||
/// Contains metadata about the player.
|
||||
/// </summary>
|
||||
IInnerPlayerInfo PlayerInfo { get; }
|
||||
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>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player doesn't have a PlayerInfo.</exception>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask SetNameAsync(string name);
|
||||
|
||||
@@ -43,6 +45,7 @@ namespace Impostor.Api.Net.Inner.Objects
|
||||
/// Visible to all players.
|
||||
/// </summary>
|
||||
/// <param name="colorType">A color for the player.</param>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player doesn't have a PlayerInfo.</exception>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask SetColorAsync(ColorType colorType);
|
||||
|
||||
@@ -51,6 +54,7 @@ namespace Impostor.Api.Net.Inner.Objects
|
||||
/// Visible to all players.
|
||||
/// </summary>
|
||||
/// <param name="hatId">An hat for the player.</param>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player doesn't have a PlayerInfo.</exception>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask SetHatAsync(string hatId);
|
||||
|
||||
@@ -59,6 +63,7 @@ namespace Impostor.Api.Net.Inner.Objects
|
||||
/// Visible to all players.
|
||||
/// </summary>
|
||||
/// <param name="petId">A pet for the player.</param>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player doesn't have a PlayerInfo.</exception>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask SetPetAsync(string petId);
|
||||
|
||||
@@ -67,9 +72,26 @@ namespace Impostor.Api.Net.Inner.Objects
|
||||
/// Visible to all players.
|
||||
/// </summary>
|
||||
/// <param name="skinId">A skin for the player.</param>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player doesn't have a PlayerInfo.</exception>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask SetSkinAsync(string skinId);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the visor of the current <see cref="IInnerPlayerControl" />.
|
||||
/// Visible to all players.
|
||||
/// </summary>
|
||||
/// <param name="visorId">A visor for the player.</param>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask SetVisorAsync(string visorId);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the nameplate of the current <see cref="IInnerPlayerControl" />.
|
||||
/// Visible to all players.
|
||||
/// </summary>
|
||||
/// <param name="nameplateId">A nameplate for the player.</param>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask SetNamePlateAsync(string nameplateId);
|
||||
|
||||
/// <summary>
|
||||
/// Send a chat message as the current <see cref="IInnerPlayerControl" />.
|
||||
/// Visible to all players.
|
||||
@@ -98,32 +120,60 @@ namespace Impostor.Api.Net.Inner.Objects
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player is not the impostor.</exception>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player is dead.</exception>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when target is dead.</exception>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player or target player doesn't have a PlayerInfo.</exception>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result);
|
||||
|
||||
/// <summary>
|
||||
/// Murder <paramref name="target" /> player successfully.
|
||||
/// Murder <paramref name="target" /> player.
|
||||
/// </summary>
|
||||
/// <param name="target">Target player to murder.</param>
|
||||
/// <param name="result">The result of the murder operation.</param>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask ForceMurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result);
|
||||
|
||||
/// <summary>
|
||||
/// Murder <paramref name="target" /> player after validating if this is a valid kill.
|
||||
/// </summary>
|
||||
/// <param name="target">Target player to murder.</param>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player is not the impostor.</exception>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player is dead.</exception>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when target is dead.</exception>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player or target player doesn't have a PlayerInfo.</exception>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
[Obsolete("Please switch to version with the MurderResultFlags argument")]
|
||||
ValueTask MurderPlayerAsync(IInnerPlayerControl target);
|
||||
|
||||
/// <summary>
|
||||
/// Protect <paramref name="target" /> player.
|
||||
/// </summary>
|
||||
/// <param name="target">Target player to protect.</param>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when target is a guardian angel.</exception>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when target is dead.</exception>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player or target player doesn't have a PlayerInfo.</exception>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask ProtectPlayerAsync(IInnerPlayerControl target);
|
||||
|
||||
/// <summary>
|
||||
/// Protect <paramref name="target" /> player.
|
||||
/// </summary>
|
||||
/// <param name="target">Target player to protect.</param>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask ForceProtectPlayerAsync(IInnerPlayerControl target);
|
||||
|
||||
/// <summary>
|
||||
/// Exile the current player. This doesn't produce a body to be reported.
|
||||
/// Visible to all players.
|
||||
/// </summary>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown if player to be exiled is already dead.</exception>
|
||||
/// <exception cref="ImpostorProtocolException">Thrown when player or target player doesn't have a PlayerInfo.</exception>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask ExileAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Exile the current player. This doesn't produce a body to be reported.
|
||||
/// Visible to all players.
|
||||
/// </summary>
|
||||
/// <returns>Task that must be awaited.</returns>
|
||||
ValueTask ExileAsync();
|
||||
ValueTask ForceExileAsync();
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ namespace Impostor.Api.Net.Messages
|
||||
public const byte SetActivePodType = 21;
|
||||
public const byte QueryPlatformIds = 22;
|
||||
public const byte QueryLobbyInfo = 23;
|
||||
public const byte EndGameHostMigration = 24;
|
||||
|
||||
private static readonly Dictionary<byte, string> FlagCache;
|
||||
|
||||
@@ -41,7 +42,7 @@ namespace Impostor.Api.Net.Messages
|
||||
|
||||
public static string FlagToString(byte flag)
|
||||
{
|
||||
return FlagCache[flag];
|
||||
return FlagCache.TryGetValue(flag, out var res) ? res : $"Unknown Flag {flag}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,15 @@ namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc06SetName
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, string name)
|
||||
public static void Serialize(IMessageWriter writer, uint netId, string name)
|
||||
{
|
||||
writer.Write(netId);
|
||||
writer.Write(name);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out string name)
|
||||
public static void Deserialize(IMessageReader reader, out uint netId, out string name)
|
||||
{
|
||||
netId = reader.ReadUInt32();
|
||||
name = reader.ReadString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc08SetColor
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, ColorType color)
|
||||
public static void Serialize(IMessageWriter writer, uint netId, ColorType color)
|
||||
{
|
||||
writer.Write(netId);
|
||||
writer.Write((byte)color);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out ColorType color)
|
||||
public static void Deserialize(IMessageReader reader, out uint netId, out ColorType color)
|
||||
{
|
||||
netId = reader.ReadUInt32();
|
||||
color = (ColorType)reader.ReadByte();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,13 @@ namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc29SetTasks
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, byte playerId, ReadOnlyMemory<byte> taskTypeIds)
|
||||
public static void Serialize(IMessageWriter writer, ReadOnlyMemory<byte> taskTypeIds)
|
||||
{
|
||||
writer.Write(playerId);
|
||||
writer.Write(taskTypeIds);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out byte playerId, out ReadOnlyMemory<byte> taskTypeIds)
|
||||
public static void Deserialize(IMessageReader reader, out ReadOnlyMemory<byte> taskTypeIds)
|
||||
{
|
||||
playerId = reader.ReadByte();
|
||||
taskTypeIds = reader.ReadBytesAndSize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc39SetHatStr
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, string hat)
|
||||
public static void Serialize(IMessageWriter writer, string hat, byte nextRpcSequenceId)
|
||||
{
|
||||
writer.Write(hat);
|
||||
writer.Write(nextRpcSequenceId);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out string hat)
|
||||
public static void Deserialize(IMessageReader reader, out string hat, out byte nextRpcSequenceId)
|
||||
{
|
||||
hat = reader.ReadString();
|
||||
nextRpcSequenceId = reader.ReadByte();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc40SetSkinStr
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, string skin)
|
||||
public static void Serialize(IMessageWriter writer, string skin, byte nextRpcSequenceId)
|
||||
{
|
||||
writer.Write(skin);
|
||||
writer.Write(nextRpcSequenceId);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out string skin)
|
||||
public static void Deserialize(IMessageReader reader, out string skin, out byte nextRpcSequenceId)
|
||||
{
|
||||
skin = reader.ReadString();
|
||||
nextRpcSequenceId = reader.ReadByte();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc41SetPetStr
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, string pet)
|
||||
public static void Serialize(IMessageWriter writer, string pet, byte nextRpcSequenceId)
|
||||
{
|
||||
writer.Write(pet);
|
||||
writer.Write(nextRpcSequenceId);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out string pet)
|
||||
public static void Deserialize(IMessageReader reader, out string pet, out byte nextRpcSequenceId)
|
||||
{
|
||||
pet = reader.ReadString();
|
||||
nextRpcSequenceId = reader.ReadByte();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc42SetVisorStr
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, string visor)
|
||||
public static void Serialize(IMessageWriter writer, string visor, byte nextRpcSequenceId)
|
||||
{
|
||||
writer.Write(visor);
|
||||
writer.Write(nextRpcSequenceId);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out string visor)
|
||||
public static void Deserialize(IMessageReader reader, out string visor, out byte nextRpcSequenceId)
|
||||
{
|
||||
visor = reader.ReadString();
|
||||
nextRpcSequenceId = reader.ReadByte();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc43SetNamePlateStr
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, string namePlate)
|
||||
public static void Serialize(IMessageWriter writer, string namePlate, byte nextRpcSequenceId)
|
||||
{
|
||||
writer.Write(namePlate);
|
||||
writer.Write(nextRpcSequenceId);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out string namePlate)
|
||||
public static void Deserialize(IMessageReader reader, out string namePlate, out byte nextRpcSequenceId)
|
||||
{
|
||||
namePlate = reader.ReadString();
|
||||
nextRpcSequenceId = reader.ReadByte();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc44SetRole
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, RoleTypes role)
|
||||
public static void Serialize(IMessageWriter writer, RoleTypes role, bool canOverrideRole)
|
||||
{
|
||||
writer.Write((ushort)role);
|
||||
writer.Write(canOverrideRole);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out RoleTypes role)
|
||||
public static void Deserialize(IMessageReader reader, out RoleTypes role, out bool canOverrideRole)
|
||||
{
|
||||
role = (RoleTypes)reader.ReadUInt16();
|
||||
canOverrideRole = reader.ReadBoolean();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc62CheckVanish
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, float maxDuration)
|
||||
{
|
||||
writer.Write(maxDuration);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out float maxDuration)
|
||||
{
|
||||
maxDuration = reader.ReadSingle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc63StartVanish
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer)
|
||||
{
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc64CheckAppear
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, bool shouldAnimate)
|
||||
{
|
||||
writer.Write(shouldAnimate);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out bool shouldAnimate)
|
||||
{
|
||||
shouldAnimate = reader.ReadBoolean();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Impostor.Api.Net.Messages.Rpcs
|
||||
{
|
||||
public static class Rpc65StartAppear
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, bool shouldAnimate)
|
||||
{
|
||||
writer.Write(shouldAnimate);
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out bool shouldAnimate)
|
||||
{
|
||||
shouldAnimate = reader.ReadBoolean();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ namespace Impostor.Api.Net.Messages.S2C
|
||||
writer.Write(hostId);
|
||||
writer.Write(player.Client.Name);
|
||||
player.Client.PlatformSpecificData.Serialize(writer);
|
||||
writer.WritePacked(player.Character?.PlayerInfo.PlayerLevel ?? 1);
|
||||
writer.WritePacked(player.Character?.PlayerInfo?.PlayerLevel ?? 1);
|
||||
|
||||
// ProductUserId and FriendCode are not yet known, so set them to an empty string
|
||||
writer.Write(string.Empty);
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Impostor.Api.Net.Messages.S2C
|
||||
writer.WritePacked(ply.Client.Id);
|
||||
writer.Write(ply.Client.Name);
|
||||
ply.Client.PlatformSpecificData.Serialize(writer);
|
||||
writer.WritePacked(ply.Character?.PlayerInfo.PlayerLevel ?? 1);
|
||||
writer.WritePacked(ply.Character?.PlayerInfo?.PlayerLevel ?? 1);
|
||||
|
||||
// ProductUserId and FriendCode are not yet known, so set them to an empty string
|
||||
writer.Write(string.Empty);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Impostor.Api.Games;
|
||||
|
||||
namespace Impostor.Api.Net.Messages.S2C
|
||||
{
|
||||
public class Message24EndGameHostMigrationS2C
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, GameCode gameCode, int hostClientId)
|
||||
{
|
||||
writer.StartMessage(MessageFlags.EndGameHostMigration);
|
||||
gameCode.Serialize(writer);
|
||||
writer.Write(hostClientId);
|
||||
writer.EndMessage();
|
||||
}
|
||||
|
||||
public static void Deserialize(IMessageReader reader, out GameCode gameCode, out int hostClientId)
|
||||
{
|
||||
gameCode = reader.ReadInt32();
|
||||
hostClientId = reader.ReadInt32();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
using Impostor.Api.Events;
|
||||
using Impostor.Api.Games;
|
||||
using Impostor.Api.Net;
|
||||
|
||||
namespace Impostor.Server.Events
|
||||
{
|
||||
public class GameCreatedEvent : IGameCreatedEvent
|
||||
{
|
||||
public GameCreatedEvent(IGame game)
|
||||
public GameCreatedEvent(IGame game, IClient? host)
|
||||
{
|
||||
Game = game;
|
||||
Host = host;
|
||||
}
|
||||
|
||||
public IGame Game { get; }
|
||||
|
||||
public IClient? Host { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using Impostor.Api.Innersloth.GameOptions;
|
||||
using Impostor.Hazel;
|
||||
|
||||
namespace Impostor.Server.Extensions;
|
||||
|
||||
public static class IGameOptionsExtensions
|
||||
{
|
||||
public static byte[] ToBytes(this IGameOptions gameOptions)
|
||||
{
|
||||
using var writer = MessageWriter.Get(MessageType.Unreliable);
|
||||
writer.Write(gameOptions.Version);
|
||||
writer.StartMessage(0);
|
||||
writer.Write((byte)gameOptions.GameMode);
|
||||
gameOptions.Serialize(writer);
|
||||
writer.EndMessage();
|
||||
return writer.ToByteArray(false);
|
||||
}
|
||||
|
||||
public static string ToBase64String(this IGameOptions gameOptions)
|
||||
{
|
||||
return Convert.ToBase64String(gameOptions.ToBytes());
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using Impostor.Api.Config;
|
||||
using Impostor.Api.Games;
|
||||
using Impostor.Api.Games.Managers;
|
||||
using Impostor.Api.Innersloth;
|
||||
using Impostor.Server.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -50,6 +51,7 @@ public sealed class GamesController : ControllerBase
|
||||
[HttpGet]
|
||||
public IActionResult Index(int mapId, GameKeywords lang, int numImpostors, [FromHeader] AuthenticationHeaderValue authorization)
|
||||
{
|
||||
// NOTE: this method is no longer used by Among Us 16.0.0 and is only kept for backwards compatibility
|
||||
if (authorization.Scheme != "Bearer" || authorization.Parameter == null)
|
||||
{
|
||||
return BadRequest();
|
||||
@@ -75,6 +77,7 @@ public sealed class GamesController : ControllerBase
|
||||
[HttpPost]
|
||||
public IActionResult Post(int gameId)
|
||||
{
|
||||
// NOTE: this method is no longer used by Among Us 16.0.0 and is only kept for backwards compatibility
|
||||
var code = new GameCode(gameId);
|
||||
var game = _gameManager.Find(code);
|
||||
|
||||
@@ -97,6 +100,38 @@ public sealed class GamesController : ControllerBase
|
||||
return Ok(_hostServer);
|
||||
}
|
||||
|
||||
[HttpGet("{gameId}")]
|
||||
public IActionResult Show([FromRoute] int gameId)
|
||||
{
|
||||
var code = new GameCode(gameId);
|
||||
var game = _gameManager.Find(code);
|
||||
|
||||
// If the game was not found, print an error message.
|
||||
if (game == null)
|
||||
{
|
||||
return NotFound(new FindGameByCodeResponse(new MatchmakerError(DisconnectReason.GameNotFound)));
|
||||
}
|
||||
|
||||
return Ok(new FindGameByCodeResponse(GameListing.From(game)));
|
||||
}
|
||||
|
||||
[HttpGet("filtered")]
|
||||
public IActionResult ShowFilteredLobbies()
|
||||
{
|
||||
// TODO: implement this stub
|
||||
var response = new
|
||||
{
|
||||
games = Array.Empty<GameListing>(),
|
||||
metadata = new
|
||||
{
|
||||
allGamesCount = _gameManager.Games.Count(),
|
||||
matchingGamesCount = 0,
|
||||
},
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
private static uint ConvertAddressToNumber(IPAddress address)
|
||||
{
|
||||
#pragma warning disable CS0618 // Among Us only supports IPv4
|
||||
@@ -151,6 +186,21 @@ public sealed class GamesController : ControllerBase
|
||||
public required DisconnectReason Reason { get; init; }
|
||||
}
|
||||
|
||||
private class FindGameByCodeResponse
|
||||
{
|
||||
[SetsRequiredMembers]
|
||||
public FindGameByCodeResponse(MatchmakerError error) => (Errors, Game) = (new[] { error }, null);
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public FindGameByCodeResponse(GameListing game) => (Errors, Game) = (null, game);
|
||||
|
||||
[JsonPropertyName("Errors")]
|
||||
public required MatchmakerError[]? Errors { get; init; }
|
||||
|
||||
[JsonPropertyName("Game")]
|
||||
public required GameListing? Game { get; init; }
|
||||
}
|
||||
|
||||
private class GameListing
|
||||
{
|
||||
[JsonPropertyName("IP")]
|
||||
@@ -168,12 +218,18 @@ public sealed class GamesController : ControllerBase
|
||||
[JsonPropertyName("HostName")]
|
||||
public required string HostName { get; init; }
|
||||
|
||||
[JsonPropertyName("TrueHostName")]
|
||||
public required string TrueHostName { get; init; }
|
||||
|
||||
[JsonPropertyName("HostPlatformName")]
|
||||
public required string HostPlatformName { get; init; }
|
||||
|
||||
[JsonPropertyName("Platform")]
|
||||
public required Platforms Platform { get; init; }
|
||||
|
||||
[JsonPropertyName("QuickChat")]
|
||||
public required QuickChatModes QuickChat { get; init; }
|
||||
|
||||
[JsonPropertyName("Age")]
|
||||
public required int Age { get; init; }
|
||||
|
||||
@@ -189,6 +245,9 @@ public sealed class GamesController : ControllerBase
|
||||
[JsonPropertyName("Language")]
|
||||
public required GameKeywords Language { get; init; }
|
||||
|
||||
[JsonPropertyName("Options")]
|
||||
public required string Options { get; init; }
|
||||
|
||||
public static GameListing From(IGame game)
|
||||
{
|
||||
var platform = game.Host?.Client.PlatformSpecificData;
|
||||
@@ -200,13 +259,16 @@ public sealed class GamesController : ControllerBase
|
||||
GameId = game.Code,
|
||||
PlayerCount = game.PlayerCount,
|
||||
HostName = game.DisplayName ?? game.Host?.Client.Name ?? "Unknown host",
|
||||
TrueHostName = game.DisplayName ?? game.Host?.Client.Name ?? "Unknown host",
|
||||
HostPlatformName = platform?.PlatformName ?? string.Empty,
|
||||
Platform = platform?.Platform ?? Platforms.Unknown,
|
||||
QuickChat = game.Host?.Client.ChatMode ?? QuickChatModes.QuickChatOnly,
|
||||
Age = 0,
|
||||
MaxPlayers = game.Options.MaxPlayers,
|
||||
NumImpostors = game.Options.NumImpostors,
|
||||
MapId = game.Options.Map,
|
||||
Language = game.Options.Keywords,
|
||||
Options = game.Options.ToBase64String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Impostor.Server.Http;
|
||||
|
||||
/// <summary>
|
||||
/// Generate a diagnostic page to show that the Impostor HTTP server is working.
|
||||
/// </summary>
|
||||
[Route("/")]
|
||||
public sealed class HelloController : ControllerBase
|
||||
{
|
||||
private static bool _shownHello = false;
|
||||
private readonly ILogger<HelloController> _logger;
|
||||
|
||||
public HelloController(ILogger<HelloController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult GetHello()
|
||||
{
|
||||
if (!_shownHello)
|
||||
{
|
||||
_shownHello = true;
|
||||
_logger.LogInformation("Impostor's Http server is reachable (this message is only printed once per start)");
|
||||
}
|
||||
|
||||
return Ok(
|
||||
"""
|
||||
Impostor is running, please configure your Among Us to connect to a game
|
||||
To generate a region file, go to https://impostor.github.io/Impostor
|
||||
"""
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -35,21 +35,75 @@ namespace Impostor.Server.Net
|
||||
_customMessageManager = customMessageManager;
|
||||
}
|
||||
|
||||
public override async ValueTask<bool> ReportCheatAsync(CheatContext context, string message)
|
||||
public override async ValueTask<bool> ReportCheatAsync(CheatContext context, CheatCategory category, string message)
|
||||
{
|
||||
if (!_antiCheatConfig.Enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogWarning("Client {Name} ({Id}) was caught cheating: [{Context}] {Message}", Name, Id, context.Name, message);
|
||||
|
||||
if (_antiCheatConfig.BanIpFromGame)
|
||||
if (Player != null && Player.IsHost)
|
||||
{
|
||||
Player?.Game.BanIp(Connection.EndPoint.Address);
|
||||
var isHostCheatingAllowed = _antiCheatConfig.AllowCheatingHosts switch {
|
||||
CheatingHostMode.Always => true,
|
||||
CheatingHostMode.IfRequested => GameVersion.HasDisableServerAuthorityFlag,
|
||||
CheatingHostMode.Never => false,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if (isHostCheatingAllowed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await DisconnectAsync(DisconnectReason.Hacking, context.Name + ": " + message);
|
||||
bool LogUnknownCategory(CheatCategory category)
|
||||
{
|
||||
_logger.LogWarning("Unknown cheat category {Category} was used when reporting", category);
|
||||
return true;
|
||||
}
|
||||
|
||||
var isCategoryEnabled = category switch
|
||||
{
|
||||
CheatCategory.ProtocolExtension => _antiCheatConfig.ForbidProtocolExtensions,
|
||||
CheatCategory.GameFlow => _antiCheatConfig.EnableGameFlowChecks,
|
||||
CheatCategory.InvalidObject => _antiCheatConfig.EnableInvalidObjectChecks,
|
||||
CheatCategory.MustBeHost => _antiCheatConfig.EnableMustBeHostChecks,
|
||||
CheatCategory.ColorLimits => _antiCheatConfig.EnableColorLimitChecks,
|
||||
CheatCategory.NameLimits => _antiCheatConfig.EnableNameLimitChecks,
|
||||
CheatCategory.Ownership => _antiCheatConfig.EnableOwnershipChecks,
|
||||
CheatCategory.Role => _antiCheatConfig.EnableRoleChecks,
|
||||
CheatCategory.Target => _antiCheatConfig.EnableTargetChecks,
|
||||
CheatCategory.Other => true,
|
||||
_ => LogUnknownCategory(category),
|
||||
};
|
||||
|
||||
if (!isCategoryEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var supportCode = Random.Shared.Next(0, 999_999).ToString("000-000");
|
||||
|
||||
_logger.LogWarning("Client {Name} ({Id}) was caught cheating: [{SupportCode}] [{Context}-{Category}] {Message}", Name, Id, supportCode, context.Name, category, message);
|
||||
|
||||
if (Player is { } player)
|
||||
{
|
||||
if (_antiCheatConfig.BanIpFromGame)
|
||||
{
|
||||
player.Game.BanIp(Connection.EndPoint.Address);
|
||||
}
|
||||
|
||||
await player.Game.HandleRemovePlayer(Id, DisconnectReason.Hacking);
|
||||
}
|
||||
|
||||
var disconnectMessage =
|
||||
$"""
|
||||
You have been caught cheating and were {(_antiCheatConfig.BanIpFromGame ? "banned" : "kicked")} from the lobby.
|
||||
For questions, contact your server admin and share the following code: {supportCode}.
|
||||
""";
|
||||
|
||||
await DisconnectAsync(DisconnectReason.Custom, disconnectMessage);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -308,7 +362,9 @@ namespace Impostor.Server.Net
|
||||
{
|
||||
if (Player != null)
|
||||
{
|
||||
await Player.Game.HandleRemovePlayer(Id, DisconnectReason.ExitGame);
|
||||
// The client never sends over the real disconnect reason so we always assume ExitGame
|
||||
var isRemote = reason == "The remote sent a disconnect request";
|
||||
await Player.Game.HandleRemovePlayer(Id, isRemote ? DisconnectReason.ExitGame : DisconnectReason.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -318,6 +374,7 @@ namespace Impostor.Server.Net
|
||||
|
||||
_logger.LogInformation("Client {0} disconnecting, reason: {1}", Id, reason);
|
||||
_clientManager.Remove(this);
|
||||
await _gameManager.OnClientDisconnectAsync(this);
|
||||
}
|
||||
|
||||
private bool IsPacketAllowed(IMessageReader message, bool hostOnly)
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api;
|
||||
using Impostor.Api.Innersloth;
|
||||
using Impostor.Api.Innersloth.Customization;
|
||||
using Impostor.Api.Net;
|
||||
using Impostor.Server.Net.State;
|
||||
|
||||
@@ -39,13 +40,20 @@ namespace Impostor.Server.Net
|
||||
|
||||
public ClientPlayer? Player { get; set; }
|
||||
|
||||
public ColorType? PreviousColor { get; set; } = null;
|
||||
|
||||
IClientPlayer? IClient.Player => Player;
|
||||
|
||||
public virtual ValueTask<bool> ReportCheatAsync(CheatContext context, string message)
|
||||
public virtual ValueTask<bool> ReportCheatAsync(CheatContext context, CheatCategory category, string message)
|
||||
{
|
||||
return new ValueTask<bool>(false);
|
||||
}
|
||||
|
||||
public ValueTask<bool> ReportCheatAsync(CheatContext context, string message)
|
||||
{
|
||||
return ReportCheatAsync(context, CheatCategory.Other, message);
|
||||
}
|
||||
|
||||
public abstract ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType);
|
||||
|
||||
public abstract ValueTask HandleDisconnectAsync(string reason);
|
||||
@@ -54,5 +62,20 @@ namespace Impostor.Server.Net
|
||||
{
|
||||
await Connection.CustomDisconnectAsync(reason, message);
|
||||
}
|
||||
|
||||
public bool Equals(IClient? other)
|
||||
{
|
||||
return other != null && Id == other.Id;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return Equals(obj as ClientBase);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,11 @@ namespace Impostor.Server.Net.Hazel
|
||||
{
|
||||
await Client.HandleMessageAsync(message, e.Type);
|
||||
}
|
||||
|
||||
if (!IsConnected)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Impostor.Server.Net.Inner
|
||||
{
|
||||
if (!sender.IsOwner(this))
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, $"Failed ownership check on {GetType().Name}"))
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Ownership, $"Failed ownership check on {GetType().Name}"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace Impostor.Server.Net.Inner
|
||||
{
|
||||
if (!sender.IsHost)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, "Failed host check"))
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.MustBeHost, "Failed host check"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace Impostor.Server.Net.Inner
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, "Failed target check"))
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Target, "Failed target check"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ namespace Impostor.Server.Net.Inner
|
||||
{
|
||||
if (target != null)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, "Failed broadcast check"))
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Target, "Failed broadcast check"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -64,7 +64,7 @@ namespace Impostor.Server.Net.Inner
|
||||
{
|
||||
if (target == null || !target.IsHost)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, "Failed cmd check"))
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Target, "Failed cmd check"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -73,11 +73,18 @@ namespace Impostor.Server.Net.Inner
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateImpostor(CheatContext context, IClientPlayer sender, InnerPlayerInfo playerInfo, bool value = true)
|
||||
protected async ValueTask<bool> ValidateImpostor(CheatContext context, IClientPlayer sender, InnerPlayerInfo? playerInfo, bool value = true)
|
||||
{
|
||||
if (playerInfo.IsImpostor != value)
|
||||
if (playerInfo == null)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, "Failed impostor check"))
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.InvalidObject, "Couldn't check if Impostor, playerInfo not set"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (playerInfo.IsImpostor != value)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, "Failed impostor check"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -86,11 +93,18 @@ namespace Impostor.Server.Net.Inner
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateCanVent(CheatContext context, IClientPlayer sender, InnerPlayerInfo playerInfo, bool value = true)
|
||||
protected async ValueTask<bool> ValidateCanVent(CheatContext context, IClientPlayer sender, InnerPlayerInfo? playerInfo, bool value = true)
|
||||
{
|
||||
if (playerInfo.CanVent != value)
|
||||
if (playerInfo == null)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, "Failed can vent check"))
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.InvalidObject, "Couldn't check if can vent, playerInfo not set"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (playerInfo.CanVent != value)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, "Failed can vent check"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -99,11 +113,18 @@ namespace Impostor.Server.Net.Inner
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async ValueTask<bool> ValidateRole(CheatContext context, IClientPlayer sender, InnerPlayerInfo playerInfo, RoleTypes role)
|
||||
protected async ValueTask<bool> ValidateRole(CheatContext context, IClientPlayer sender, InnerPlayerInfo? playerInfo, RoleTypes role)
|
||||
{
|
||||
if (playerInfo.RoleType != role)
|
||||
if (playerInfo == null)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, $"Failed role = {role} check"))
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.InvalidObject, "Couldn't check role, playerInfo not set"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (playerInfo.RoleType != role)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, $"Failed role = {role} check"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -114,7 +135,7 @@ namespace Impostor.Server.Net.Inner
|
||||
|
||||
protected async ValueTask<bool> UnregisteredCall(CheatContext context, IClientPlayer sender)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(context, "Client sent unregistered call"))
|
||||
if (await sender.Client.ReportCheatAsync(context, CheatCategory.ProtocolExtension, "Client sent unregistered call"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api;
|
||||
using Impostor.Api.Events.Managers;
|
||||
using Impostor.Api.Net;
|
||||
using Impostor.Api.Net.Custom;
|
||||
@@ -36,17 +37,13 @@ namespace Impostor.Server.Net.Inner.Objects.Components
|
||||
|
||||
public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
|
||||
{
|
||||
if (call != RpcCalls.BootFromVent && !await ValidateOwnership(call, sender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (call)
|
||||
{
|
||||
case RpcCalls.EnterVent:
|
||||
case RpcCalls.ExitVent:
|
||||
{
|
||||
if (!await ValidateCanVent(call, sender, _playerControl.PlayerInfo))
|
||||
if (!await ValidateOwnership(call, sender) ||
|
||||
!await ValidateCanVent(call, sender, _playerControl.PlayerInfo))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -67,7 +64,7 @@ namespace Impostor.Server.Net.Inner.Objects.Components
|
||||
|
||||
if (Game.GameNet.ShipStatus == null)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(call, "Client interacted with vent on unknown map"))
|
||||
if (await sender.Client.ReportCheatAsync(call, CheatCategory.ProtocolExtension, "Client interacted with vent on unknown map"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -77,7 +74,7 @@ namespace Impostor.Server.Net.Inner.Objects.Components
|
||||
|
||||
if (!Game.GameNet.ShipStatus.Data.Vents.TryGetValue(ventId, out var vent))
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(call, "Client interacted with nonexistent vent"))
|
||||
if (await sender.Client.ReportCheatAsync(call, CheatCategory.ProtocolExtension, "Client interacted with nonexistent vent"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -105,17 +102,34 @@ namespace Impostor.Server.Net.Inner.Objects.Components
|
||||
}
|
||||
|
||||
case RpcCalls.ClimbLadder:
|
||||
{
|
||||
if (!await ValidateOwnership(call, sender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Rpc31ClimbLadder.Deserialize(reader, out var ladderId, out var lastClimbLadderSid);
|
||||
break;
|
||||
}
|
||||
|
||||
case RpcCalls.Pet:
|
||||
{
|
||||
if (!await ValidateOwnership(call, sender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Rpc49Pet.Deserialize(reader, out var position, out var petPosition);
|
||||
break;
|
||||
}
|
||||
|
||||
case RpcCalls.CancelPet:
|
||||
{
|
||||
if (!await ValidateOwnership(call, sender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Rpc50CancelPet.Deserialize(reader);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Impostor.Server.Net.Inner.Objects.Components
|
||||
{
|
||||
_logger = logger;
|
||||
_votes = new Dictionary<int, int[]>();
|
||||
Components.Add(this);
|
||||
}
|
||||
|
||||
public override ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
|
||||
@@ -69,7 +70,7 @@ namespace Impostor.Server.Net.Inner.Objects.Components
|
||||
|
||||
if (clientId != sender.Client.Id)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(RpcCalls.AddVote, $"Client sent {nameof(RpcCalls.AddVote)} as other client"))
|
||||
if (await sender.Client.ReportCheatAsync(RpcCalls.AddVote, CheatCategory.Ownership, $"Client sent {nameof(RpcCalls.AddVote)} as other client"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -53,12 +53,20 @@ internal abstract class InnerGameManager : InnerNetObject, IInnerGameManager
|
||||
|
||||
public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
|
||||
{
|
||||
// Let all logic components process this RPC. If at least one component handles it, return true.
|
||||
var result = false;
|
||||
foreach (var logicComponent in _logicComponents)
|
||||
{
|
||||
await logicComponent.HandleRpcAsync(call, reader);
|
||||
result |= await logicComponent.HandleRpcAsync(call, reader);
|
||||
}
|
||||
|
||||
return true;
|
||||
// If no component accepted it, try and find a custom RPC that can deal with it.
|
||||
if (!result)
|
||||
{
|
||||
result |= await base.HandleRpcAsync(sender, target, call, reader);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace Impostor.Server.Net.Inner.Objects.GameManager.Logic;
|
||||
|
||||
internal abstract class GameLogicComponent
|
||||
{
|
||||
public virtual ValueTask HandleRpcAsync(RpcCalls callId, IMessageReader reader)
|
||||
public virtual ValueTask<bool> HandleRpcAsync(RpcCalls callId, IMessageReader reader)
|
||||
{
|
||||
throw new NotImplementedException($"Unhandled RpcCall {callId}");
|
||||
return ValueTask.FromResult(false);
|
||||
}
|
||||
|
||||
public virtual ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api;
|
||||
using Impostor.Api.Net.Inner;
|
||||
using Impostor.Api.Net.Messages.Rpcs;
|
||||
using Impostor.Server.Events.Player;
|
||||
|
||||
namespace Impostor.Server.Net.Inner.Objects
|
||||
{
|
||||
internal partial class InnerGameData
|
||||
{
|
||||
public partial class TaskInfo
|
||||
{
|
||||
public async ValueTask CompleteAsync()
|
||||
{
|
||||
if (_playerInfo.Controller == null)
|
||||
{
|
||||
throw new ImpostorException("Can't complete a task that doesn't have a player assigned");
|
||||
}
|
||||
|
||||
var player = _playerInfo.Controller;
|
||||
|
||||
if (Complete)
|
||||
{
|
||||
throw new ImpostorException("Can't complete a task that is already completed");
|
||||
}
|
||||
|
||||
Complete = true;
|
||||
|
||||
// Send RPC.
|
||||
using var writer = player.Game.StartRpc(player.NetId, RpcCalls.CompleteTask);
|
||||
Rpc01CompleteTask.Serialize(writer, Id);
|
||||
await player.Game.FinishRpcAsync(writer);
|
||||
|
||||
// Notify plugins.
|
||||
await _eventManager.CallAsync(new PlayerCompletedTaskEvent(player.Game, player.Game.GetClientPlayer(player.OwnerId)!, player, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using Impostor.Api.Events.Managers;
|
||||
using Impostor.Api.Innersloth.Maps;
|
||||
using Impostor.Api.Net.Inner.Objects;
|
||||
|
||||
namespace Impostor.Server.Net.Inner.Objects
|
||||
{
|
||||
internal partial class InnerGameData
|
||||
{
|
||||
public partial class TaskInfo : ITaskInfo
|
||||
{
|
||||
private readonly InnerPlayerInfo _playerInfo;
|
||||
private readonly IEventManager _eventManager;
|
||||
|
||||
public TaskInfo(InnerPlayerInfo playerInfo, IEventManager eventManager, uint id, TaskData? task)
|
||||
{
|
||||
_playerInfo = playerInfo;
|
||||
_eventManager = eventManager;
|
||||
Id = id;
|
||||
Task = task;
|
||||
}
|
||||
|
||||
public uint Id { get; internal set; }
|
||||
|
||||
public TaskData? Task { get; internal set; }
|
||||
|
||||
public bool Complete { get; internal set; }
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
{
|
||||
writer.WritePacked(Id);
|
||||
writer.Write(Complete);
|
||||
}
|
||||
|
||||
public void Deserialize(IMessageReader reader)
|
||||
{
|
||||
Id = reader.ReadPackedUInt32();
|
||||
Complete = reader.ReadBoolean();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api;
|
||||
using Impostor.Api.Events.Managers;
|
||||
using Impostor.Api.Net;
|
||||
using Impostor.Api.Net.Custom;
|
||||
using Impostor.Api.Net.Inner;
|
||||
using Impostor.Api.Net.Inner.Objects;
|
||||
using Impostor.Api.Net.Messages.Rpcs;
|
||||
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
|
||||
internal partial class InnerGameData : IInnerGameData
|
||||
{
|
||||
private readonly ILogger<InnerGameData> _logger;
|
||||
private readonly IEventManager _eventManager;
|
||||
private readonly ConcurrentDictionary<byte, InnerPlayerInfo> _allPlayers;
|
||||
|
||||
public InnerGameData(ICustomMessageManager<ICustomRpc> customMessageManager, Game game, ILogger<InnerGameData> logger, IEventManager eventManager, IServiceProvider serviceProvider) : base(customMessageManager, game)
|
||||
{
|
||||
_logger = logger;
|
||||
_eventManager = eventManager;
|
||||
_allPlayers = new ConcurrentDictionary<byte, InnerPlayerInfo>();
|
||||
|
||||
Components.Add(this);
|
||||
Components.Add(ActivatorUtilities.CreateInstance<InnerVoteBanSystem>(serviceProvider, game));
|
||||
}
|
||||
private readonly ConcurrentDictionary<byte, InnerPlayerInfo> _allPlayers = new();
|
||||
private readonly ConcurrentDictionary<int, InnerPlayerInfo> _allPlayersByClientId = new();
|
||||
|
||||
public int PlayerCount => _allPlayers.Count;
|
||||
|
||||
public IReadOnlyDictionary<byte, InnerPlayerInfo> Players => _allPlayers;
|
||||
|
||||
internal IReadOnlyDictionary<int, InnerPlayerInfo> PlayersByClientId => _allPlayersByClientId;
|
||||
|
||||
public InnerPlayerInfo? GetPlayerById(byte id)
|
||||
{
|
||||
if (id == byte.MaxValue)
|
||||
@@ -46,103 +25,33 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
return _allPlayers.TryGetValue(id, out var player) ? player : null;
|
||||
}
|
||||
|
||||
public override ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
|
||||
internal bool AddPlayer(InnerPlayerInfo playerInfo)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return _allPlayers.TryAdd(playerInfo.PlayerId, playerInfo) &&
|
||||
_allPlayersByClientId.TryAdd(playerInfo.ClientId, playerInfo);
|
||||
}
|
||||
|
||||
public override async ValueTask DeserializeAsync(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
|
||||
internal void RemovePlayer(byte playerId)
|
||||
{
|
||||
if (!await ValidateHost(CheatContext.Deserialize, sender))
|
||||
_allPlayers.TryRemove(playerId, out var player);
|
||||
|
||||
if (player != null)
|
||||
{
|
||||
return;
|
||||
_allPlayersByClientId.TryRemove(player.ClientId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
while (reader.Position < reader.Length)
|
||||
internal byte GetNextAvailablePlayerId()
|
||||
{
|
||||
for (byte i = 0; i < byte.MaxValue; i++)
|
||||
{
|
||||
var inner = reader.ReadMessage();
|
||||
var playerInfo = this.GetPlayerById(inner.Tag);
|
||||
if (playerInfo != null)
|
||||
if (!Players.ContainsKey(i))
|
||||
{
|
||||
playerInfo.Deserialize(inner);
|
||||
}
|
||||
else
|
||||
{
|
||||
playerInfo = new InnerPlayerInfo(inner.Tag);
|
||||
playerInfo.Deserialize(inner);
|
||||
|
||||
if (!_allPlayers.TryAdd(playerInfo.PlayerId, playerInfo))
|
||||
{
|
||||
throw new ImpostorException("Failed to add player to InnerGameData.");
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
|
||||
{
|
||||
if (!await ValidateHost(call, sender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (call)
|
||||
{
|
||||
case RpcCalls.SetTasks:
|
||||
{
|
||||
Rpc29SetTasks.Deserialize(reader, out var playerId, out var taskTypeIds);
|
||||
SetTasks(playerId, taskTypeIds);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return await base.HandleRpcAsync(sender, target, call, reader);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal InnerPlayerInfo? AddPlayer(InnerPlayerControl control)
|
||||
{
|
||||
var playerId = control.PlayerId;
|
||||
var playerInfo = new InnerPlayerInfo(control.PlayerId);
|
||||
|
||||
if (_allPlayers.TryAdd(playerId, playerInfo))
|
||||
{
|
||||
return playerInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal void RemovePlayer(InnerPlayerControl control)
|
||||
{
|
||||
_allPlayers.TryRemove(control.PlayerInfo.PlayerId, out _);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var taskId = 0u;
|
||||
foreach (var taskTypeId in taskTypeIds.Span)
|
||||
{
|
||||
var mapTasks = Game.GameNet!.ShipStatus?.Data.Tasks;
|
||||
var taskType = (mapTasks != null && mapTasks.ContainsKey(taskTypeId)) ? mapTasks[taskTypeId] : null;
|
||||
player.Tasks.Add(new TaskInfo(player, _eventManager, taskId++, taskType));
|
||||
}
|
||||
return byte.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
|
||||
if (playerId != sender.Character!.PlayerId)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(RpcCalls.CastVote, $"Client sent {nameof(RpcCalls.CastVote)} to an unowned {nameof(InnerPlayerControl)}"))
|
||||
if (await sender.Client.ReportCheatAsync(RpcCalls.CastVote, CheatCategory.Ownership, $"Client sent {nameof(RpcCalls.CastVote)} to an unowned {nameof(InnerPlayerControl)}"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -287,9 +287,9 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
var max = MaxPair(self, out var tie);
|
||||
var exiled = tie ? null : Game.GameNet.GameData!.GetPlayerById(max.Key)?.Controller;
|
||||
|
||||
if (exiled != null)
|
||||
if (exiled != null && exiled.PlayerInfo != null)
|
||||
{
|
||||
exiled.Die(DeathReason.Exile);
|
||||
exiled.PlayerInfo.LastDeathReason = DeathReason.Exile;
|
||||
await _eventManager.CallAsync(new PlayerExileEvent(Game, Game.GetClientPlayer(exiled!.OwnerId)!, exiled));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api;
|
||||
using Impostor.Api.Innersloth;
|
||||
@@ -16,50 +17,103 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
|
||||
IInnerCustomNetworkTransform IInnerPlayerControl.NetworkTransform => NetworkTransform;
|
||||
|
||||
IInnerPlayerInfo IInnerPlayerControl.PlayerInfo => PlayerInfo;
|
||||
IInnerPlayerInfo? IInnerPlayerControl.PlayerInfo => PlayerInfo;
|
||||
|
||||
public async ValueTask SetNameAsync(string name)
|
||||
{
|
||||
PlayerInfo.PlayerName = name;
|
||||
if (PlayerInfo == null)
|
||||
{
|
||||
throw new ImpostorProtocolException("Cannot set name, PlayerInfo is null");
|
||||
}
|
||||
|
||||
PlayerInfo.CurrentOutfit.PlayerName = name;
|
||||
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.SetName);
|
||||
writer.Write(name);
|
||||
Rpc06SetName.Serialize(writer, PlayerInfo.NetId, name);
|
||||
await Game.FinishRpcAsync(writer);
|
||||
}
|
||||
|
||||
public async ValueTask SetColorAsync(ColorType color)
|
||||
{
|
||||
if (PlayerInfo == null)
|
||||
{
|
||||
throw new ImpostorProtocolException("Cannot set color, PlayerInfo is null");
|
||||
}
|
||||
|
||||
PlayerInfo.CurrentOutfit.Color = color;
|
||||
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.SetColor);
|
||||
Rpc08SetColor.Serialize(writer, color);
|
||||
Rpc08SetColor.Serialize(writer, PlayerInfo.NetId, color);
|
||||
await Game.FinishRpcAsync(writer);
|
||||
}
|
||||
|
||||
public async ValueTask SetHatAsync(string hatId)
|
||||
{
|
||||
if (PlayerInfo == null)
|
||||
{
|
||||
throw new ImpostorProtocolException("Cannot set hat, PlayerInfo is null");
|
||||
}
|
||||
|
||||
PlayerInfo.CurrentOutfit.HatId = hatId;
|
||||
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.SetHat);
|
||||
Rpc39SetHatStr.Serialize(writer, hatId);
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.SetHatStr);
|
||||
Rpc39SetHatStr.Serialize(writer, hatId, PlayerInfo.GetNextRpcSequenceId(RpcCalls.SetHatStr));
|
||||
await Game.FinishRpcAsync(writer);
|
||||
}
|
||||
|
||||
public async ValueTask SetPetAsync(string petId)
|
||||
{
|
||||
if (PlayerInfo == null)
|
||||
{
|
||||
throw new ImpostorProtocolException("Cannot set pet, PlayerInfo is null");
|
||||
}
|
||||
|
||||
PlayerInfo.CurrentOutfit.PetId = petId;
|
||||
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.SetPet);
|
||||
Rpc41SetPetStr.Serialize(writer, petId);
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.SetPetStr);
|
||||
Rpc41SetPetStr.Serialize(writer, petId, PlayerInfo.GetNextRpcSequenceId(RpcCalls.SetPetStr));
|
||||
await Game.FinishRpcAsync(writer);
|
||||
}
|
||||
|
||||
public async ValueTask SetSkinAsync(string skinId)
|
||||
{
|
||||
if (PlayerInfo == null)
|
||||
{
|
||||
throw new ImpostorProtocolException("Cannot set skin, PlayerInfo is null");
|
||||
}
|
||||
|
||||
PlayerInfo.CurrentOutfit.SkinId = skinId;
|
||||
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.SetSkin);
|
||||
Rpc40SetSkinStr.Serialize(writer, skinId);
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.SetSkinStr);
|
||||
Rpc40SetSkinStr.Serialize(writer, skinId, PlayerInfo.GetNextRpcSequenceId(RpcCalls.SetSkinStr));
|
||||
await Game.FinishRpcAsync(writer);
|
||||
}
|
||||
|
||||
public async ValueTask SetVisorAsync(string visorId)
|
||||
{
|
||||
if (PlayerInfo == null)
|
||||
{
|
||||
throw new ImpostorProtocolException("Cannot set visor, PlayerInfo is null");
|
||||
}
|
||||
|
||||
PlayerInfo.CurrentOutfit.VisorId = visorId;
|
||||
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.SetVisorStr);
|
||||
Rpc42SetVisorStr.Serialize(writer, visorId, PlayerInfo.GetNextRpcSequenceId(RpcCalls.SetVisorStr));
|
||||
await Game.FinishRpcAsync(writer);
|
||||
}
|
||||
|
||||
public async ValueTask SetNamePlateAsync(string nameplateId)
|
||||
{
|
||||
if (PlayerInfo == null)
|
||||
{
|
||||
throw new ImpostorProtocolException("Cannot set nameplate, PlayerInfo is null");
|
||||
}
|
||||
|
||||
PlayerInfo.CurrentOutfit.NamePlateId = nameplateId;
|
||||
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.SetNamePlateStr);
|
||||
Rpc43SetNamePlateStr.Serialize(writer, nameplateId, PlayerInfo.GetNextRpcSequenceId(RpcCalls.SetNamePlateStr));
|
||||
await Game.FinishRpcAsync(writer);
|
||||
}
|
||||
|
||||
@@ -82,23 +136,53 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
await Game.FinishRpcAsync(writer, player.OwnerId);
|
||||
}
|
||||
|
||||
private bool ValidateMurderPlayer(IInnerPlayerControl target, MurderResultFlags result, [NotNullWhen(false)] out string? invalidReason)
|
||||
{
|
||||
if (PlayerInfo == null)
|
||||
{
|
||||
invalidReason = "Tried to murder a player, but the murderer didn't have a playerinfo";
|
||||
}
|
||||
else if (!PlayerInfo.IsImpostor)
|
||||
{
|
||||
invalidReason = "Tried to murder a player, but murderer was not an impostor.";
|
||||
}
|
||||
else if (PlayerInfo.IsDead)
|
||||
{
|
||||
invalidReason = "Tried to murder a player, but murderer was not alive.";
|
||||
}
|
||||
else if (target.PlayerInfo == null)
|
||||
{
|
||||
invalidReason = "Tried to murder a player, but the murderer didn't have a playerinfo";
|
||||
}
|
||||
else if (target.PlayerInfo.IsImpostor)
|
||||
{
|
||||
invalidReason = "Tried to murder a player, but target is an impostor";
|
||||
}
|
||||
else if (target.PlayerInfo.IsDead)
|
||||
{
|
||||
invalidReason = "Tried to murder a player, but target was not alive.";
|
||||
}
|
||||
else
|
||||
{
|
||||
invalidReason = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async ValueTask MurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result)
|
||||
{
|
||||
if (!PlayerInfo.IsImpostor)
|
||||
if (!ValidateMurderPlayer(target, result, out var reason))
|
||||
{
|
||||
throw new ImpostorProtocolException("Tried to murder a player, but murderer was not the impostor.");
|
||||
throw new ImpostorProtocolException(reason);
|
||||
}
|
||||
|
||||
if (PlayerInfo.IsDead)
|
||||
{
|
||||
throw new ImpostorProtocolException("Tried to murder a player, but murderer was not alive.");
|
||||
}
|
||||
|
||||
if (target.PlayerInfo.IsDead)
|
||||
{
|
||||
throw new ImpostorProtocolException("Tried to murder a player, but target was not alive.");
|
||||
}
|
||||
await ForceMurderPlayerAsync(target, result);
|
||||
}
|
||||
|
||||
public async ValueTask ForceMurderPlayerAsync(IInnerPlayerControl target, MurderResultFlags result)
|
||||
{
|
||||
if (!result.IsFailed())
|
||||
{
|
||||
((InnerPlayerControl)target).Die(DeathReason.Kill);
|
||||
@@ -118,25 +202,43 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
|
||||
public async ValueTask ProtectPlayerAsync(IInnerPlayerControl target)
|
||||
{
|
||||
if (target.PlayerInfo.RoleType == RoleTypes.GuardianAngel)
|
||||
if (target.PlayerInfo == null)
|
||||
{
|
||||
throw new ImpostorProtocolException("Tried to protect another Guardian Angel");
|
||||
throw new ImpostorProtocolException("Tried to exile a player, but target didn't have a playerinfo");
|
||||
}
|
||||
else if (target.PlayerInfo.IsDead)
|
||||
{
|
||||
throw new ImpostorProtocolException("Tried to protect a player that is dead");
|
||||
}
|
||||
|
||||
await ForceProtectPlayerAsync(target);
|
||||
}
|
||||
|
||||
public async ValueTask ForceProtectPlayerAsync(IInnerPlayerControl target)
|
||||
{
|
||||
((InnerPlayerControl)target).Protect(this);
|
||||
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.ProtectPlayer);
|
||||
Rpc45ProtectPlayer.Serialize(writer, target, PlayerInfo.CurrentOutfit.Color);
|
||||
Rpc45ProtectPlayer.Serialize(writer, target, PlayerInfo?.CurrentOutfit.Color ?? ColorType.Red);
|
||||
await Game.FinishRpcAsync(writer);
|
||||
}
|
||||
|
||||
public async ValueTask ExileAsync()
|
||||
{
|
||||
if (PlayerInfo.IsDead)
|
||||
if (PlayerInfo == null)
|
||||
{
|
||||
throw new ImpostorProtocolException("Tried to exile a player, but target didn't have a playerinfo");
|
||||
}
|
||||
else if (PlayerInfo.IsDead)
|
||||
{
|
||||
throw new ImpostorProtocolException("Tried to exile a player, but target was not alive.");
|
||||
}
|
||||
|
||||
await ForceExileAsync();
|
||||
}
|
||||
|
||||
public async ValueTask ForceExileAsync()
|
||||
{
|
||||
// Update player.
|
||||
Die(DeathReason.Exile);
|
||||
|
||||
@@ -148,5 +250,19 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
// Notify plugins.
|
||||
await _eventManager.CallAsync(new PlayerExileEvent(Game, Game.GetClientPlayer(OwnerId)!, this));
|
||||
}
|
||||
|
||||
public async ValueTask StartVanishAsync()
|
||||
{
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.StartVanish);
|
||||
Rpc63StartVanish.Serialize(writer);
|
||||
await Game.FinishRpcAsync(writer);
|
||||
}
|
||||
|
||||
public async ValueTask StartAppearAsync(bool shouldAnimate)
|
||||
{
|
||||
using var writer = Game.StartRpc(NetId, RpcCalls.StartAppear);
|
||||
Rpc65StartAppear.Serialize(writer, shouldAnimate);
|
||||
await Game.FinishRpcAsync(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,7 @@ using Impostor.Api.Net.Inner.Objects;
|
||||
|
||||
namespace Impostor.Server.Net.Inner.Objects
|
||||
{
|
||||
internal partial class InnerPlayerInfo : IInnerPlayerInfo
|
||||
internal partial class InnerPlayerInfo : InnerNetObject, IInnerPlayerInfo
|
||||
{
|
||||
IEnumerable<ITaskInfo> IInnerPlayerInfo.Tasks => Tasks;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api.Events.Managers;
|
||||
using Impostor.Api.Games;
|
||||
using Impostor.Api.Innersloth;
|
||||
using Impostor.Api.Innersloth.Customization;
|
||||
using Impostor.Api.Innersloth.GameOptions;
|
||||
using Impostor.Api.Net;
|
||||
using Impostor.Api.Net.Custom;
|
||||
using Impostor.Api.Net.Inner;
|
||||
using Impostor.Api.Net.Messages.Rpcs;
|
||||
using Impostor.Api.Utils;
|
||||
using Impostor.Server.Net.State;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Impostor.Server.Net.Inner.Objects
|
||||
{
|
||||
internal partial class InnerPlayerInfo
|
||||
{
|
||||
public InnerPlayerInfo(byte playerId)
|
||||
private readonly IEventManager _eventManager;
|
||||
private readonly ILogger<InnerPlayerInfo> _logger;
|
||||
|
||||
public InnerPlayerInfo(ICustomMessageManager<ICustomRpc> customMessageManager, IEventManager eventManager, Game game, ILogger<InnerPlayerInfo> logger) : base(customMessageManager, game)
|
||||
{
|
||||
PlayerId = playerId;
|
||||
Components.Add(this);
|
||||
_eventManager = eventManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public InnerPlayerControl? Controller { get; internal set; }
|
||||
|
||||
public byte PlayerId { get; }
|
||||
public byte PlayerId { get; internal set; }
|
||||
|
||||
public string PlayerName { get; internal set; } = string.Empty;
|
||||
public int ClientId { get; internal set; }
|
||||
|
||||
public string PlayerName => CurrentOutfit.PlayerName;
|
||||
|
||||
public Dictionary<PlayerOutfitType, PlayerOutfit> Outfits { get; } = new()
|
||||
{
|
||||
@@ -32,17 +47,19 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
|
||||
public RoleTypes? RoleType { get; internal set; }
|
||||
|
||||
public RoleTypes? RoleWhenAlive { get; internal set; }
|
||||
|
||||
public bool Disconnected { get; internal set; }
|
||||
|
||||
public bool IsImpostor => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.ImpostorGhost;
|
||||
public bool IsImpostor => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.ImpostorGhost or RoleTypes.Phantom;
|
||||
|
||||
public bool CanVent => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Engineer;
|
||||
public bool CanVent => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Phantom or RoleTypes.Engineer;
|
||||
|
||||
public bool IsDead { get; internal set; }
|
||||
|
||||
public DeathReason LastDeathReason { get; internal set; }
|
||||
|
||||
public List<InnerGameData.TaskInfo> Tasks { get; internal set; } = new List<InnerGameData.TaskInfo>(0);
|
||||
public List<TaskInfo> Tasks { get; internal set; } = new List<TaskInfo>(0);
|
||||
|
||||
public DateTimeOffset LastMurder { get; set; }
|
||||
|
||||
@@ -71,13 +88,56 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
}
|
||||
}
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
public override ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
writer.Write(PlayerId);
|
||||
writer.WritePacked(ClientId);
|
||||
|
||||
writer.Write((byte)Outfits.Count);
|
||||
foreach (var outfit in Outfits)
|
||||
{
|
||||
writer.Write((byte)outfit.Key);
|
||||
outfit.Value.Serialize(writer);
|
||||
}
|
||||
|
||||
writer.WritePacked(PlayerLevel);
|
||||
|
||||
var flag = 0;
|
||||
if (Disconnected)
|
||||
{
|
||||
flag = (byte)(flag | 1u);
|
||||
}
|
||||
|
||||
if (IsDead)
|
||||
{
|
||||
flag = (byte)(flag | 4u);
|
||||
}
|
||||
|
||||
writer.Write((byte)flag);
|
||||
|
||||
writer.Write((ushort)(RoleType ?? 0));
|
||||
writer.Write(RoleWhenAlive.HasValue);
|
||||
if (RoleWhenAlive.HasValue)
|
||||
{
|
||||
writer.Write((ushort)RoleWhenAlive.Value);
|
||||
}
|
||||
|
||||
writer.Write((byte)Tasks.Count);
|
||||
for (var i = 0; i < Tasks.Count; i++)
|
||||
{
|
||||
Tasks[i].Serialize(writer);
|
||||
}
|
||||
|
||||
writer.Write(string.Empty); // FriendCode
|
||||
writer.Write(string.Empty); // PUID
|
||||
return new ValueTask<bool>(true);
|
||||
}
|
||||
|
||||
public void Deserialize(IMessageReader reader)
|
||||
public override ValueTask DeserializeAsync(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
|
||||
{
|
||||
PlayerId = reader.ReadByte();
|
||||
ClientId = reader.ReadPackedInt32();
|
||||
|
||||
Outfits.Clear();
|
||||
var b = reader.ReadByte();
|
||||
for (var i = 0; i < b; i++)
|
||||
@@ -93,7 +153,15 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
Disconnected = (flag & 1) != 0;
|
||||
IsDead = (flag & 4) != 0;
|
||||
|
||||
_ = (RoleTypes)reader.ReadUInt16(); // ignore the RoleType here and only trust the SetRole rpc
|
||||
// Ignore the RoleType here and only trust the SetRole RPC, as
|
||||
// RoleType is not nullable in vanilla, while Impostor checks game
|
||||
// starts based on assigned roles.
|
||||
_ = (RoleTypes)reader.ReadUInt16();
|
||||
|
||||
if (reader.ReadBoolean())
|
||||
{
|
||||
RoleWhenAlive = (RoleTypes)reader.ReadUInt16();
|
||||
}
|
||||
|
||||
var taskCount = reader.ReadByte();
|
||||
|
||||
@@ -106,6 +174,87 @@ namespace Impostor.Server.Net.Inner.Objects
|
||||
{
|
||||
Tasks[i].Deserialize(reader);
|
||||
}
|
||||
|
||||
// Impostor doesn't expose fields that aren't properly validated
|
||||
reader.ReadString(); // FriendCode
|
||||
reader.ReadString(); // PUID
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
|
||||
{
|
||||
switch (call)
|
||||
{
|
||||
case RpcCalls.SetTasks:
|
||||
Rpc29SetTasks.Deserialize(reader, out var taskTypeIds);
|
||||
SetTasks(taskTypeIds);
|
||||
break;
|
||||
|
||||
default:
|
||||
return await base.HandleRpcAsync(sender, target, call, reader);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetTasks(ReadOnlyMemory<byte> taskTypeIds)
|
||||
{
|
||||
if (Disconnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Tasks = new List<TaskInfo>(taskTypeIds.Length);
|
||||
|
||||
var taskId = 0u;
|
||||
foreach (var taskTypeId in taskTypeIds.Span)
|
||||
{
|
||||
var mapTasks = Game.GameNet!.ShipStatus?.Data.Tasks;
|
||||
var taskType = (mapTasks != null && mapTasks.ContainsKey(taskTypeId)) ? mapTasks[taskTypeId] : null;
|
||||
Tasks.Add(new TaskInfo(this, _eventManager, taskId++, taskType));
|
||||
}
|
||||
}
|
||||
|
||||
public byte GetNextRpcSequenceId(RpcCalls rpc)
|
||||
{
|
||||
var defaultOutfit = this.Outfits[PlayerOutfitType.Default];
|
||||
|
||||
byte nextRpcSequenceId = 0;
|
||||
const byte SequenceIdAddition = 5;
|
||||
|
||||
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
||||
switch (rpc)
|
||||
{
|
||||
case RpcCalls.SetHatStr:
|
||||
defaultOutfit.HatSequenceId += SequenceIdAddition;
|
||||
nextRpcSequenceId = defaultOutfit.HatSequenceId;
|
||||
break;
|
||||
case RpcCalls.SetSkinStr:
|
||||
defaultOutfit.SkinSequenceId += SequenceIdAddition;
|
||||
nextRpcSequenceId = defaultOutfit.SkinSequenceId;
|
||||
break;
|
||||
case RpcCalls.SetPetStr:
|
||||
defaultOutfit.PetSequenceId += SequenceIdAddition;
|
||||
nextRpcSequenceId = defaultOutfit.PetSequenceId;
|
||||
break;
|
||||
case RpcCalls.SetVisorStr:
|
||||
defaultOutfit.VisorSequenceId += SequenceIdAddition;
|
||||
nextRpcSequenceId = defaultOutfit.VisorSequenceId;
|
||||
break;
|
||||
case RpcCalls.SetNamePlateStr:
|
||||
defaultOutfit.NamePlateSequenceId += SequenceIdAddition;
|
||||
nextRpcSequenceId = defaultOutfit.NamePlateSequenceId;
|
||||
break;
|
||||
default:
|
||||
_logger.LogError(
|
||||
"{NextRpcSequenceIdName} was called for an RPC it doesn\'t know how to handle: {Rpc}",
|
||||
nameof(GetNextRpcSequenceId),
|
||||
rpc);
|
||||
break;
|
||||
}
|
||||
|
||||
return nextRpcSequenceId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,16 +74,12 @@ namespace Impostor.Server.Net.Inner.Objects.ShipStatus
|
||||
|
||||
public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
|
||||
{
|
||||
if (!await ValidateCmd(call, sender, target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (call)
|
||||
{
|
||||
case RpcCalls.CloseDoorsOfType:
|
||||
{
|
||||
if (!await ValidateImpostor(call, sender, sender.Character!.PlayerInfo))
|
||||
if (!await ValidateCmd(call, sender, target) ||
|
||||
!await ValidateImpostor(call, sender, sender.Character!.PlayerInfo))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -94,6 +90,11 @@ namespace Impostor.Server.Net.Inner.Objects.ShipStatus
|
||||
|
||||
case RpcCalls.UpdateSystem:
|
||||
{
|
||||
if (!await ValidateCmd(call, sender, target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: properly deserialize this RPC
|
||||
// Rpc35UpdateSystem.Deserialize(reader, Game, out var systemType, out var playerControl, out var sequenceId, out var state, out var ventId);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api;
|
||||
using Impostor.Api.Events.Managers;
|
||||
using Impostor.Api.Innersloth.Maps;
|
||||
using Impostor.Api.Net.Inner;
|
||||
using Impostor.Api.Net.Inner.Objects;
|
||||
using Impostor.Api.Net.Messages.Rpcs;
|
||||
using Impostor.Server.Events.Player;
|
||||
|
||||
namespace Impostor.Server.Net.Inner.Objects
|
||||
{
|
||||
internal class TaskInfo : ITaskInfo
|
||||
{
|
||||
private readonly InnerPlayerInfo _playerInfo;
|
||||
private readonly IEventManager _eventManager;
|
||||
|
||||
public TaskInfo(InnerPlayerInfo playerInfo, IEventManager eventManager, uint id, TaskData? task)
|
||||
{
|
||||
_playerInfo = playerInfo;
|
||||
_eventManager = eventManager;
|
||||
Id = id;
|
||||
Task = task;
|
||||
}
|
||||
|
||||
public uint Id { get; internal set; }
|
||||
|
||||
public TaskData? Task { get; internal set; }
|
||||
|
||||
public bool Complete { get; internal set; }
|
||||
|
||||
public void Serialize(IMessageWriter writer)
|
||||
{
|
||||
writer.WritePacked(Id);
|
||||
writer.Write(Complete);
|
||||
}
|
||||
|
||||
public void Deserialize(IMessageReader reader)
|
||||
{
|
||||
Id = reader.ReadPackedUInt32();
|
||||
Complete = reader.ReadBoolean();
|
||||
}
|
||||
|
||||
public async ValueTask CompleteAsync()
|
||||
{
|
||||
if (_playerInfo.Controller == null)
|
||||
{
|
||||
throw new ImpostorException("Can't complete a task that doesn't have a player assigned");
|
||||
}
|
||||
|
||||
var player = _playerInfo.Controller;
|
||||
|
||||
if (Complete)
|
||||
{
|
||||
throw new ImpostorException("Can't complete a task that is already completed");
|
||||
}
|
||||
|
||||
Complete = true;
|
||||
|
||||
// Send RPC.
|
||||
using var writer = player.Game.StartRpc(player.NetId, RpcCalls.CompleteTask);
|
||||
Rpc01CompleteTask.Serialize(writer, Id);
|
||||
await player.Game.FinishRpcAsync(writer);
|
||||
|
||||
// Notify plugins.
|
||||
await _eventManager.CallAsync(new PlayerCompletedTaskEvent(player.Game, player.Game.GetClientPlayer(player.OwnerId)!, player, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,28 @@ namespace Impostor.Server.Net.Manager
|
||||
_clients = new ConcurrentDictionary<int, ClientBase>();
|
||||
_compatibilityManager = compatibilityManager;
|
||||
_compatibilityConfig = compatibilityConfig.Value;
|
||||
|
||||
if (_compatibilityConfig.AllowFutureGameVersions
|
||||
|| _compatibilityConfig.AllowHostAuthority
|
||||
|| _compatibilityConfig.AllowVersionMixing)
|
||||
{
|
||||
_logger.LogWarning("One or more compatibility options were enabled, please mention these when seeking support:");
|
||||
|
||||
if (_compatibilityConfig.AllowFutureGameVersions)
|
||||
{
|
||||
_logger.LogWarning("AllowFutureGameVersions, which allows future Among Us versions to connect that were unknown at the time this Impostor was built");
|
||||
}
|
||||
|
||||
if (_compatibilityConfig.AllowHostAuthority)
|
||||
{
|
||||
_logger.LogWarning("AllowHostAuthority, which allows game hosts to control more game features, but it uses less well tested code on the client, which causes some bugs");
|
||||
}
|
||||
|
||||
if (_compatibilityConfig.AllowVersionMixing)
|
||||
{
|
||||
_logger.LogWarning("AllowVersionMixing, which allows players to join games created on different game versions that they may not be 100% compatible with");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ClientBase> Clients => _clients.Values;
|
||||
@@ -79,6 +101,20 @@ namespace Impostor.Server.Net.Manager
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn when players connect using the +25 flag that disables server authority. The host authority version
|
||||
// of 2024.6.18 contains various bugs that break the game, so print a message if this mode is in use
|
||||
if (clientVersion.HasDisableServerAuthorityFlag)
|
||||
{
|
||||
if (!_compatibilityConfig.AllowHostAuthority)
|
||||
{
|
||||
_logger.LogInformation("Player {Name} kicked because they requested host authority.", name);
|
||||
await connection.CustomDisconnectAsync(DisconnectReason.Custom, DisconnectMessages.HostAuthorityUnsupported);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogWarning("Player {Name} connected with server authority disabled, this may cause issues as there are known bugs in this mode. Please mention that this mode is in use when asking for support.", name);
|
||||
}
|
||||
|
||||
if (name.Length > 10)
|
||||
{
|
||||
await connection.CustomDisconnectAsync(DisconnectReason.Custom, DisconnectMessages.UsernameLength);
|
||||
|
||||
@@ -17,12 +17,22 @@ internal class CompatibilityManager : ICompatibilityManager
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new GameVersion(2023, 10, 1), // 2023.10.24
|
||||
new GameVersion(2023, 11, 15), // 2023.11.28
|
||||
new GameVersion(2024, 3, 1), // 2024.6.18
|
||||
new GameVersion(2024, 4, 1), // 2024.8.13
|
||||
new GameVersion(2024, 4, 2), // 2024.9.4
|
||||
new GameVersion(2024, 8, 10), // 2024.10.29
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new GameVersion(2024, 2, 1), // 2024.3.5
|
||||
new GameVersion(2024, 8, 11), // 16.0.0 (2025-03-25)
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new GameVersion(2025, 4, 15), // 16.0.5 (2025-05-20)
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new GameVersion(2025, 7, 15), // 17.0.0 (2025-09-09)
|
||||
},
|
||||
};
|
||||
|
||||
@@ -62,6 +72,12 @@ internal class CompatibilityManager : ICompatibilityManager
|
||||
return null;
|
||||
}
|
||||
|
||||
private CompatibilityGroup GetCompatibilityGroupOrDefault(GameVersion clientVersion)
|
||||
{
|
||||
// If the compatibility group is not defined, we assume it is not compatible with anything else than itself
|
||||
return TryGetCompatibilityGroup(clientVersion) ?? new CompatibilityGroup(new[] { clientVersion.Normalize() });
|
||||
}
|
||||
|
||||
public VersionCompareResult CanConnectToServer(GameVersion clientVersion)
|
||||
{
|
||||
if (this.TryGetCompatibilityGroup(clientVersion) != null)
|
||||
@@ -90,13 +106,8 @@ internal class CompatibilityManager : ICompatibilityManager
|
||||
return GameJoinError.None;
|
||||
}
|
||||
|
||||
var hostCompatGroup = this.TryGetCompatibilityGroup(hostVersion);
|
||||
var playerCompatGroup = this.TryGetCompatibilityGroup(clientVersion);
|
||||
|
||||
if (hostCompatGroup == null || playerCompatGroup == null)
|
||||
{
|
||||
return GameJoinError.InvalidClient;
|
||||
}
|
||||
var hostCompatGroup = GetCompatibilityGroupOrDefault(hostVersion);
|
||||
var playerCompatGroup = GetCompatibilityGroupOrDefault(clientVersion);
|
||||
|
||||
if (hostCompatGroup != playerCompatGroup)
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Impostor.Server.Net.Manager
|
||||
private readonly IEventManager _eventManager;
|
||||
private readonly IGameCodeFactory _gameCodeFactory;
|
||||
private readonly ICompatibilityManager _compatibilityManager;
|
||||
private readonly ConcurrentDictionary<IClient, Game?> _gamesCreatedBy;
|
||||
|
||||
public GameManager(
|
||||
ILogger<GameManager> logger,
|
||||
@@ -49,6 +50,7 @@ namespace Impostor.Server.Net.Manager
|
||||
_games = new ConcurrentDictionary<int, Game>();
|
||||
_compatibilityConfig = compatibilityConfig.Value;
|
||||
_compatibilityManager = compatibilityManager;
|
||||
_gamesCreatedBy = new ConcurrentDictionary<IClient, Game?>();
|
||||
}
|
||||
|
||||
IEnumerable<IGame> IGameManager.Games => _games.Select(kv => kv.Value);
|
||||
@@ -85,6 +87,12 @@ namespace Impostor.Server.Net.Manager
|
||||
|
||||
public async ValueTask<IGame?> CreateAsync(IClient? owner, IGameOptions options, GameFilterOptions filterOptions)
|
||||
{
|
||||
if (owner != null && !_gamesCreatedBy.TryAdd(owner, null))
|
||||
{
|
||||
_logger.LogWarning("Connection {Name}({ClientId}) has tried to create a second game, blocked", owner.Name, owner.Id);
|
||||
return null;
|
||||
}
|
||||
|
||||
var @event = new GameCreationEvent(this, owner);
|
||||
await _eventManager.CallAsync(@event);
|
||||
|
||||
@@ -93,11 +101,16 @@ namespace Impostor.Server.Net.Manager
|
||||
return null;
|
||||
}
|
||||
|
||||
var (success, game) = await TryCreateAsync(options, filterOptions, @event.GameCode);
|
||||
var (success, game) = await TryCreateAsync(options, filterOptions, owner, @event.GameCode);
|
||||
|
||||
for (var i = 0; i < 10 && !success; i++)
|
||||
{
|
||||
(success, game) = await TryCreateAsync(options, filterOptions);
|
||||
(success, game) = await TryCreateAsync(options, filterOptions, owner);
|
||||
}
|
||||
|
||||
if (owner != null)
|
||||
{
|
||||
_gamesCreatedBy[owner] = game;
|
||||
}
|
||||
|
||||
if (!success || game == null)
|
||||
@@ -113,7 +126,7 @@ namespace Impostor.Server.Net.Manager
|
||||
return CreateAsync(null, options, filterOptions);
|
||||
}
|
||||
|
||||
private async ValueTask<(bool Success, Game? Game)> TryCreateAsync(IGameOptions options, GameFilterOptions filterOptions, GameCode? desiredGameCode = null)
|
||||
private async ValueTask<(bool Success, Game? Game)> TryCreateAsync(IGameOptions options, GameFilterOptions filterOptions, IClient? owner, GameCode? desiredGameCode = null)
|
||||
{
|
||||
var gameCode = desiredGameCode ?? _gameCodeFactory.Create();
|
||||
var game = ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options, filterOptions);
|
||||
@@ -125,9 +138,18 @@ namespace Impostor.Server.Net.Manager
|
||||
|
||||
_logger.LogDebug("Created game with code {0}.", game.Code);
|
||||
|
||||
await _eventManager.CallAsync(new GameCreatedEvent(game));
|
||||
await _eventManager.CallAsync(new GameCreatedEvent(game, owner));
|
||||
|
||||
return (true, game);
|
||||
}
|
||||
|
||||
internal async ValueTask OnClientDisconnectAsync(IClient client)
|
||||
{
|
||||
if (_gamesCreatedBy.TryRemove(client, out var game) && game is { PlayerCount: 0, GameState: not GameStates.Destroyed })
|
||||
{
|
||||
_logger.LogWarning("Client {Name}({ClientId}) left empty game open when disconnecting", client.Name, client.Id);
|
||||
await RemoveAsync(game.Code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ namespace Impostor.Server.Net
|
||||
_serverConfig.ResolvePublicIp(),
|
||||
_serverConfig.PublicPort);
|
||||
|
||||
var runningOutsideContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == null;
|
||||
if (_serverConfig.PublicIp == "127.0.0.1")
|
||||
{
|
||||
// NOTE: If this warning annoys you, set your PublicIp to "localhost"
|
||||
@@ -48,11 +49,9 @@ namespace Impostor.Server.Net
|
||||
_logger.LogError("To allow people on other devices to connect to your server, change this value to your Public IP address");
|
||||
_logger.LogError("For more info on how to do this see https://github.com/Impostor/Impostor/blob/master/docs/Server-configuration.md");
|
||||
}
|
||||
|
||||
var runningOutsideContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == null;
|
||||
if (_httpServerConfig.ListenIp == "0.0.0.0" && runningOutsideContainer)
|
||||
else if (_httpServerConfig.ListenIp == "0.0.0.0" && runningOutsideContainer)
|
||||
{
|
||||
_logger.LogWarning("Your HTTP server is exposed to the public internet, we recommend setting up a reverse proxy and enabling HTTPS");
|
||||
_logger.LogWarning("Since Among Us 16.0.5 it is required to support HTTPS for players to connect, we recommend setting up a reverse proxy:");
|
||||
_logger.LogWarning("See https://github.com/Impostor/Impostor/blob/master/docs/Http-server.md for instructions");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api;
|
||||
using Impostor.Api.Innersloth;
|
||||
@@ -30,12 +31,21 @@ namespace Impostor.Server.Net.State
|
||||
/// </summary>
|
||||
private const int CurrentClient = -3;
|
||||
|
||||
/// <summary>
|
||||
/// Used to list objects that are managed by the game server.
|
||||
/// </summary>
|
||||
private const int ServerOwned = -4;
|
||||
|
||||
/// <summary>
|
||||
/// The first NetId that is considered as a server owned Network ID that the client will not allocate by default.
|
||||
/// </summary>
|
||||
private const int MinServerNetId = 100000;
|
||||
|
||||
private static readonly Dictionary<uint, Type> SpawnableObjects = new()
|
||||
{
|
||||
[0] = typeof(InnerSkeldShipStatus),
|
||||
[1] = typeof(InnerMeetingHud),
|
||||
[2] = typeof(InnerLobbyBehaviour),
|
||||
[3] = typeof(InnerGameData),
|
||||
[4] = typeof(InnerPlayerControl),
|
||||
[5] = typeof(InnerMiraShipStatus),
|
||||
[6] = typeof(InnerPolusShipStatus),
|
||||
@@ -43,13 +53,19 @@ namespace Impostor.Server.Net.State
|
||||
[8] = typeof(InnerAirshipStatus),
|
||||
[9] = typeof(InnerHideAndSeekManager),
|
||||
[10] = typeof(InnerNormalGameManager),
|
||||
[11] = typeof(InnerPlayerInfo),
|
||||
[12] = typeof(InnerVoteBanSystem),
|
||||
[13] = typeof(InnerFungleShipStatus),
|
||||
};
|
||||
|
||||
private static readonly Dictionary<Type, uint> SpawnableObjectIds = SpawnableObjects.ToDictionary((i) => i.Value, (i) => i.Key);
|
||||
|
||||
private readonly List<InnerNetObject> _allObjects = new List<InnerNetObject>();
|
||||
|
||||
private readonly Dictionary<uint, InnerNetObject> _allObjectsFast = new Dictionary<uint, InnerNetObject>();
|
||||
|
||||
private uint _nextNetId = MinServerNetId;
|
||||
|
||||
public T? FindObjectByNetId<T>(uint netId)
|
||||
where T : IInnerNetObject
|
||||
{
|
||||
@@ -121,10 +137,10 @@ namespace Impostor.Server.Net.State
|
||||
|
||||
case GameDataTag.SpawnFlag:
|
||||
{
|
||||
// Only the host is allowed to despawn objects.
|
||||
// Only the host is allowed to spawn objects.
|
||||
if (!sender.IsHost)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.SpawnFlag)), "Tried to send SpawnFlag as non-host."))
|
||||
if (await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.SpawnFlag)), CheatCategory.MustBeHost, "Tried to send SpawnFlag as non-host."))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -230,6 +246,8 @@ namespace Impostor.Server.Net.State
|
||||
{
|
||||
// Sender is only allowed to change his own scene.
|
||||
var clientId = reader.ReadPackedInt32();
|
||||
var scene = reader.ReadString();
|
||||
|
||||
if (clientId != sender.Client.Id)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
@@ -239,15 +257,40 @@ namespace Impostor.Server.Net.State
|
||||
return false;
|
||||
}
|
||||
|
||||
sender.Scene = reader.ReadString();
|
||||
// According to game assembly, sender is only allowed to send OnlineGame.
|
||||
if (scene != "OnlineGame")
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Player {PlayerName} ({ClientId}) tried to send SceneChangeFlag with disallowed scene \"{Scene}\".",
|
||||
sender.Client.Name,
|
||||
sender.Client.Id,
|
||||
scene);
|
||||
return false;
|
||||
}
|
||||
|
||||
sender.Scene = scene;
|
||||
|
||||
_logger.LogTrace("> Scene {0} to {1}", clientId, sender.Scene);
|
||||
|
||||
await SyncServerObjectsAsync(sender);
|
||||
await SpawnPlayerInfoAsync(sender);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case GameDataTag.ReadyFlag:
|
||||
{
|
||||
var clientId = reader.ReadPackedInt32();
|
||||
|
||||
if (clientId != sender.Client.Id)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Player {0} ({1}) tried to send ReadyFlag for another player.",
|
||||
sender.Client.Name,
|
||||
sender.Client.Id);
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogTrace("> IsReady {0}", clientId);
|
||||
break;
|
||||
}
|
||||
@@ -259,7 +302,7 @@ namespace Impostor.Server.Net.State
|
||||
|
||||
if (clientId != sender.Client.Id)
|
||||
{
|
||||
if (await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.ConsoleDeclareClientPlatformFlag)), "Client sent info with wrong client id"))
|
||||
if (await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.ConsoleDeclareClientPlatformFlag)), CheatCategory.Ownership, "Client sent info with wrong client id"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -303,9 +346,17 @@ namespace Impostor.Server.Net.State
|
||||
break;
|
||||
}
|
||||
|
||||
case InnerGameData data:
|
||||
case InnerPlayerInfo playerInfo:
|
||||
{
|
||||
GameNet.GameData = data;
|
||||
if (!GameNet.GameData.AddPlayer(playerInfo))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Could not add PlayerInfo for playerId {PlayerId} with NetId {newId}, already have NetId {oldNetId}",
|
||||
playerInfo.PlayerId,
|
||||
playerInfo.NetId,
|
||||
GameNet.GameData.GetPlayerById(playerInfo.PlayerId)?.NetId);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -331,11 +382,11 @@ namespace Impostor.Server.Net.State
|
||||
}
|
||||
else
|
||||
{
|
||||
await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.SpawnFlag)), "Failed to find player that spawned the InnerPlayerControl");
|
||||
await sender.Client.ReportCheatAsync(new CheatContext(nameof(GameDataTag.SpawnFlag)), CheatCategory.GameFlow, "Failed to find player that spawned the InnerPlayerControl");
|
||||
}
|
||||
|
||||
// Hook up InnerPlayerControl <-> InnerPlayerControl.PlayerInfo.
|
||||
var playerInfo = GameNet.GameData!.GetPlayerById(control.PlayerId) ?? GameNet.GameData.AddPlayer(control);
|
||||
var playerInfo = GameNet.GameData.GetPlayerById(control.PlayerId);
|
||||
|
||||
if (playerInfo != null)
|
||||
{
|
||||
@@ -379,12 +430,6 @@ namespace Impostor.Server.Net.State
|
||||
break;
|
||||
}
|
||||
|
||||
case InnerGameData:
|
||||
{
|
||||
GameNet.GameData = null;
|
||||
break;
|
||||
}
|
||||
|
||||
case InnerVoteBanSystem:
|
||||
{
|
||||
GameNet.VoteBan = null;
|
||||
@@ -397,13 +442,18 @@ namespace Impostor.Server.Net.State
|
||||
break;
|
||||
}
|
||||
|
||||
case InnerPlayerControl control:
|
||||
case InnerPlayerInfo playerInfo:
|
||||
{
|
||||
if (GameState != GameStates.Started && GameState != GameStates.Starting)
|
||||
{
|
||||
GameNet.GameData?.RemovePlayer(control);
|
||||
GameNet.GameData.RemovePlayer(playerInfo.PlayerId);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case InnerPlayerControl control:
|
||||
{
|
||||
// Remove InnerPlayerControl <-> IClientPlayer.
|
||||
if (TryGetPlayer(control.OwnerId, out var player))
|
||||
{
|
||||
@@ -416,6 +466,71 @@ namespace Impostor.Server.Net.State
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask SyncServerObjectsAsync(ClientPlayer sender)
|
||||
{
|
||||
foreach (var obj in _allObjectsFast.Values)
|
||||
{
|
||||
if (obj.OwnerId == ServerOwned)
|
||||
{
|
||||
_logger.LogTrace("Syncing {Type} {NetId}", obj.GetType(), obj.NetId);
|
||||
await SendObjectSpawnAsync(obj, sender.Client.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask SpawnPlayerInfoAsync(ClientPlayer sender)
|
||||
{
|
||||
// Hosts spawn PlayerInfo objects if they requested authority
|
||||
if (IsHostAuthoritive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only spawn a new PlayerInfo if one has not yet been spawned
|
||||
if (GameNet.GameData.PlayersByClientId.ContainsKey(sender.Client.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playerInfo = (InnerPlayerInfo)ActivatorUtilities.CreateInstance(_serviceProvider, typeof(InnerPlayerInfo), this);
|
||||
playerInfo.SpawnFlags = SpawnFlags.None;
|
||||
playerInfo.NetId = _nextNetId++;
|
||||
playerInfo.OwnerId = ServerOwned;
|
||||
playerInfo.ClientId = sender.Client.Id;
|
||||
playerInfo.PlayerId = GameNet.GameData.GetNextAvailablePlayerId();
|
||||
|
||||
// If player played a previous game, restore their color
|
||||
var prevColor = sender.Client.PreviousColor;
|
||||
if (prevColor.HasValue)
|
||||
{
|
||||
_logger.LogTrace("Color restored to {Color}", prevColor.Value);
|
||||
playerInfo.CurrentOutfit.Color = prevColor.Value;
|
||||
}
|
||||
|
||||
if (!AddNetObject(playerInfo))
|
||||
{
|
||||
_logger.LogError("Couldn't spawn PlayerInfo for {Name} ({ClientId})", sender.Client.Name, sender.Client.Id);
|
||||
playerInfo.NetId = uint.MaxValue;
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Spawning PlayerInfo (netId {Netid})", playerInfo.NetId);
|
||||
await OnSpawnAsync(sender, playerInfo);
|
||||
await SendObjectSpawnAsync(playerInfo);
|
||||
}
|
||||
|
||||
private async ValueTask DespawnPlayerInfoAsync(InnerPlayerInfo playerInfo)
|
||||
{
|
||||
if (playerInfo.OwnerId == ServerOwned)
|
||||
{
|
||||
_logger.LogDebug("Despawning PlayerInfo {nid}", playerInfo.NetId);
|
||||
GameNet.GameData.RemovePlayer(playerInfo.PlayerId);
|
||||
RemoveNetObject(playerInfo);
|
||||
|
||||
await SendObjectDespawnAsync(playerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private bool AddNetObject(InnerNetObject obj)
|
||||
{
|
||||
if (_allObjectsFast.ContainsKey(obj.NetId))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api.Games;
|
||||
@@ -43,6 +44,12 @@ namespace Impostor.Server.Net.State
|
||||
player.Value.Limbo = LimboStates.PreSpawn;
|
||||
}
|
||||
|
||||
// Delete all PlayerInfo objects
|
||||
foreach (var playerInfo in GameNet.GameData.Players.Values.ToArray())
|
||||
{
|
||||
await DespawnPlayerInfoAsync(playerInfo);
|
||||
}
|
||||
|
||||
await _eventManager.CallAsync(new GameEndedEvent(this, gameOverReason));
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,23 @@ namespace Impostor.Server.Net.State
|
||||
}
|
||||
|
||||
public IMessageWriter StartRpc(uint targetNetId, RpcCalls callId, int? targetClientId = null, MessageType type = MessageType.Reliable)
|
||||
{
|
||||
var writer = StartGameData(targetClientId, type);
|
||||
|
||||
writer.StartMessage(GameDataTag.RpcFlag);
|
||||
writer.WritePacked(targetNetId);
|
||||
writer.Write((byte)callId);
|
||||
|
||||
return writer;
|
||||
}
|
||||
|
||||
public ValueTask FinishRpcAsync(IMessageWriter writer, int? targetClientId = null)
|
||||
{
|
||||
writer.EndMessage();
|
||||
return FinishGameDataAsync(writer, targetClientId);
|
||||
}
|
||||
|
||||
public IMessageWriter StartGameData(int? targetClientId = null, MessageType type = MessageType.Reliable)
|
||||
{
|
||||
var writer = MessageWriter.Get(type);
|
||||
|
||||
@@ -54,17 +71,12 @@ namespace Impostor.Server.Net.State
|
||||
writer.WritePacked(targetClientId.Value);
|
||||
}
|
||||
|
||||
writer.StartMessage(GameDataTag.RpcFlag);
|
||||
writer.WritePacked(targetNetId);
|
||||
writer.Write((byte)callId);
|
||||
|
||||
return writer;
|
||||
}
|
||||
|
||||
public ValueTask FinishRpcAsync(IMessageWriter writer, int? targetClientId = null)
|
||||
public ValueTask FinishGameDataAsync(IMessageWriter writer, int? targetClientId = null)
|
||||
{
|
||||
writer.EndMessage();
|
||||
writer.EndMessage();
|
||||
|
||||
return targetClientId.HasValue
|
||||
? SendToAsync(writer, targetClientId.Value)
|
||||
@@ -100,5 +112,36 @@ namespace Impostor.Server.Net.State
|
||||
{
|
||||
Message12WaitForHostS2C.Serialize(message, clear, Code, player.Client.Id);
|
||||
}
|
||||
|
||||
private async ValueTask SendObjectSpawnAsync(InnerNetObject obj, int? targetClientId = null)
|
||||
{
|
||||
using var writer = StartGameData(targetClientId);
|
||||
writer.StartMessage(GameDataTag.SpawnFlag);
|
||||
writer.WritePacked(SpawnableObjectIds[obj.GetType()]);
|
||||
writer.WritePacked(obj.OwnerId);
|
||||
writer.Write((byte)obj.SpawnFlags);
|
||||
|
||||
var components = obj.GetComponentsInChildren<InnerNetObject>();
|
||||
writer.WritePacked(components.Count);
|
||||
foreach (var component in components)
|
||||
{
|
||||
writer.WritePacked(obj.NetId);
|
||||
writer.StartMessage(1);
|
||||
await component.SerializeAsync(writer, true);
|
||||
writer.EndMessage();
|
||||
}
|
||||
|
||||
writer.EndMessage();
|
||||
await FinishGameDataAsync(writer, targetClientId);
|
||||
}
|
||||
|
||||
private async ValueTask SendObjectDespawnAsync(InnerNetObject obj, int? targetClientId = null)
|
||||
{
|
||||
using var writer = StartGameData(targetClientId);
|
||||
writer.StartMessage(GameDataTag.DespawnFlag);
|
||||
writer.WritePacked(obj.NetId);
|
||||
writer.EndMessage();
|
||||
await FinishGameDataAsync(writer, targetClientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,10 +81,19 @@ namespace Impostor.Server.Net.State
|
||||
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();
|
||||
await player.Client.DisconnectAsync(isBan ? DisconnectReason.Banned : DisconnectReason.Kicked);
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up the PlayerInfo if we own it and we're still in the lobby
|
||||
if (GameState == GameStates.NotStarted)
|
||||
{
|
||||
if (GameNet.GameData.PlayersByClientId.TryGetValue(playerId, out var playerInfo))
|
||||
{
|
||||
await DespawnPlayerInfoAsync(playerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ using Impostor.Api.Config;
|
||||
using Impostor.Api.Events.Managers;
|
||||
using Impostor.Api.Games;
|
||||
using Impostor.Api.Innersloth;
|
||||
using Impostor.Api.Innersloth.Customization;
|
||||
using Impostor.Api.Innersloth.GameOptions;
|
||||
using Impostor.Api.Net;
|
||||
using Impostor.Api.Net.Inner.Objects;
|
||||
using Impostor.Api.Net.Manager;
|
||||
using Impostor.Api.Net.Messages.S2C;
|
||||
using Impostor.Server.Events;
|
||||
@@ -131,6 +133,18 @@ namespace Impostor.Server.Net.State
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Check if there are players using a color.</summary>
|
||||
/// <param name="color">The color to check for.</param>
|
||||
/// <param name="exceptBy">Exempt a player from being checked.</param>
|
||||
/// <returns>True if there is player other than exceptBy that uses that color.</returns>
|
||||
internal bool IsColorUsed(ColorType color, IInnerPlayerControl? exceptBy = null)
|
||||
{
|
||||
return Players.Any(p => p.Character != null &&
|
||||
p.Character != exceptBy &&
|
||||
p.Character.PlayerInfo != null &&
|
||||
p.Character.PlayerInfo.CurrentOutfit.Color == color);
|
||||
}
|
||||
|
||||
private ValueTask BroadcastJoinMessage(IMessageWriter message, bool clear, ClientPlayer player)
|
||||
{
|
||||
Message01JoinGameS2C.SerializeJoin(message, clear, Code, player, HostId);
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Impostor.Server.Net.State
|
||||
|
||||
IInnerLobbyBehaviour? IGameNet.LobbyBehaviour => LobbyBehaviour;
|
||||
|
||||
IInnerGameData? IGameNet.GameData => GameData;
|
||||
IInnerGameData IGameNet.GameData => GameData;
|
||||
|
||||
IInnerVoteBanSystem? IGameNet.VoteBan => VoteBan;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Impostor.Server.Net.State
|
||||
|
||||
public InnerLobbyBehaviour? LobbyBehaviour { get; internal set; }
|
||||
|
||||
public InnerGameData? GameData { get; internal set; }
|
||||
public InnerGameData GameData { get; internal set; } = new();
|
||||
|
||||
public InnerVoteBanSystem? VoteBan { get; internal set; }
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"Server": {
|
||||
"PublicIp": "127.0.0.1",
|
||||
"PublicPort": 22023,
|
||||
"ListenIp": "0.0.0.0",
|
||||
"ListenPort": 22023
|
||||
},
|
||||
"HttpServer": {
|
||||
"Enabled": true,
|
||||
"ListenIp": "0.0.0.0",
|
||||
"ListenPort": 22023
|
||||
},
|
||||
"AntiCheat": {
|
||||
"Enabled": true,
|
||||
"BanIpFromGame": true
|
||||
},
|
||||
"Timeout": {
|
||||
"SpawnTimeout": 2500,
|
||||
"ConnectionTimeout": 2500
|
||||
},
|
||||
"Compatibility": {
|
||||
"AllowFutureGameVersions": false,
|
||||
"AllowVersionMixing": false
|
||||
},
|
||||
"Debug": {
|
||||
"GameRecorderEnabled": true,
|
||||
"GameRecorderPath": ""
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"Server": {
|
||||
"PublicIp": "127.0.0.1",
|
||||
"PublicPort": 22023,
|
||||
@@ -12,6 +12,28 @@
|
||||
},
|
||||
"AntiCheat": {
|
||||
"Enabled": true,
|
||||
"BanIpFromGame": true
|
||||
"BanIpFromGame": true,
|
||||
"AllowCheatingHosts": "Never",
|
||||
"EnableGameFlowChecks": true,
|
||||
"EnableMustBeHostChecks": true,
|
||||
"EnableColorLimitChecks": true,
|
||||
"EnableNameLimitChecks": true,
|
||||
"EnableOwnershipChecks": true,
|
||||
"EnableRoleChecks": true,
|
||||
"EnableTargetChecks": true,
|
||||
"ForbidProtocolExtensions": true
|
||||
},
|
||||
"Timeout": {
|
||||
"SpawnTimeout": 2500,
|
||||
"ConnectionTimeout": 2500
|
||||
},
|
||||
"Compatibility": {
|
||||
"AllowFutureGameVersions": false,
|
||||
"AllowHostAuthority": false,
|
||||
"AllowVersionMixing": false
|
||||
},
|
||||
"Debug": {
|
||||
"GameRecorderEnabled": false,
|
||||
"GameRecorderPath": ""
|
||||
}
|
||||
}
|
||||
@@ -48,11 +48,23 @@ public sealed class CompatibilityManagerTests
|
||||
public static IEnumerable<object[]> CanJoinGameData =>
|
||||
new List<object[]>
|
||||
{
|
||||
// Same version -> can join
|
||||
new object[] { GameJoinError.None, new GameVersion(1, 0, 0), new GameVersion(1, 0, 0) },
|
||||
new object[] { GameJoinError.InvalidClient, new GameVersion(1, 0, 0), new GameVersion(100, 0, 0) },
|
||||
|
||||
// Compatible versions -> can join
|
||||
new object[] { GameJoinError.None, new GameVersion(2, 0, 0), new GameVersion(2, 1, 0) },
|
||||
|
||||
// Different incompatible versions -> show correct error
|
||||
new object[] { GameJoinError.ClientOutdated, new GameVersion(2, 0, 0), new GameVersion(1, 0, 0) },
|
||||
new object[] { GameJoinError.ClientTooNew, new GameVersion(1, 0, 0), new GameVersion(2, 0, 0) },
|
||||
new object[] { GameJoinError.None, new GameVersion(2, 0, 0), new GameVersion(2, 1, 0) },
|
||||
|
||||
// Versions unknown to Impostor -> also show correct errors
|
||||
new object[] { GameJoinError.ClientTooNew, new GameVersion(0, 0, 1), new GameVersion(1, 0, 0) },
|
||||
new object[] { GameJoinError.ClientOutdated, new GameVersion(1, 0, 0), new GameVersion(0, 0, 1) },
|
||||
new object[] { GameJoinError.ClientTooNew, new GameVersion(1, 0, 0), new GameVersion(100, 0, 0) },
|
||||
new object[] { GameJoinError.ClientOutdated, new GameVersion(100, 0, 0), new GameVersion(1, 0, 0) },
|
||||
new object[] { GameJoinError.ClientTooNew, new GameVersion(0, 0, 1), new GameVersion(100, 0, 0) },
|
||||
|
||||
};
|
||||
|
||||
[Theory]
|
||||
|
||||
Reference in new issue
Block a user