Compare commits

..
Author SHA1 Message Date
miniduikboot 4452621cd3 Reset PlayerInfo on PlayerControl spawn
When (re)spawning players get a new player control netobject, which is
the perfect moment to discharge them from their former duties and reset
their PlayerInfo to a default state: no role, no tasks and not dead.
2024-08-13 21:18:22 +02:00
miniduikboot 53209aa78f Make PlayerInfo as dirty when setting role/death 2024-08-13 21:16:54 +02:00
miniduikboot c729d26ff6 Sync dirty objects 2024-08-13 21:16:54 +02:00
miniduikboot e2157b0a6f Clean up owned playerinfo's when disconnecting 2024-08-13 21:16:13 +02:00
miniduikboot ad18eda32f Bump to 2024.8.13
Network side seems unchanged and officials allow crossplay
2024-08-13 21:00:51 +02:00
miniduikboot efda5bcef2 Spawn PlayerInfo's if host is not authoritive
This PlayerInfo also needs to be synced over if it is server owned
2024-08-13 21:00:51 +02:00
miniduikboot 6a947457a5 Add NetId to SetName and SetColor
2024.6.18 adds a NetId field *in front* of the existing fields to help link back to the PlayerInfo. We don't actually need this NetId and assume it's called on the correct PlayerControl, so we ignore it.
2024-08-13 21:00:51 +02:00
miniduikboot a3c6ca77c1 Set RoleWhenAlive when set to a Ghost role 2024-08-13 21:00:51 +02:00
miniduikboot 9f3f826b17 Serialize Color -1 as 0 in PlayerOutfit
Official servers send 0 instead of -1 when the outfit is still
incomplete to prevent some client issues.
2024-08-13 21:00:51 +02:00
miniduikboot 36671ba1ba Demote InnerGameData, promote InnerPlayerInfo
In 2024.6.18 Innersloth promoted PlayerInfo to its own network object, and gave it to the server. InnerGameData is now a container that holds these players. This commit implements the changes necessary to handle these separate PlayerInfo network objects.
2024-08-13 21:00:51 +02:00
miniduikboot ace97ddc1d Add dirty tracking to PlayerOutfit
To prevent excessive modifications in the codebase, which is prone to
issues if a callsite is missed, automatically do this using properties.

Keep the access modifiers intact for now.
2024-08-13 21:00:51 +02:00
miniduikboot 4dc4c90e8b Add sequence id's to playeroutfit 2024-08-13 21:00:51 +02:00
miniduikboot 9379c05676 Add canOverrideRole param to SetRole
This is currently unused by Innersloth
2024-08-13 21:00:51 +02:00
miniduikboot ef98f4d20e Implement GameOptions V8
This adds a field for RulesPresets and SpecialGameModes, as well as
RoleOptions for the new roles.
2024-08-13 21:00:51 +02:00
miniduikboot 9f6947b484 VoteBanSystem now contains itself as a component
This makes it consistent with the other spawnables.
2024-08-13 21:00:51 +02:00
miniduikboot 76ae1a3385 Update Dumpostor to v1.0.1 and run for 6.18
Add the RulesPresets and SpecialGamemodes enums
2024-08-13 21:00:51 +02:00
miniduikboot 3c8a002d6e Prepare for 2024.6.18 update
Major changes are incoming, this doesn't yet work.
2024-08-13 21:00:51 +02:00
miniduikboot 35c66cda5f Change default ListenIp to match with config
The default ListenIp for the base game server has been set to 0.0.0.0 in
config.json for as long as I remember, and a different value is only
sporadically used. I remember a case where someone was fancy with Split
DNS. 0 is better as a default than localhost, so change the code
accordingly to aligh with config.json
2024-08-10 18:50:57 +02:00
Bilibili_MengChu 5d4b3d34f5 Modify config.json GameRecorderEnabled Default Value 2024-08-10 18:50:57 +02:00
Bilibili_MengChu d94cf78a98 Update docs/Server-configuration.md File Description 2024-08-10 18:50:57 +02:00
Bilibili_MengChu cfa66fa5cb Delete full-config-example.json and merge it into config.json 2024-08-10 18:50:57 +02:00
miniduikboot 2d82e1af19 Bump System.Text.Json to 8.0.4
Fixes CVE-2024-30105 (High 7.5/10)
https://github.com/advisories/GHSA-hh2w-p6rv-4g7w

Impostor doesn't use the affected
JsonSerializer.DeserializeAsyncEnumerable method as it doesn't accept
JSON formatted payloads, but the NU1903 warning is shown on all builds,
leading to warning fatigue.
2024-07-29 19:59:24 +02:00
miniduikboot c6bad15346 Let hosts handle exiles
Previously Impostor liked to kill exiled players server side to raise an
event. This is quite early, as the host will SetRole them to a ghost
role later.

However as PlayerInfo is server-owned by default in 2024.6.18, it starts
killing players client side. This caused meetings to immediately
conclude if the final impostor was voted out, ruining the cutscene.

So this commit makes Impostor wait for hosts to actually kill the player
using SetRole. Do set the LastDeathReason property though for players
using the API.
2024-07-29 19:58:33 +02:00
miniduikboot b26f451a07 Make Start/Finish GameData API public 2024-07-29 19:57:52 +02:00
miniduikboot 4f7ef58b00 Refactor Start/FinishRpc to add GameData methods
Rpc's are a specific sort of GameData packets, so it makes sense to
split and extract a method for sending GameData packets from these functions.
2024-07-29 19:57:52 +02:00
miniduikbootandjs6pak fe3ea67510 Don't DC the client if they send an unknown flag
This item is only used in a LogTrace call, so it seems excessive to kick
a player on servers just for a log statement

Co-Authored-By: js6pak <me@6pak.dev>
2024-07-27 19:13:42 +02:00
miniduikboot c522dd9f88 Add compatibility option to allow host authority
There are various bugs in the 2024.6.18 implementation of host-authoritive
multiplayer such that I can't recommend players to use it. Fixing these
issues on the server side is impossible, and I can't smell remotely if
clients have applied the necessary fixes. The currently known bugs
include at least:

- Unable to join more than 8 players in a room
- Unable for new players to join if host switched from host authoritive
  to server authoritive
- Host is unable to kill if it is in a host authoritive game while it
  thinks it is in server authoritive mode

If Innersloth fixes this mode in future game patches, this option may be
reconsidered.
2024-07-27 18:49:04 +02:00
miniduikboot 46caeb76c2 Add new option to full config example
Some people were confused by config-full.json, thinking that this file
should be edited too. This is not the case and the values in this file
don't apply as it was only meant as an example, so rename the file to
make that more clear.

Also update this file to add the options from the Modular Anticheat
2024-07-27 17:05:34 +02:00
miniduikboot b218079f9e Merge pull request #599 from miniduikboot/fix/compatman-no-invalid-clients
Improve CompatMgr behaviour for unknown versions
2024-07-19 20:11:44 +02:00
miniduikboot a61524842f Improve CompatMgr behaviour for unknown versions
Currently, Impostor will show an "Client is in an invalid state."
message if a player tries to join a match where the host or the joining
player is using a version unknown to Impostor.

This message confused a *lot* of people, so this PR changes the API such
that if the version is not registered, the version itself is put in a
compatibility group and compared, assuming no other game versions are
compatible with it. This works fine in most cases, as Among Us network
versions generally increase over time.

Failure on 2222.2.2 (which is ancient at this point, so likely no longer
used) and future cases where Innersloth messes up (which is unlikely) is
an acceptable consequence to me.
2024-06-14 21:49:12 +02:00
miniduikboot 34c14f69a4 Merge pull request #598 from miniduikboot/fix/custom-rpc-gamelogic
Accept RPC's called on GameManager
2024-06-14 07:13:20 +02:00
miniduikboot da0ee81b5d Accept RPC's called on GameManager
Previously Impostor would just throw when a method was called on a
GameManager, but this caused issues with client mods that did just this,
even through vanilla never calls an RPC on GameManager.

This commit changes that throw statement to pass the exception to the
custom message handler/anticheat instead, which is also how it works for
the other NetObjects
2024-06-13 20:52:25 +02:00
miniduikboot 468a71d01e Merge pull request #597 from Impostor/feature/2024.6.4
Add 2024.6.4 game version
2024-06-05 19:43:03 +02:00
miniduikboot 6f05946689 Bump Impostor version 2024-06-05 19:16:59 +02:00
miniduikboot b2a9bab125 Add 2024.6.4 game version
Officials allow crossplay and after inspecting the Mono I don't see any
major changes, so I'm following officials here
2024-06-04 20:42:18 +02:00
miniduikboot 9f55fe205d Prepare release 1.9.2 2024-06-01 16:12:10 +02:00
miniduikboot 734643c8c9 Merge pull request #596 from miniduikboot/fix/cleanup-disposed-clients
Clean up clients that refuse to quit properly
2024-05-30 21:47:31 +02:00
miniduikboot 661421da5a Clean up clients that refuse to quit properly
When a player is kicked or banned from a game, the client is expected to
disconnect in response. When they decide not to *or* the disconnect
packet is lost, Impostor just silently disposes of the connection.
This does not remove them from ClientManager, which is how I tracked
this bug down.

By sending them a proper disconnect message, clients should have
received *a* disconnection message before dropping their connection.

Future work should split this entire method in removing players from a
game and processing kick/bans because the latter feature is not relevant
for other callers of this method.
2024-05-26 22:05:21 +02:00
miniduikboot ff2e80d939 Merge pull request #594 from miniduikboot/feature/refine-modular-ac
Change some anticheat checks categories
2024-05-11 13:54:49 +02:00
miniduikboot ddd701320d Add similar AC check to HandleSetName 2024-04-28 20:55:50 +02:00
miniduikboot be39c9facf Move Name checks to NameLimits 2024-04-28 20:55:50 +02:00
miniduikboot 6f56e1acaf Move remaining Color checks to ColorLimits
Some mods add custom colors and still use the standard vanilla SetColor
RPC, which leads to issues with this check.
2024-04-28 20:50:41 +02:00
miniduikboot f1f5185f10 Merge pull request #593 from Pietrodjaowjao/master
Patch game-breaking exploits
2024-04-28 20:44:40 +02:00
miniduikboot 414303b2c1 Documentation: Update .NET 7 to .NET 8 2024-04-28 18:31:34 +02:00
Pietrodjaowjao 0ba700ef68 Forgot to actually return 2024-04-28 05:12:29 -03:00
Pietrodjaowjao c2b5a3e9df gamedata 2024-04-28 04:28:09 -03:00
miniduikboot ac12bacb00 Merge pull request #591 from NikoCat233/add-broadcast-check-to-most-of-calls
[AntiCheat] Add ValidateBroadCast check to most of the rpc calls
2024-04-28 08:22:55 +02:00
miniduikboot 2f3e1a620b Fix embarrassing bugs 2024-04-27 20:20:04 +02:00
miniduikboot d391b24aad Remove use of SetInfected RPC
Client no longer sends the RPC nor does anything with it when receiving it
2024-04-27 16:55:50 +02:00
miniduikboot 5854000220 Refactor if statements to be more vertically dense
Multiple guard clauses make the file harder to navigate, so put them in
a single guard clause with one validation rule per line. Some statements
in the file were already set up like this, so this commit makes it consistent
2024-04-27 16:45:15 +02:00
miniduikboot ef022b04e4 Add Broadcast checks to remaining RPC's 2024-04-27 16:43:50 +02:00
miniduikboot 155c3e69d9 Merge pull request #589 from miniduikboot/feature/game-created-event-add-host
Add Host property to IGameCreatedEvent
2024-04-21 21:44:24 +02:00
Niko233 3c9b0c305c Add broadcast 2024-04-21 14:48:54 +08:00
miniduikboot 2c0f45c5d1 Add Host property to IGameCreatedEvent
When a game has just been created, it does not yet have any players in
them, and as a result you can't get the host or its connection id when
this event is called. This makes it annoying to link between this event
and the IGameCreationEvent that came just before it.

Having this link is useful for the usecase of a game code allocation
plugin that allocates codes from a pool, and needs to return codes to
a pool if these codes weren't picked.
2024-04-12 22:53:01 +02:00
miniduikboot 9b7b1e8b26 Merge pull request #571 from Impostor/bugfix/misc-ac
Miscellaneous Anticheat fixes
2024-04-04 20:17:44 +02:00
miniduikboot 1a11b5f43e Rework Color calculation loop to always terminate 2024-04-04 20:12:44 +02:00
miniduikboot bdd65e9e9f Don't dequeue a color when none are available 2024-04-04 20:10:23 +02:00
miniduikboot ba9eeb5bb4 Check AC config in SetName/Color checks
Previously this was not done causing these checks to be unconditionally enabled
2024-04-04 20:10:23 +02:00
miniduikboot 5dafd9fe5b Allow host-only mods to desync kill timeouts 2024-04-04 20:10:23 +02:00
miniduikboot 7f943e6b2e Allow host-only mods to desync murder roles 2024-04-04 20:10:23 +02:00
miniduikboot 28f3cb3b2e Make Impostor's AC modular (#559)
* Add extra configuration options for the anticheat

* Implement option to exempt hosts from AC checks

* Add AntiCheatConfig to InnerNetObject and friends

* Categorize calls in InnerNetObject.Anticheat

* Categorize other calls to ReportCheatAsync

* Update cosmetics AC handling

When Innersloth changed SetHat etc to SetHatStr as part of the cosmicube
update we accidentally created a few dead methods. Restore and update
the code.

* Push up cheat category to ReportCheatAsync

This makes it possible to log failed checks for diagnosis. Also much
cleaner now that we don't have to push AntiCheatConfig everywhere.
Should've done this immediately q.q

* Add back old version of ReportCheatAsync

some plugins used this method. affected plugins should rebuild after the
next impostor release

* Add documentation for modular anticheat

* Address comments

* Sort AntiCheatConfig keys

* Rework the option to allow cheating hosts

This option should actually be tristate: to not break host-only mods we
should disable the anticheat for hosts running host-only mods.

* Document CheatingHostMode

* Split Limits category in Color and Name Limits
2024-04-04 20:09:59 +02:00
65 changed files with 1239 additions and 409 deletions

No files matched your search

+2 -2
View File
@@ -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
+1 -1
View File
@@ -16,7 +16,7 @@ Depending on your host you may also need to port forward Impostor to the interne
## 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.
+21 -13
View File
@@ -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,29 @@ 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. |
| **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 +97,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
+1 -9
View File
@@ -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
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>11</LangVersion>
<VersionPrefix>1.9.1</VersionPrefix>
<VersionPrefix>1.10.0</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.4" 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.4, )",
"resolved": "8.0.4",
"contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
"System.Buffers": "4.5.1",
+31
View File
@@ -0,0 +1,31 @@
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>Legacy category for unsorted anticheat checks.</summary>
Other,
}
@@ -7,5 +7,23 @@
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 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.";
}
}
+1 -1
View File
@@ -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; }
}
}
+23 -4
View File
@@ -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);
}
private byte HatSequenceId { get; set; } = 0;
private byte PetSequenceId { get; set; } = 0;
private byte SkinSequenceId { get; set; } = 0;
private byte VisorSequenceId { get; set; } = 0;
private 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,7 +2,6 @@
"0": "SkeldShipStatus",
"1": "MeetingHud",
"2": "LobbyBehaviour",
"3": "GameData",
"4": "PlayerControl",
"5": "MiraShipStatus",
"6": "PolusShipStatus",
@@ -10,5 +9,7 @@
"8": "AirshipStatus",
"9": "HideAndSeekManager",
"10": "NormalGameManager",
"11": "NetworkedPlayerInfo",
"12": "VoteBanSystem",
"13": "FungleShipStatus"
}
@@ -6,5 +6,8 @@
"GuardianAngel": 4,
"Shapeshifter": 5,
"CrewmateGhost": 6,
"ImpostorGhost": 7
"ImpostorGhost": 7,
"Noisemaker": 8,
"Phantom": 9,
"Tracker": 10
}
@@ -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,33 @@
"ReportNotificationBody": 3324,
"MissingGuardianEmailText": 3325,
"AprilFoolsModeToggle": 3326,
"LobbyChangeSettingNotification": 3327,
"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,
"MiningPit": 3400,
"Jungle": 3401,
"BuildSandcastle": 3402,
@@ -1399,5 +1473,11 @@
"HttpErrorBadGateway": 3531,
"HttpErrorServiceUnavailable": 3532,
"HttpErrorGatewayTimeout": 3533,
"UserVersionError": 3534
"UserVersionError": 3534,
"QCLobbyRoleNoisemakers": 3535,
"QCLobbyRoleTrackers": 3536,
"QCLobbyRolePhantoms": 3537,
"QCSelfWasTracking": 3538,
"QCSelfGoingToNoisemaker": 3539,
"QCSelfSawRoleInLocation": 3540
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"dumpostorVersion": "1.0.0",
"gameVersion": "2024.3.5",
"dumpostorVersion": "1.0.1",
"gameVersion": "2024.6.18",
"platformType": "StandaloneItch"
}
@@ -2,7 +2,7 @@ namespace Impostor.Api.Innersloth.GameOptions;
public class HideNSeekGameOptions : IGameOptions
{
public const int LatestVersion = 7;
public const int LatestVersion = 8;
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;
@@ -96,6 +102,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 +133,7 @@ public class HideNSeekGameOptions : IGameOptions
MaxPingTime = reader.ReadSingle();
CrewmateTimeInVent = reader.ReadSingle();
if (Version > 7)
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
}
@@ -129,6 +141,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 +172,7 @@ public class HideNSeekGameOptions : IGameOptions
writer.Write(MaxPingTime);
writer.Write(CrewmateTimeInVent);
if (Version > 7)
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 = 8;
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;
@@ -133,6 +139,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();
@@ -163,7 +175,7 @@ public class NormalGameOptions : IGameOptions
RoleOptions.Deserialize(reader);
if (Version > 7)
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
}
@@ -171,6 +183,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);
@@ -201,7 +219,7 @@ public class NormalGameOptions : IGameOptions
RoleOptions.Serialize(writer);
if (Version > 7)
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
}
@@ -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);
}
}
@@ -31,6 +31,9 @@ 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),
_ => throw new ArgumentOutOfRangeException(nameof(roleType), roleType, null),
};
@@ -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);
}
}
+4
View File
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Impostor.Api.Innersloth;
@@ -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
{
}
}
@@ -41,7 +41,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,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();
}
}
}
@@ -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; }
}
}
+42 -2
View File
@@ -35,14 +35,54 @@ 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 (Player != null && Player.IsHost)
{
var isHostCheatingAllowed = _antiCheatConfig.AllowCheatingHosts switch {
CheatingHostMode.Always => true,
CheatingHostMode.IfRequested => GameVersion.HasDisableServerAuthorityFlag,
CheatingHostMode.Never => false,
_ => false,
};
if (isHostCheatingAllowed)
{
return false;
}
}
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.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;
}
_logger.LogWarning("Client {Name} ({Id}) was caught cheating: [{Context}-{Category}] {Message}", Name, Id, context.Name, category, message);
if (_antiCheatConfig.BanIpFromGame)
{
+6 -1
View File
@@ -41,11 +41,16 @@ namespace Impostor.Server.Net
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);
@@ -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;
}
@@ -77,7 +77,7 @@ namespace Impostor.Server.Net.Inner
{
if (playerInfo.IsImpostor != value)
{
if (await sender.Client.ReportCheatAsync(context, "Failed impostor check"))
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, "Failed impostor check"))
{
return false;
}
@@ -90,7 +90,7 @@ namespace Impostor.Server.Net.Inner
{
if (playerInfo.CanVent != value)
{
if (await sender.Client.ReportCheatAsync(context, "Failed can vent check"))
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, "Failed can vent check"))
{
return false;
}
@@ -103,7 +103,7 @@ namespace Impostor.Server.Net.Inner
{
if (playerInfo.RoleType != role)
{
if (await sender.Client.ReportCheatAsync(context, $"Failed role = {role} check"))
if (await sender.Client.ReportCheatAsync(context, CheatCategory.Role, $"Failed role = {role} check"))
{
return false;
}
@@ -114,7 +114,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;
}
@@ -29,6 +29,8 @@ namespace Impostor.Server.Net.Inner
public SpawnFlags SpawnFlags { get; internal set; }
public virtual bool IsDirty { get; internal set; }
public bool IsOwnedBy(IClientPlayer player)
{
return OwnerId == player.Client.Id ||
@@ -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;
@@ -67,7 +68,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 +78,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;
}
@@ -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,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;
}
@@ -289,7 +289,7 @@ namespace Impostor.Server.Net.Inner.Objects
if (exiled != null)
{
exiled.Die(DeathReason.Exile);
exiled.PlayerInfo.LastDeathReason = DeathReason.Exile;
await _eventManager.CallAsync(new PlayerExileEvent(Game, Game.GetClientPlayer(exiled!.OwnerId)!, exiled));
}
@@ -20,8 +20,9 @@ namespace Impostor.Server.Net.Inner.Objects
public async ValueTask SetNameAsync(string name)
{
PlayerInfo.PlayerName = name;
PlayerInfo.CurrentOutfit.PlayerName = name;
// TODO check playerinfo ownership
using var writer = Game.StartRpc(NetId, RpcCalls.SetName);
writer.Write(name);
await Game.FinishRpcAsync(writer);
@@ -32,7 +33,7 @@ namespace Impostor.Server.Net.Inner.Objects
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);
}
@@ -113,7 +113,8 @@ namespace Impostor.Server.Net.Inner.Objects
{
case RpcCalls.PlayAnimation:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -124,7 +125,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.CompleteTask:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -145,21 +147,10 @@ namespace Impostor.Server.Net.Inner.Objects
break;
}
case RpcCalls.SetInfected:
{
if (!await ValidateOwnership(call, sender) || !await ValidateHost(call, sender))
{
return false;
}
Rpc03SetInfected.Deserialize(reader, out var infectedIds);
await HandleSetInfected(infectedIds);
break;
}
case RpcCalls.CheckName:
{
if (!await ValidateOwnership(call, sender) || !await ValidateCmd(call, sender, target))
if (!await ValidateOwnership(call, sender) ||
!await ValidateCmd(call, sender, target))
{
return false;
}
@@ -170,18 +161,20 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.SetName:
{
if (!await ValidateHost(call, sender))
if (!await ValidateHost(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
Rpc06SetName.Deserialize(reader, out var name);
Rpc06SetName.Deserialize(reader, out var _, out var name);
return await HandleSetName(sender, name);
}
case RpcCalls.CheckColor:
{
if (!await ValidateOwnership(call, sender) || !await ValidateCmd(call, sender, target))
if (!await ValidateOwnership(call, sender) ||
!await ValidateCmd(call, sender, target))
{
return false;
}
@@ -192,46 +185,50 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.SetColor:
{
if (!await ValidateHost(call, sender))
if (!await ValidateHost(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
Rpc08SetColor.Deserialize(reader, out var color);
Rpc08SetColor.Deserialize(reader, out var _, out var color);
return await HandleSetColor(sender, color);
}
case RpcCalls.SetHatStr:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
Rpc39SetHatStr.Deserialize(reader, out var hat);
return true;
return await HandleSetHat(sender, hat);
}
case RpcCalls.SetSkinStr:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
Rpc40SetSkinStr.Deserialize(reader, out var skin);
return true;
return await HandleSetSkin(sender, skin);
}
case RpcCalls.SetVisorStr:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
Rpc42SetVisorStr.Deserialize(reader, out var visor);
return true;
return await HandleSetVisor(sender, visor);
}
case RpcCalls.SetNamePlateStr:
@@ -242,12 +239,13 @@ namespace Impostor.Server.Net.Inner.Objects
}
Rpc43SetNamePlateStr.Deserialize(reader, out var namePlate);
return true;
return await HandleSetNamePlate(sender, namePlate);
}
case RpcCalls.SetLevel:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -258,7 +256,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.ReportDeadBody:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -269,7 +268,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.MurderPlayer:
{
if (!await ValidateHost(call, sender))
if (!await ValidateHost(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -280,7 +280,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.SendChat:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -291,7 +292,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.StartMeeting:
{
if (!await ValidateHost(call, sender))
if (!await ValidateHost(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -303,7 +305,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.SetScanner:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -314,7 +317,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.SendChatNote:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -325,7 +329,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.SetPetStr:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -336,7 +341,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.SetStartCounter:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -347,7 +353,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.UsePlatform:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -358,7 +365,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.SendQuickChat:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -369,13 +377,22 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.SetRole:
{
if (!await ValidateHost(call, sender))
if (!await ValidateHost(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
Rpc44SetRole.Deserialize(reader, out var role);
Rpc44SetRole.Deserialize(reader, out var role, out var _);
if (role is RoleTypes.ImpostorGhost or RoleTypes.CrewmateGhost or RoleTypes.GuardianAngel)
{
PlayerInfo.RoleWhenAlive = PlayerInfo.RoleType;
PlayerInfo.IsDead = true;
}
PlayerInfo.RoleType = role;
PlayerInfo.IsDirty = true;
if (Game.GameState == GameStates.Starting && Game.Players.All(clientPlayer => clientPlayer.Character?.PlayerInfo.RoleType != null))
{
@@ -387,7 +404,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.ProtectPlayer:
{
if (!await ValidateHost(call, sender))
if (!await ValidateHost(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -398,12 +416,9 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.Shapeshift:
{
if (!await ValidateHost(call, sender))
{
return false;
}
if (!await ValidateRole(call, sender, PlayerInfo, RoleTypes.Shapeshifter))
if (!await ValidateHost(call, sender) ||
!await ValidateRole(call, sender, PlayerInfo, RoleTypes.Shapeshifter) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -415,12 +430,9 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.CheckMurder:
{
if (!await ValidateOwnership(call, sender))
{
return false;
}
if (!await ValidateImpostor(call, sender, PlayerInfo))
if (!await ValidateOwnership(call, sender) ||
!await ValidateImpostor(call, sender, PlayerInfo) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -431,23 +443,22 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.CheckProtect:
{
if (!await ValidateOwnership(call, sender))
{
return false;
}
if (!await ValidateRole(call, sender, PlayerInfo, RoleTypes.GuardianAngel))
if (!await ValidateOwnership(call, sender) ||
!await ValidateRole(call, sender, PlayerInfo, RoleTypes.GuardianAngel) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
// CheckProtect should only be passed to Host and not handled server side
Rpc48CheckProtect.Deserialize(reader, Game, out _);
break;
}
case RpcCalls.CheckZipline:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateCmd(call, sender, target))
{
return false;
}
@@ -458,7 +469,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.UseZipline:
{
if (!await ValidateHost(call, sender))
if (!await ValidateHost(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -469,7 +481,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.TriggerSpores:
{
if (!await ValidateHost(call, sender))
if (!await ValidateHost(call, sender) ||
!await ValidateBroadcast(call, sender, target))
{
return false;
}
@@ -480,7 +493,8 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.CheckSpore:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateCmd(call, sender, target))
{
return false;
}
@@ -491,12 +505,14 @@ namespace Impostor.Server.Net.Inner.Objects
case RpcCalls.CheckShapeshift:
{
if (!await ValidateOwnership(call, sender))
if (!await ValidateOwnership(call, sender) ||
!await ValidateRole(call, sender, PlayerInfo, RoleTypes.Shapeshifter) ||
!await ValidateCmd(call, sender, target))
{
return false;
}
Rpc46Shapeshift.Deserialize(reader, Game, out var playerControl, out var shouldAnimate);
Rpc55CheckShapeshift.Deserialize(reader, Game, out var playerControl, out var shouldAnimate);
break;
}
@@ -522,6 +538,7 @@ namespace Impostor.Server.Net.Inner.Objects
{
PlayerInfo.IsDead = true;
PlayerInfo.LastDeathReason = reason;
PlayerInfo.IsDirty = true;
}
internal void Protect(InnerPlayerControl guardianAngel)
@@ -565,9 +582,17 @@ namespace Impostor.Server.Net.Inner.Objects
private async ValueTask<bool> HandleCheckName(ClientPlayer sender, string name)
{
if (Game.GameState == GameStates.Started)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, CheatCategory.GameFlow, "Client tried to set a name midgame"))
{
return false;
}
}
if (name.Length > 10)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, "Client sent name exceeding 10 characters"))
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, CheatCategory.NameLimits, "Client sent name exceeding 10 characters"))
{
return false;
}
@@ -575,7 +600,7 @@ namespace Impostor.Server.Net.Inner.Objects
if (string.IsNullOrWhiteSpace(name) || !name.All(TextBox.IsCharAllowed))
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, "Client sent name containing illegal characters"))
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, CheatCategory.NameLimits, "Client sent name containing illegal characters"))
{
return false;
}
@@ -583,7 +608,7 @@ namespace Impostor.Server.Net.Inner.Objects
if (sender.Client.Name != name)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, "Client sent name not matching his name from handshake"))
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckName, CheatCategory.NameLimits, "Client sent name not matching his name from handshake"))
{
return false;
}
@@ -598,7 +623,7 @@ namespace Impostor.Server.Net.Inner.Objects
{
if (Game.GameState == GameStates.Started)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, "Client tried to set a name midgame"))
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.GameFlow, "Client tried to set a name midgame"))
{
return false;
}
@@ -608,7 +633,7 @@ namespace Impostor.Server.Net.Inner.Objects
{
if (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.PlayerName == name))
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, "Client sent name that is already used"))
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.NameLimits, "Client sent name that is already used"))
{
return false;
}
@@ -616,7 +641,7 @@ namespace Impostor.Server.Net.Inner.Objects
if (sender.Client.Name != name)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, "Client sent name not matching his name from handshake"))
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.NameLimits, "Client sent name not matching his name from handshake"))
{
return false;
}
@@ -647,30 +672,40 @@ namespace Impostor.Server.Net.Inner.Objects
if (name != expected)
{
_logger.LogWarning($"Client sent {nameof(RpcCalls.SetName)} with incorrect name");
await SetNameAsync(expected);
return false;
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.NameLimits, "Client sent SetName with incorrect name"))
{
await SetNameAsync(expected);
return false;
}
}
}
else
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, $"Client sent {nameof(RpcCalls.SetName)} for a player that didn't request it"))
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.NameLimits, $"Client sent {nameof(RpcCalls.SetName)} for a player that didn't request it"))
{
return false;
}
}
}
PlayerInfo.PlayerName = name;
PlayerInfo.CurrentOutfit.PlayerName = name;
return true;
}
private async ValueTask<bool> HandleCheckColor(ClientPlayer sender, ColorType color)
{
if (Game.GameState == GameStates.Started)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckColor, CheatCategory.GameFlow, "Client tried to ask for a color midgame"))
{
return false;
}
}
if ((byte)color > ColorsCount)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckColor, "Client sent invalid color"))
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckColor, CheatCategory.ColorLimits, "Client sent invalid color"))
{
return false;
}
@@ -685,7 +720,7 @@ namespace Impostor.Server.Net.Inner.Objects
{
if (Game.GameState == GameStates.Started)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, "Client tried to set a color midgame"))
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.GameFlow, "Client tried to set a color midgame"))
{
return false;
}
@@ -695,7 +730,7 @@ namespace Impostor.Server.Net.Inner.Objects
{
if (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.CurrentOutfit.Color == color))
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, "Client sent a color that is already used"))
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.ColorLimits, "Client sent a color that is already used"))
{
return false;
}
@@ -703,24 +738,44 @@ namespace Impostor.Server.Net.Inner.Objects
}
else
{
if (!RequestedColorId.Any())
if (RequestedColorId.Any())
{
_logger.LogWarning($"Client sent {nameof(RpcCalls.SetColor)} for a player that didn't request it");
return false;
var expected = RequestedColorId.Dequeue();
for (var colorOffset = 0; colorOffset <= ColorsCount; colorOffset++)
{
var possibleColor = (ColorType)((byte)(expected + colorOffset) % ColorsCount);
if (!Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.CurrentOutfit.Color == possibleColor))
{
expected = possibleColor;
break;
}
if (colorOffset == ColorsCount)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.ColorLimits, "Client sent SetColor but all colors are already in use"))
{
await SetColorAsync(expected);
return false;
}
}
}
if (color != expected)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.ColorLimits, "Client sent SetColor with incorrect color"))
{
await SetColorAsync(expected);
return false;
}
}
}
var expected = RequestedColorId.Dequeue();
while (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.CurrentOutfit.Color == expected))
else
{
expected = (ColorType)(((byte)expected + 1) % ColorsCount);
}
if (color != expected)
{
_logger.LogWarning($"Client sent {nameof(RpcCalls.SetColor)} with incorrect color");
await SetColorAsync(expected);
return false;
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.GameFlow, "Client sent SetColor for a player that didn't request it"))
{
return false;
}
}
}
@@ -731,7 +786,8 @@ namespace Impostor.Server.Net.Inner.Objects
private async ValueTask<bool> HandleSetHat(ClientPlayer sender, string hat)
{
if (Game.GameState == GameStates.Started && await sender.Client.ReportCheatAsync(RpcCalls.SetHat, "Client tried to change hat while not in lobby"))
if (Game.GameState == GameStates.Started &&
await sender.Client.ReportCheatAsync(RpcCalls.SetHat, CheatCategory.GameFlow, "Client tried to change hat while not in lobby"))
{
return false;
}
@@ -743,7 +799,8 @@ namespace Impostor.Server.Net.Inner.Objects
private async ValueTask<bool> HandleSetSkin(ClientPlayer sender, string skin)
{
if (Game.GameState == GameStates.Started && await sender.Client.ReportCheatAsync(RpcCalls.SetSkin, "Client tried to change skin while not in lobby"))
if (Game.GameState == GameStates.Started &&
await sender.Client.ReportCheatAsync(RpcCalls.SetSkin, CheatCategory.GameFlow, "Client tried to change skin while not in lobby"))
{
return false;
}
@@ -753,6 +810,32 @@ namespace Impostor.Server.Net.Inner.Objects
return true;
}
private async ValueTask<bool> HandleSetVisor(ClientPlayer sender, string visor)
{
if (Game.GameState == GameStates.Started &&
await sender.Client.ReportCheatAsync(RpcCalls.SetVisor, CheatCategory.GameFlow, "Client tried to change visor while not in lobby"))
{
return false;
}
PlayerInfo.CurrentOutfit.VisorId = visor;
return true;
}
private async ValueTask<bool> HandleSetNamePlate(ClientPlayer sender, string skin)
{
if (Game.GameState == GameStates.Started &&
await sender.Client.ReportCheatAsync(RpcCalls.SetNamePlate, CheatCategory.GameFlow, "Client tried to change skin while not in lobby"))
{
return false;
}
PlayerInfo.CurrentOutfit.NamePlateId = skin;
return true;
}
private async ValueTask<bool> HandleCheckMurder(ClientPlayer sender, InnerPlayerControl? target)
{
if (!PlayerInfo.CanMurder(Game, _dateTimeProvider))
@@ -762,15 +845,21 @@ namespace Impostor.Server.Net.Inner.Objects
// This request was made too quickly by spamming the kill button, cancel it if we're in server authoritive mode
return _game.IsHostAuthoritive;
}
else if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, "Client tried to murder too fast"))
else if (!_game.IsHostAuthoritive)
{
return false;
// Host-only mods may desync the kill timeout between players
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.GameFlow, "Client tried to murder too fast"))
{
return false;
}
}
}
if (target == null || target.PlayerInfo.IsImpostor)
// 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, "Client tried to murder invalid target"))
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckMurder, CheatCategory.GameFlow, "Client tried to murder invalid target"))
{
return false;
}
@@ -807,15 +896,15 @@ namespace Impostor.Server.Net.Inner.Objects
{
if (!_game.IsHostAuthoritive)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Client tried to murder directly"))
if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, CheatCategory.GameFlow, "Client tried to murder directly"))
{
return false;
}
}
if (target == null || target.PlayerInfo.IsImpostor)
if (target == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Client tried to murder invalid target"))
if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, CheatCategory.GameFlow, "Client tried to murder invalid target"))
{
return false;
}
@@ -824,7 +913,7 @@ namespace Impostor.Server.Net.Inner.Objects
// If the host is also the impostor that committed the murder, CheckMurder is actually sent *after* the MurderPlayer RPC
if (sender.Character != this && target != IsMurdering)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Host tried to murder incorrect target"))
if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, CheatCategory.GameFlow, "Host tried to murder incorrect target"))
{
return false;
}
@@ -859,7 +948,7 @@ namespace Impostor.Server.Net.Inner.Objects
{
if (target == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckProtect, "Client tried to protect invalid target"))
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckProtect, CheatCategory.Target, "Client tried to protect invalid target"))
{
return false;
}
@@ -867,12 +956,9 @@ namespace Impostor.Server.Net.Inner.Objects
return true;
}
if (PlayerInfo.RoleType != RoleTypes.GuardianAngel)
if (!await ValidateRole(RpcCalls.ProtectPlayer, sender, PlayerInfo, RoleTypes.GuardianAngel))
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckProtect, "Client tried to protect but it wasn't a guardian angel"))
{
return false;
}
}
((InnerPlayerControl)target).Protect(this);
@@ -896,7 +982,8 @@ namespace Impostor.Server.Net.Inner.Objects
private async ValueTask<bool> HandleSetPet(ClientPlayer sender, string pet)
{
if (Game.GameState == GameStates.Started && await sender.Client.ReportCheatAsync(RpcCalls.SetPet, "Client tried to change pet while not in lobby"))
if (Game.GameState == GameStates.Started &&
await sender.Client.ReportCheatAsync(RpcCalls.SetPet, CheatCategory.GameFlow, "Client tried to change pet while not in lobby"))
{
return false;
}
@@ -910,7 +997,7 @@ namespace Impostor.Server.Net.Inner.Objects
{
if (!sender.IsHost && startCounter != -1)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.MurderPlayer, "Client tried to set start counter as a non-host"))
if (await sender.Client.ReportCheatAsync(RpcCalls.SetStartCounter, CheatCategory.MustBeHost, "Client tried to set start counter as a non-host"))
{
return false;
}
@@ -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,35 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
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.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 ILogger<InnerPlayerInfo> _logger;
public InnerPlayerInfo(ICustomMessageManager<ICustomRpc> customMessageManager, Game game, ILogger<InnerPlayerInfo> logger) : base(customMessageManager, game)
{
PlayerId = playerId;
Components.Add(this);
_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,6 +42,8 @@ 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;
@@ -48,6 +60,12 @@ namespace Impostor.Server.Net.Inner.Objects
public uint PlayerLevel { get; internal set; }
public override bool IsDirty
{
get => CurrentOutfit.IsDirty;
internal set => CurrentOutfit.IsDirty = value;
}
public bool CanMurder(IGame game, IDateTimeProvider dateTimeProvider)
{
if (!IsImpostor)
@@ -71,13 +89,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 +154,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 +175,12 @@ 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;
}
}
}
@@ -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,8 @@ internal class CompatibilityManager : ICompatibilityManager
{
new[]
{
new GameVersion(2023, 10, 1), // 2023.10.24
new GameVersion(2023, 11, 15), // 2023.11.28
},
new[]
{
new GameVersion(2024, 2, 1), // 2024.3.5
new GameVersion(2024, 3, 1), // 2024.6.18
new GameVersion(2024, 4, 1), // 2024.8.13
},
};
@@ -62,6 +58,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 +92,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)
{
@@ -93,11 +93,11 @@ 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 (!success || game == null)
@@ -113,7 +113,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,7 +125,7 @@ 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);
}
+129 -17
View File
@@ -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,39 @@ 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 {0} ({1}) tried to send SceneChangeFlag with disallowed scene.",
sender.Client.Name,
sender.Client.Id);
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 +301,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;
}
@@ -284,6 +326,17 @@ namespace Impostor.Server.Net.State
}
}
// Check for dirty netobjects
foreach (var netObject in _allObjectsFast.Values)
{
if (netObject.IsDirty && netObject.OwnerId == ServerOwned)
{
_logger.LogTrace("Sending over {Type} {NetId}", netObject.GetType().Name, netObject.NetId);
await SendObjectData(netObject);
netObject.IsDirty = false;
}
}
return true;
}
@@ -303,9 +356,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,16 +392,23 @@ 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)
{
playerInfo.Controller = control;
control.PlayerInfo = playerInfo;
// Reset PlayerInfo
playerInfo.RoleType = null;
playerInfo.RoleWhenAlive = null;
playerInfo.Tasks.Clear();
playerInfo.IsDead = false;
playerInfo.IsDirty = true;
}
if (player != null)
@@ -379,12 +447,6 @@ namespace Impostor.Server.Net.State
break;
}
case InnerGameData:
{
GameNet.GameData = null;
break;
}
case InnerVoteBanSystem:
{
GameNet.VoteBan = null;
@@ -397,13 +459,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 +483,51 @@ 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 (!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 bool AddNetObject(InnerNetObject obj)
{
if (_allObjectsFast.ContainsKey(obj.NetId))
+59 -6
View File
@@ -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,46 @@ 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 ValueTask SendObjectDespawn(InnerNetObject obj, int? targetClientId = null)
{
using var writer = StartGameData(targetClientId);
writer.StartMessage(GameDataTag.DespawnFlag);
writer.WritePacked(obj.NetId);
writer.EndMessage();
return FinishGameDataAsync(writer, targetClientId);
}
private async ValueTask SendObjectData(InnerNetObject obj, int? targetClientId = null)
{
using var writer = StartGameData(targetClientId);
writer.StartMessage(GameDataTag.DataFlag);
writer.WritePacked(obj.NetId);
await obj.SerializeAsync(writer, false);
writer.EndMessage();
await FinishGameDataAsync(writer, targetClientId);
}
}
}
+19 -1
View File
@@ -81,10 +81,28 @@ 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
foreach (var playerInfo in GameNet.GameData.Players.Values)
{
if (playerInfo.ClientId == playerId)
{
if (playerInfo.OwnerId == ServerOwned)
{
_logger.LogInformation("Destroying PlayerInfo {nid}", playerInfo.NetId);
GameNet.GameData.RemovePlayer(playerInfo.PlayerId);
RemoveNetObject(playerInfo);
await SendObjectDespawn(playerInfo);
}
break;
}
}
return true;
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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; }
-29
View File
@@ -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": ""
}
}
+24 -2
View File
@@ -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]