Compare commits

...
34 Commits
Author SHA1 Message Date
miniduikboot 8f54566245 Add trailing comma
Fixes compile warning
2025-06-05 22:39:11 +02:00
miniduikboot a70e875a4d Update System.Text.Json 2025-06-05 22:39:11 +02:00
miniduikboot e473e4cdb4 Merge pull request #692 from miniduikboot/doc/https-now-mandatory
Update docs to add that HTTPS is now mandatory
2025-06-05 22:31:25 +02:00
miniduikboot 9169fab67d Update FAQ for AU3D 2025-06-02 22:52:45 +02:00
miniduikboot 565bf26ff8 Clarify startup warning 2025-06-02 22:52:24 +02:00
miniduikboot 8fb996b314 Update documentation for mandatory HTTPS
The new Unity version used by Among Us 16.0.5 disables the
"InsecureHttpOption", which blocks UnityWebRequests via HTTP. Therefore
all Impostor servers should now switch to HTTPS.

https://docs.unity3d.com/2022.3/Documentation/ScriptReference/InsecureHttpOption.html

Thanks to TianMengLucky for reporting this and NikoCat233 for
identifying the cause
2025-06-02 22:49:45 +02:00
miniduikboot 46a6005ba5 Don't clear RoleOptions on update
Among Us always adds a constant amount of roles to this dict, so it
doesn't make sense to clear this dictionary every time.

Furthermore, previously it was possible to trigger a
ConcurrentModificationException when running Deserialize in parallel
with Serialize. This should no longer happen, as new keys are no longer added
2025-05-28 22:34:57 +02:00
miniduikboot 0ac0b1d403 Address review comments 2025-05-22 23:50:28 +02:00
miniduikboot e1bd85313a Remove AllowNull from PlayerControl.PlayerInfo
It is possible to spawn invalid network objects without an attached
PlayerInfo, and Impostor will throw exceptions everywhere if you happen
to spawn one. Officials do allow spawning objects like this, so allow it
in Impostor for compatibility reasons
2025-05-22 23:50:28 +02:00
miniduikboot fff9713697 Add new cheat category for InvalidObject checks
For some network objects, like PlayerControl, we expect that there is an
PlayerInfo object associated to them. If there isn't, throw an exception
in this AC category
2025-05-22 23:50:28 +02:00
miniduikboot be0a0b917c Add Murder/Protect/Exile API without checks
Force(.*)Async will perform the same function as \1Async but without
throwing exceptions. The old API is maintained for backwards
compatibility reasons.

Murder checks are now shared with the main function that handles
CheckMurder, as ProtectPlayerAsync and ExileAsync aren't used in the
code I didn't make a similar change there. Checks were also synced
between both methods
2025-05-22 23:50:28 +02:00
miniduikboot c672e19e99 Always store previous color
Because it's only restored in standard SA networking, the stored value
will be ignored in HA. This is preferred to logging when in HA mode
2025-05-22 23:48:50 +02:00
miniduikboot 104376b43f Record and restore player colors
This prevents situations where a player that requested red has a
different color when rejoining because red is the default color of a
PlayerInfo object. This is most noticeable if the host is late with
rejoining, and a lot of players are alreading in WaitingForHost limbo.

When the host finally rejoins, they will not assign Red immediately to
players that requested Red as it is "in use", and instead assign the
next color.

By recording and restoring this is avoided, and players tend to get
their own colors back in most cases.

This is not something that officials do at the moment, but I don't think
this has an impact on mods, as a normal checkcolor/setcolor cycle will
take place as usual.
2025-05-22 23:48:50 +02:00
miniduikboot 92ca5aa9d7 Merge pull request #690 from Impostor/feature/16.0.5
Add 16.0.5 support
2025-05-22 23:13:49 +02:00
miniduikboot 67e94e8efc Add 16.0.5
Crossplay with the previous version is not supported, so it gets its own
compatibility group.

Further analysis on the changes is still needed
2025-05-20 19:48:57 +02:00
miniduikboot da8353655d Merge pull request #683 from Impostor/feature/16.0.0
Add support for 16.0.0
2025-04-03 21:31:35 +02:00
miniduikboot dab8751f1d Add note to old game methods
These are no longer used, so warn people who do want to edit them.

I don't foresee these getting removed soon though, as we do want to kick
players on old versions for having an old game version
2025-04-01 23:50:17 +02:00
miniduikboot 723495e390 Add GameTags enum
Can't be generated from game files at the moment unfortunately
2025-04-01 23:49:46 +02:00
miniduikboot cd220530ac Remove GameListingV2
The game ignores unknown fields when deserializing, and as only fields
were added it's acceptable to merge the structs together
2025-04-01 23:48:32 +02:00
miniduikbootandNiko233 fc2679a8d2 Add stub for Find Lobby endpoint
Co-Authored-By: Niko233 <139348239+NikoCat233@users.noreply.github.com>
2025-03-30 20:35:19 +02:00
miniduikbootandNiko233 28e933092a Stop hardcoding the GameOptions in GameListingV2
To prevent needing to pull in Impostor.Hazel, use an extension method
instead.

Thanks Niko for researching this part

Co-Authored-By: Niko233 <139348239+NikoCat233@users.noreply.github.com>
2025-03-30 20:35:03 +02:00
miniduikboot 06289d46c0 Bump minor version
At this point it doesn't seem necessary to drop support for old
Amoguses, so just bump the minor version for support purposes
2025-03-30 20:34:18 +02:00
miniduikboot 6bd13c0399 Forbid crossplay between 2024.10.29 and 16.0
Innersloth also forbids it, and it's likely not possible due to the new
GameOption version
2025-03-30 20:34:18 +02:00
miniduikboot 7e85419b82 Add endpoint to find games by code
This is used in the new "Enter Code" flow

3 extra fields are added to GameListing, including a Base 64
serialization method for game options
2025-03-30 20:34:18 +02:00
miniduikboot 3e2c3f1525 Update Game Options
Creating a game is now possible
2025-03-30 20:34:18 +02:00
miniduikboot 61cbd5c4d8 Add new network version 2025-03-30 20:34:18 +02:00
miniduikbootandjs6pak 495dc01948 Add diagnostic http message (#663)
* Add diagnostic message to test HTTP server

This makes it possible to diagnose if the reverse proxy setup is working
correctly or if the game configuration is wrong

* Print console log as well

Only print it once to prevent console spam

* Update src/Impostor.Server/Http/HelloController.cs

Co-authored-by: js6pak <me@6pak.dev>

---------

Co-authored-by: js6pak <me@6pak.dev>
2025-01-26 18:00:03 +01:00
Chipmunk a448ece73e Add missing cosmetic methods to IInnerPlayerControl 2025-01-26 17:58:42 +01:00
miniduikboot e7b98f1842 Push down AC checks in PlayerPhysics as well
I've checked Impostor for issues that are similar to #664, and found
this one: if mods add a custom rpc on PlayerPhysics, Impostor will check
ownership of it, which isn't intentional.
2025-01-26 17:42:18 +01:00
miniduikboot d65e99fe45 Move down target AC check in InnerShipStatus
Mods may send RPC's on a ShipStatus that aren't broadcasts. Previously
Impostor would forbid that, which doesn't make sense to me. By moving
down the check it'll allow mods to broadcast RPC's on ShipStatus.

Reported-By: xtracube
2025-01-26 17:41:13 +01:00
miniduikboot bb980096ff Bump version to 1.10.2 2024-12-22 17:02:56 +01:00
miniduikbootandjs6pak 58a8bda8e7 Check for disconnection between handling messages
Without this, it is possible for clients to get the remainder of their
messages handled in a packet.

Co-Authored-By: js6pak <me@6pak.dev>
2024-12-22 17:02:56 +01:00
miniduikbootandjs6pak a7c37b6bcf Clean up abandoned games on disconnect
When a client disconnects without ever having joined a game, it leaves
behind an empty game on the server. Remove these when a player
disconnects.

Co-Authored-By: js6pak <me@6pak.dev>
2024-12-22 17:02:56 +01:00
miniduikbootandjs6pak 792c9626e4 Prevent clients from creating a second game
Legitimate clients will never create a second game using the same
connection.

Co-Authored-By: js6pak <me@6pak.dev>
2024-12-22 17:02:56 +01:00
35 changed files with 713 additions and 134 deletions

No files matched your search

+8
View File
@@ -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
View File
@@ -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).
+12 -9
View File
@@ -10,7 +10,9 @@ 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.
@@ -21,8 +23,9 @@ Depending on your host you may also need to port forward Impostor to the interne
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)
```
+14 -13
View File
@@ -25,19 +25,20 @@ 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. |
| **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. |
| **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. |
| 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
+6
View File
@@ -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:
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>11</LangVersion>
<VersionPrefix>1.10.1</VersionPrefix>
<VersionPrefix>1.10.3</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.4" 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" />
@@ -60,9 +60,9 @@
},
"System.Text.Json": {
"type": "Direct",
"requested": "[8.0.4, )",
"resolved": "8.0.4",
"contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
"requested": "[8.0.5, )",
"resolved": "8.0.5",
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
"System.Buffers": "4.5.1",
+3
View File
@@ -26,6 +26,9 @@ public enum CheatCategory
/// <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,
}
+3 -1
View File
@@ -1,4 +1,4 @@
namespace Impostor.Api.Config
namespace Impostor.Api.Config
{
public class AntiCheatConfig
{
@@ -14,6 +14,8 @@
public bool EnableMustBeHostChecks { get; set; } = true;
public bool EnableInvalidObjectChecks { get; set; } = true;
public bool EnableColorLimitChecks { get; set; } = true;
public bool EnableNameLimitChecks { get; set; } = true;
@@ -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 = 8;
public const int LatestVersion = 9;
public HideNSeekGameOptions(byte version = LatestVersion)
{
@@ -93,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);
@@ -133,6 +138,11 @@ public class HideNSeekGameOptions : IGameOptions
MaxPingTime = reader.ReadSingle();
CrewmateTimeInVent = reader.ReadSingle();
if (Version >= 9)
{
Tag = (GameTags)reader.ReadByte();
}
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
@@ -172,6 +182,11 @@ public class HideNSeekGameOptions : IGameOptions
writer.Write(MaxPingTime);
writer.Write(CrewmateTimeInVent);
if (Version >= 9)
{
writer.Write((byte)Tag);
}
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
@@ -4,7 +4,7 @@ namespace Impostor.Api.Innersloth.GameOptions;
public class NormalGameOptions : IGameOptions
{
public const int LatestVersion = 8;
public const int LatestVersion = 9;
public NormalGameOptions(byte version = LatestVersion)
{
@@ -128,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)
@@ -173,6 +178,11 @@ public class NormalGameOptions : IGameOptions
AnonymousVotes = reader.ReadBoolean();
TaskBarUpdate = (TaskBarUpdate)reader.ReadByte();
if (Version >= 9)
{
Tag = (GameTags)reader.ReadByte();
}
RoleOptions.Deserialize(reader);
if (Version > LatestVersion)
@@ -217,6 +227,11 @@ public class NormalGameOptions : IGameOptions
writer.Write(AnonymousVotes);
writer.Write((byte)TaskBarUpdate);
if (Version >= 9)
{
writer.Write((byte)Tag);
}
RoleOptions.Serialize(writer);
if (Version > LatestVersion)
@@ -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++)
@@ -37,7 +35,7 @@ public class RoleOptionsCollection
_ => throw new ArgumentOutOfRangeException(nameof(roleType), roleType, null),
};
Roles.Add(roleType, new RoleData(roleType, roleOptions, roleRate));
Roles[roleType] = new RoleData(roleType, roleOptions, roleRate);
}
}
+1 -1
View File
@@ -8,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.
@@ -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();
}
}
@@ -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,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
"""
);
}
}
+2
View File
@@ -67,6 +67,7 @@ namespace Impostor.Server.Net
{
CheatCategory.ProtocolExtension => _antiCheatConfig.ForbidProtocolExtensions,
CheatCategory.GameFlow => _antiCheatConfig.EnableGameFlowChecks,
CheatCategory.InvalidObject => _antiCheatConfig.EnableInvalidObjectChecks,
CheatCategory.MustBeHost => _antiCheatConfig.EnableMustBeHostChecks,
CheatCategory.ColorLimits => _antiCheatConfig.EnableColorLimitChecks,
CheatCategory.NameLimits => _antiCheatConfig.EnableNameLimitChecks,
@@ -373,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)
+18
View File
@@ -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,6 +40,8 @@ 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, CheatCategory category, string message)
@@ -59,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;
}
}
}
}
@@ -73,9 +73,16 @@ 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, 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"))
{
@@ -86,9 +93,16 @@ 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, 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"))
{
@@ -99,9 +113,16 @@ 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, 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"))
{
@@ -37,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;
}
@@ -106,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;
}
@@ -287,7 +287,7 @@ 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.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,10 +17,15 @@ namespace Impostor.Server.Net.Inner.Objects
IInnerCustomNetworkTransform IInnerPlayerControl.NetworkTransform => NetworkTransform;
IInnerPlayerInfo IInnerPlayerControl.PlayerInfo => PlayerInfo;
IInnerPlayerInfo? IInnerPlayerControl.PlayerInfo => PlayerInfo;
public async ValueTask SetNameAsync(string 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);
@@ -29,6 +35,11 @@ namespace Impostor.Server.Net.Inner.Objects
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);
@@ -38,6 +49,11 @@ namespace Impostor.Server.Net.Inner.Objects
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.SetHatStr);
@@ -47,6 +63,11 @@ namespace Impostor.Server.Net.Inner.Objects
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.SetPetStr);
@@ -56,6 +77,11 @@ namespace Impostor.Server.Net.Inner.Objects
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.SetSkinStr);
@@ -65,6 +91,11 @@ namespace Impostor.Server.Net.Inner.Objects
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);
@@ -74,6 +105,11 @@ namespace Impostor.Server.Net.Inner.Objects
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);
@@ -100,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);
@@ -136,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);
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Impostor.Api;
@@ -57,8 +56,7 @@ namespace Impostor.Server.Net.Inner.Objects
public InnerCustomNetworkTransform NetworkTransform { get; }
[AllowNull]
public InnerPlayerInfo PlayerInfo { get; internal set; }
public InnerPlayerInfo? PlayerInfo { get; internal set; }
internal Queue<string> RequestedPlayerName { get; } = new Queue<string>();
@@ -385,15 +383,25 @@ namespace Impostor.Server.Net.Inner.Objects
Rpc44SetRole.Deserialize(reader, out var role, out var _);
if (role is RoleTypes.ImpostorGhost or RoleTypes.CrewmateGhost or RoleTypes.GuardianAngel)
if (PlayerInfo == null)
{
PlayerInfo.RoleWhenAlive = PlayerInfo.RoleType;
PlayerInfo.IsDead = true;
if (await sender.Client.ReportCheatAsync(RpcCalls.SetRole, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return false;
}
}
else
{
if (role is RoleTypes.ImpostorGhost or RoleTypes.CrewmateGhost or RoleTypes.GuardianAngel)
{
PlayerInfo.RoleWhenAlive = PlayerInfo.RoleType;
PlayerInfo.IsDead = true;
}
PlayerInfo.RoleType = role;
}
PlayerInfo.RoleType = role;
if (Game.GameState == GameStates.Starting && Game.Players.All(clientPlayer => clientPlayer.Character?.PlayerInfo.RoleType != null))
if (Game.GameState == GameStates.Starting && Game.Players.All(clientPlayer => clientPlayer.Character?.PlayerInfo?.RoleType != null))
{
await Game.StartedAsync();
}
@@ -585,8 +593,19 @@ namespace Impostor.Server.Net.Inner.Objects
internal void Die(DeathReason reason)
{
PlayerInfo.IsDead = true;
PlayerInfo.LastDeathReason = reason;
if (PlayerInfo == null)
{
// "Custom Net Objects" aka hacked up PlayerControl objects as popularized by host only mods may trigger this
if (!Game.IsHostAuthoritive)
{
_logger.LogWarning("Tried to kill player that didn't have a PlayerInfo set, this shouldn't happen");
}
}
else
{
PlayerInfo.IsDead = true;
PlayerInfo.LastDeathReason = reason;
}
}
internal void Protect(InnerPlayerControl guardianAngel)
@@ -598,7 +617,18 @@ namespace Impostor.Server.Net.Inner.Objects
private async ValueTask HandleCompleteTask(ClientPlayer sender, uint taskId)
{
var task = PlayerInfo.Tasks.ElementAtOrDefault((int)taskId);
TaskInfo? task = null;
if (PlayerInfo == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CompleteTask, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return;
}
}
else
{
task = PlayerInfo.Tasks.ElementAtOrDefault((int)taskId);
}
if (task != null)
{
@@ -662,7 +692,10 @@ namespace Impostor.Server.Net.Inner.Objects
if (sender.IsOwner(this))
{
if (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.PlayerName == name))
if (Game.Players.Any(x => x.Character != null &&
x.Character != this &&
x.Character.PlayerInfo != null &&
x.Character.PlayerInfo.PlayerName == name))
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.NameLimits, "Client sent name that is already used"))
{
@@ -685,14 +718,20 @@ namespace Impostor.Server.Net.Inner.Objects
var expected = RequestedPlayerName.Dequeue();
var requested = expected;
if (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.PlayerName == expected))
if (Game.Players.Any(x => x.Character != null &&
x.Character != this &&
x.Character.PlayerInfo != null &&
x.Character.PlayerInfo.PlayerName == expected))
{
var i = 1;
while (true)
{
var text = expected + " " + i;
if (Game.Players.All(x => x.Character == null || x.Character == this || x.Character.PlayerInfo.PlayerName != text))
if (Game.Players.All(x => x.Character == null ||
x.Character == this ||
x.Character.PlayerInfo == null ||
x.Character.PlayerInfo.PlayerName != text))
{
expected = text;
break;
@@ -723,7 +762,17 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
PlayerInfo.CurrentOutfit.PlayerName = name;
if (PlayerInfo == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return false;
}
}
else
{
PlayerInfo.CurrentOutfit.PlayerName = name;
}
return true;
}
@@ -843,7 +892,27 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
PlayerInfo.CurrentOutfit.Color = color;
if (PlayerInfo == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return false;
}
}
else
{
PlayerInfo.CurrentOutfit.Color = color;
}
// Record the color so it can be restored on the next game
if (Game.TryGetPlayer(OwnerId, out var clientPlayer))
{
clientPlayer.Client.PreviousColor = color;
}
else
{
_logger.LogWarning("Tried to record color, but couldn't get player with id {PlayerId}", OwnerId);
}
return true;
}
@@ -856,8 +925,18 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
PlayerInfo.CurrentOutfit.HatId = hat;
PlayerInfo.CurrentOutfit.HatSequenceId = nextRpcSequenceId;
if (PlayerInfo == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetHatStr, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return false;
}
}
else
{
PlayerInfo.CurrentOutfit.HatId = hat;
PlayerInfo.CurrentOutfit.HatSequenceId = nextRpcSequenceId;
}
return true;
}
@@ -870,8 +949,18 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
PlayerInfo.CurrentOutfit.SkinId = skin;
PlayerInfo.CurrentOutfit.SkinSequenceId = nextRpcSequenceId;
if (PlayerInfo == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetSkinStr, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return false;
}
}
else
{
PlayerInfo.CurrentOutfit.SkinId = skin;
PlayerInfo.CurrentOutfit.SkinSequenceId = nextRpcSequenceId;
}
return true;
}
@@ -884,8 +973,18 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
PlayerInfo.CurrentOutfit.VisorId = visor;
PlayerInfo.CurrentOutfit.VisorSequenceId = nextRpcSequenceId;
if (PlayerInfo == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetVisorStr, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return false;
}
}
else
{
PlayerInfo.CurrentOutfit.VisorId = visor;
PlayerInfo.CurrentOutfit.VisorSequenceId = nextRpcSequenceId;
}
return true;
}
@@ -898,8 +997,18 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
PlayerInfo.CurrentOutfit.NamePlateId = namePlate;
PlayerInfo.CurrentOutfit.NamePlateSequenceId = nextRpcSequenceId;
if (PlayerInfo == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetNamePlateStr, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return false;
}
}
else
{
PlayerInfo.CurrentOutfit.NamePlateId = namePlate;
PlayerInfo.CurrentOutfit.NamePlateSequenceId = nextRpcSequenceId;
}
return true;
}
@@ -912,14 +1021,31 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
PlayerInfo.PlayerLevel = level;
if (PlayerInfo == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetLevel, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return false;
}
}
else
{
PlayerInfo.PlayerLevel = level;
}
return true;
}
private async ValueTask<bool> HandleCheckMurder(ClientPlayer sender, InnerPlayerControl? target)
{
if (!PlayerInfo.CanMurder(Game, _dateTimeProvider))
if (PlayerInfo == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return false;
}
}
else if (!PlayerInfo.CanMurder(Game, _dateTimeProvider))
{
if (IsMurdering == target)
{
@@ -934,21 +1060,11 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
}
}
// Host-only mods intentionally desync players, so it may appear that one killing role (like a genuine impostor) is killing another
// killing role (like an sheriff). So this needs to be allowed if the host requested authority.
if (target == null || (target.PlayerInfo.IsImpostor && !_game.IsHostAuthoritive))
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.GameFlow, "Client tried to murder invalid target"))
{
return false;
}
PlayerInfo.LastMurder = _dateTimeProvider.UtcNow - TimeSpan.FromMilliseconds(sender.Client.Connection.AveragePing);
IsMurdering = target;
}
PlayerInfo.LastMurder = _dateTimeProvider.UtcNow - TimeSpan.FromMilliseconds(sender.Client.Connection.AveragePing);
IsMurdering = target;
// Check if host authority mode is on
if (_game.IsHostAuthoritive)
{
@@ -956,17 +1072,34 @@ namespace Impostor.Server.Net.Inner.Objects
return true;
}
if (target != null)
if (target == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.GameFlow, "Client tried to murder a nonexisting target"))
{
return false;
}
}
else
{
var result = target.IsProtected ? MurderResultFlags.FailedProtected : MurderResultFlags.Succeeded;
if (!ValidateMurderPlayer(target, result, out var invalidReason))
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.GameFlow, invalidReason))
{
return false;
}
}
var evt = new PlayerCheckMurderEvent(Game, sender, this, target, result);
await _eventManager.CallAsync(evt);
if (!evt.IsCancelled)
{
target.ProtectedOn = null; // Clear GA protection in all cases
await MurderPlayerAsync(target, evt.Result);
// Don't repeat checks as they were already done in ValidateMP
await ForceMurderPlayerAsync(target, evt.Result);
}
}
@@ -1000,7 +1133,7 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
if (target != null && !target.PlayerInfo.IsDead)
if (target != null && target.PlayerInfo != null && !target.PlayerInfo.IsDead)
{
// In host authoritive mode every client has to figure out if the kill was prevented by guardian protection on it's own
if ((result & MurderResultFlags.Succeeded) != 0 && target.IsProtected)
@@ -1069,8 +1202,18 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
PlayerInfo.CurrentOutfit.PetId = pet;
PlayerInfo.CurrentOutfit.PetSequenceId = nextRpcSequenceId;
if (PlayerInfo == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetPetStr, CheatCategory.InvalidObject, "PlayerControl doesn't have PlayerInfo"))
{
return false;
}
}
else
{
PlayerInfo.CurrentOutfit.PetId = pet;
PlayerInfo.CurrentOutfit.PetSequenceId = nextRpcSequenceId;
}
return true;
}
@@ -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;
@@ -22,6 +22,14 @@ internal class CompatibilityManager : ICompatibilityManager
new GameVersion(2024, 4, 2), // 2024.9.4
new GameVersion(2024, 8, 10), // 2024.10.29
},
new[]
{
new GameVersion(2024, 8, 11), // 16.0.0 (2025-03-25)
},
new[]
{
new GameVersion(2025, 4, 15), // 16.0.5 (2025-05-20)
},
};
private readonly List<CompatibilityGroup> _compatibilityGroups = new();
@@ -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);
@@ -100,6 +108,11 @@ namespace Impostor.Server.Net.Manager
(success, game) = await TryCreateAsync(options, filterOptions, owner);
}
if (owner != null)
{
_gamesCreatedBy[owner] = game;
}
if (!success || game == null)
{
throw new ImpostorException("Could not create new game"); // TODO: Fix generic exception.
@@ -129,5 +142,14 @@ namespace Impostor.Server.Net.Manager
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);
}
}
}
}
+3 -4
View File
@@ -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");
}
}
@@ -499,6 +499,14 @@ namespace Impostor.Server.Net.State
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);
+4 -1
View File
@@ -139,7 +139,10 @@ namespace Impostor.Server.Net.State
/// <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.CurrentOutfit.Color == color);
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)