Compare commits

..
51 Commits
Author SHA1 Message Date
miniduikboot 703915f06f Correct order of CheatCategory items (#748)
Impostor 1.10.8 introduced PacketSize as a category, which was added in
front of "Other". This caused the value of "Other" to shift, which is a
problem for AC checks. Let's prevent making this mistake again in 1.10.9

Previous commit already moved the Other category back, this commit puts in the note for future developers
2026-10-05 22:15:34 +02:00
Delusional Syndrome bf52365a1a Add friendcode && puid api (#743)
* Add friendcode && puid api

* Add Message 01/07 for client

* add anticheat
2026-10-05 21:00:43 +02:00
linmeideli e97c28574b Add dead condition for new role 2026-10-02 22:13:22 +02:00
miniduikboot 8ee5796d89 fix typo 2026-10-02 22:13:22 +02:00
miniduikboot 7b77165421 Address review comments 2026-10-02 22:13:22 +02:00
miniduikboot 196c01d5e3 Run Dumpostor 2026-10-02 22:13:22 +02:00
miniduikboot fd40e469c5 Bump version 2026-10-02 22:13:22 +02:00
miniduikboot 27b2fc5876 Add Rpc67 SpiritGuideMessage 2026-10-02 22:13:22 +02:00
miniduikboot 8bbe6be4b0 Add SpiritGuide role 2026-10-02 22:13:22 +02:00
miniduikboot 4f372ecabb Add new game version 2026-10-02 22:13:22 +02:00
linmeideli b09c40b7e1 fix 2026-09-22 20:41:01 +02:00
miniduikboot 5ee0fdb61f Bump version 2026-08-30 18:48:37 +02:00
miniduikboot 44ebc0c6b0 Rerun Dumpostor on 18.0 2026-08-30 18:48:37 +02:00
miniduikboot 681126dfb5 Address my own comments 2026-08-30 18:48:37 +02:00
linmeideli 9f627b7b01 clean up warns 2026-08-30 18:48:37 +02:00
linmeideli 510ed07389 Basically support AMCI 2026-08-30 18:48:37 +02:00
linmeideli 8f41030faf fix 17.4 support i thk 2026-08-30 18:48:37 +02:00
linmeideli f23dd8aa25 Support Judge and new vote 2026-08-30 18:48:37 +02:00
linmeideli 857e04dbc3 Support New HideNSeekGameOptions 2026-08-30 18:48:37 +02:00
miniduikboot b78f86f0f3 Fix compile warnings 2026-08-30 18:48:37 +02:00
miniduikboot db9c6ab66c Add special game version for mobile
Thanks Niko
2026-08-30 18:48:37 +02:00
miniduikboot a161aa7045 Add basic implementation of judge overruling 2026-08-30 18:48:37 +02:00
miniduikboot caab877c80 Add support for RoleOptions v11
This is bare minimum bringup for AU v18
2026-08-30 18:48:37 +02:00
miniduikboot 202b955db1 Deserialize gamecode as packed in PackedGameDataTo (#738)
* Deserialize gamecode as packed in PackedGameDataTo

Most packets use a normal 32 bit int but not PackedGameDataTo. Instead,
Innersloth decided to waste a byte by using WritePacked. As v2 game
codes are always negative, this is less efficient, but alas.

It was documented in the pseudocode and I didn't spot the difference
when reviewing the implementation, so I made this mistake when
deduplicating this code.

Test plan:
- Start EHR on at least 2 clients
- Use the game mode "Natural Disasters"
- Observe that pixelated blobs are visible for all players, not just the host

* uopt: stop parsing code to string

* Address review comments
2026-08-14 19:47:39 +02:00
NikoCat233andminiduikboot 1cd298608a Support new http matchmaking (#685)
* Add GameFilters interface

* Add Newtonsoft.Json

Used by InnerSloth

* Port 2025.3.25 Data

* Generate OptionNames

* Fix missing CategorizedOptionNames

* Remove Newtonsoft.Json

* Add missing GameFilterList

* Basic support for new filters

* Add FiltersController

Server need to tell clients its filters

* Add Tag filter in filtergames

* Add GameTags in legacy game options

* Correctly deserialize Filters

* Minor text fixes

---------

Co-authored-by: miniduikboot <mini@duikbo.at>
2026-08-13 22:03:04 +02:00
miniduikboot f0689ac2e2 Bump Hazel to 1.0.1 (#735)
This pulls in https://github.com/Impostor/Impostor.Hazel/pull/4
2026-07-01 19:02:24 +02:00
2cad4ffa14 Added an RPC packet size anti-cheat check with config (#731)
* Added an RPC packet size anti-cheat

* Support PackedGameDataTo

* Move packet size check to HazelConnection

* Update PacketSizeLimit to 1153 and add anti-cheat docs

* Change default to value determined on officials

Thanks Niko for doing the research
---------

Co-authored-by: miniduikboot <mini@duikbo.at>
Co-authored-by: NikoCat233 <139348239+NikoCat233@users.noreply.github.com>
2026-06-21 11:17:05 +02:00
miniduikboot e9437e781b Add compat versions for 17.4
Mobile has a different version than PC for reasons unknown at the
moment, probably related to ads.
2026-06-16 06:05:34 +02:00
miniduikboot 06f4f8bf00 Gate PackedGameDataTo behind AC check 2026-05-24 20:36:02 +02:00
NikoCat233 2cb9c648f3 Add support for PackedGameDataTo 2026-05-24 20:36:02 +02:00
miniduikboot b30775c2fc Add compat version for 17.3 (#724)
* Add compat version for 17.3

* Add compat version for 17.3.1
2026-04-20 22:00:32 +02:00
miniduikboot 764c42eae3 Merge pull request #723 from miniduikboot/fix/aof-concdict
Fix race conditions in Game.Data
2026-03-30 22:30:37 +02:00
miniduikboot 4244f39640 Rename _allObjectsFast to _allObjects
Done because there is no second datastructure anymore
2026-03-27 23:32:57 +01:00
miniduikboot 069ec446cd Switch to concurrent dictionary
PR proposed in #722 doesn't solve all issues, now Nullrefs show up.

Also drop _allObjects as it's never read.
2026-03-26 23:58:45 +01:00
miniduikboot 98783e5b42 Merge pull request #717 from Impostor/feature/17.2.2
Add 17.2.2 version
2026-03-22 23:06:45 +01:00
miniduikboot fd12cd1a39 Add 17.2.2 version
Only changes are ad related, and officials allow crossplay
2026-03-17 20:18:25 +01:00
miniduikboot 139f2334a2 Merge pull request #716 from Impostor/feature/17.2-hotfix
Add 17.2 hotfix version
2026-03-04 20:31:58 +01:00
miniduikboot 60371d538c Add 17.2 hotfix version 2026-02-19 21:34:54 +01:00
miniduikboot 6e68747fe0 Merge pull request #715 from Impostor/feature/17.2
Add 17.2 version
2026-02-18 07:24:47 +01:00
miniduikboot 3736b37831 Add 17.2 version
https://www.innersloth.com/announcing-among-us-x-ace-attorney-again/
2026-02-18 07:19:36 +01:00
miniduikboot eeb85e4abc Merge pull request #712 from miniduikboot/feature/aumi-host-chat-cmds
Add support for host chat cmds
2026-02-18 07:17:58 +01:00
miniduikboot fd33a892cd Reduce warnings about +25
As Innersloth documented it in their mod info document, assume it's here
to stay, so warn about it less heavily
2026-01-27 22:41:42 +01:00
miniduikboot fbdb708941 Implement host chat commands
On January 21 Innersloth released a document that details a few new
features. One of these are host chat commands: any chat message that
starts with "/cmd" is forwarded to the host and to the host only.

Implement this feature in Impostor too for implementation parity.

For more info, see their GitHub page:
https://github.com/Innersloth-LLC/AmongUsModdingInformation
2026-01-27 22:17:03 +01:00
miniduikboot 9a60ce3d56 Merge pull request #708 from Impostor/feature/17.1
Bump compatversion to support 17.1
2026-01-27 20:38:36 +01:00
miniduikboot d7601e8057 Add another mobile-only hotfix
From their Discord:

Admin has pushed hotfix v17.1.2 for mobile players! minor changes to the ad rewards to the daily cap (subject to future change) and updated the screen to clarify when the daily cap is reset.
2025-12-11 22:20:17 +01:00
miniduikbootandNiko233 0e9454ecdf Bump compatversion to support 17.1.1
https://www.innersloth.com/ad-rewards-v17-1-1/

Thanks to Niko for reminding me about this

Co-Authored-By: Niko233 <139348239+NikoCat233@users.noreply.github.com>
2025-12-07 10:33:23 +01:00
miniduikboot ce5a491cdb Bump compatversion to support 17.1 2025-11-18 20:50:57 +01:00
miniduikboot 16c8acc849 Merge pull request #706 from NikoCat233/patch-1
Add missing comment for game version 16.1.0
2025-11-18 20:50:46 +01:00
NikoCat233 195d31cad1 Add missing comment for game version 16.1.0 2025-11-05 01:35:52 +08:00
miniduikboot 18846d3dd4 Merge pull request #705 from Impostor/feature/17.0.1
Bump to 17.0.1
2025-10-15 20:42:47 +02:00
miniduikboot eb231dc393 Bump to 17.0.1 2025-10-14 20:29:33 +02:00
93 changed files with 62844 additions and 226 deletions

No files matched your search

+19 -14
View File
@@ -25,20 +25,25 @@ Impostor has an Http Server that is used by recent versions of Among Us to conne
Impostor has an Anticheat that makes it possible to kick cheaters from games automatically. Note that the anticheat is tuned on the vanilla version of the game, so client-side modifications could trigger the Anticheat if you're playing with them.
| Key | Default | Value |
|-------------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Enabled** | `true` | Whether the anticheat should be enabled. |
| **BanIpFromGame** | `true` | When anticheat is enabled and a player is caught hacking, they will be kicked from the server. If this value is set to `true`, the player will be banned instead and will not be able to rejoin that specific game. |
| **AllowCheatingHosts** | `"Never"` | Configure whether hosts are allowed to cheat. "Never" forbids it, "Always" allows it. "IfRequested" allows hosts to cheat if they connect with the DisableServerAuthorityFlag set. |
| **EnableGameFlowChecks** | `true` | Enable checks that check if certain actions are done in the appropriate order or at the appropriate moment in the game. This includes changing cosmetics while in game or murdering too fast. |
| **EnableInvalidObjectChecks** | `true` | Enables checks that check if network objects are spawned properly. Disabling this option also implies disabling EnableRoleChecks. |
| **EnableMustBeHostChecks** | `true` | Enables checks that check if players are the host before they can do actions that require them to be host of the game. This includes starting the game and spawning objects. |
| **EnableColorLimitChecks** | `true` | Enables checks that checks if players request colors that are already in use. |
| **EnableNameLimitChecks** | `true` | Enables checks that checks if player names have a length that is possible to set using the user interface. |
| **EnableOwnershipChecks** | `true` | Enables checks that check if players are allowed to perform a certain action on themself or another player. |
| **EnableRoleChecks** | `true` | Enables checks that check if players have the correct role when performing certain role abilities like venting or murdering. |
| **EnableTargetChecks** | `true` | Enables checks that check if certain packets to everyone that should only have been sent to certain players or vice versa. This includes sending votes or network objects. |
| **ForbidProtocolExtensions** | `true` | If disabled allows players to send network packets that go beyond the network packets sent by the vanilla game. This is necessary for most mods that need all players to install it. |
| 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. |
| **AllowHostOnlyExtensions** | "IfRequested" | Configure whether [Host-Only extensions](https://github.com/Innersloth-LLC/AmongUsModdingInformation#host-only-mods) are allowed in all games ("Always"), +25 games only ("IfRequested") or by nobody ("Never") |
| **EnableGameFlowChecks** | `true` | Enable checks that check if certain actions are done in the appropriate order or at the appropriate moment in the game. This includes changing cosmetics while in game or murdering too fast. |
| **EnableInvalidObjectChecks** | `true` | Enables checks that check if network objects are spawned properly. Disabling this option also implies disabling EnableRoleChecks. |
| **EnableMustBeHostChecks** | `true` | Enables checks that check if players are the host before they can do actions that require them to be host of the game. This includes starting the game and spawning objects. |
| **EnableColorLimitChecks** | `true` | Enables checks that checks if players request colors that are already in use. |
| **EnableNameLimitChecks** | `true` | Enables checks that checks if player names have a length that is possible to set using the user interface. |
| **EnableItemLimitChecks** | `true` | Enables checks for item counts that cannot be exceeded in the official clients. |
| **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. |
| **EnableIdentifierChecks** | `true` | Enables checks that verify if the friendcode and puid a host claims for a player in their PlayerInfo match the ones known for that player. |
| **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. |
| **EnablePacketSizeChecks** | `true` | Enables checks that verify if network messages exceed the maximum allowed size. |
| **PacketSizeLimit** | `1203` | The maximum allowed size (in bytes) for a network message. The official servers of Among Us requires that a Hazel packet is < 1204 bytes, excluding headers. |
### Compatibility
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>11</LangVersion>
<VersionPrefix>1.10.4</VersionPrefix>
<VersionPrefix>1.10.9</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<EnforceCodeStyleInBuild Condition="$(MSBuildProjectName)!='Hazel'">true</EnforceCodeStyleInBuild>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
@@ -85,6 +85,14 @@ public sealed class SourceGenerator : IIncrementalGenerator
enumGenerator.Generate("SystemTypes", underlyingType: CSharpEnumUnderlyingType.Byte);
enumGenerator.Generate("Language", sourceName: "SupportedLangs");
enumGenerator.Generate("TaskTypes");
enumGenerator.Generate("BoolOptionNames", "Impostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("ByteOptionNames", "Impostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("CategorizedOptionNames", "Impostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("FloatArrayOptionNames", "Impostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("FloatOptionNames", "Impostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("Int32ArrayOptionNames", "Impostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("Int32OptionNames", "Impostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("UInt32OptionNames", "Impostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("RpcCalls", "Impostor.Api.Net.Inner", underlyingType: CSharpEnumUnderlyingType.Byte);
@@ -151,6 +151,7 @@
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
}
}
},
".NETStandard,Version=v2.0/win-x64": {}
}
}
+13
View File
@@ -1,10 +1,14 @@
namespace Impostor.Api;
// NOTE: when adding items to this enum, add them to the bottom for API stability
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 host-only mod extension is used outside of host-authoritive mode.</summary>
HostOnlyExtension,
/// <summary>A packet was sent at an inappropriate moment.</summary>
GameFlow,
@@ -29,6 +33,15 @@ public enum CheatCategory
/// <summary>A packet was sent on an invalid network object, like a PlayerControl without PlayerInfo.</summary>
InvalidObject,
/// <summary>A packet was sent that exceeded the maximum allowed RPC size.</summary>
PacketSize,
/// <summary>Legacy category for unsorted anticheat checks.</summary>
Other,
/// <summary>A packet was sent with more items than possible in the game.</summary>
ItemLimits,
/// <summary>A packet contained an identifier (friendcode or puid) that does not match the one known for that player.</summary>
Identifiers,
}
@@ -10,6 +10,8 @@ namespace Impostor.Api.Config
public CheatingHostMode AllowCheatingHosts { get; set; } = CheatingHostMode.Never;
public CheatingHostMode AllowHostOnlyExtensions { get; set; } = CheatingHostMode.IfRequested;
public bool EnableGameFlowChecks { get; set; } = true;
public bool EnableMustBeHostChecks { get; set; } = true;
@@ -20,12 +22,20 @@ namespace Impostor.Api.Config
public bool EnableNameLimitChecks { get; set; } = true;
public bool EnableItemLimitChecks { get; set; } = true;
public bool EnableOwnershipChecks { get; set; } = true;
public bool EnableRoleChecks { get; set; } = true;
public bool EnableTargetChecks { get; set; } = true;
public bool EnableIdentifierChecks { get; set; } = true;
public bool ForbidProtocolExtensions { get; set; } = true;
public bool EnablePacketSizeChecks { get; set; } = true;
public int PacketSizeLimit { get; set; } = 1203;
}
}
@@ -1,4 +1,4 @@
using Impostor.Api.Net.Inner.Objects;
using Impostor.Api.Net.Inner.Objects;
namespace Impostor.Api.Events.Meeting
{
@@ -13,5 +13,15 @@ namespace Impostor.Api.Events.Meeting
/// Gets a value indicating whether meeting result is a tie.
/// </summary>
bool IsTie { get; }
/// <summary>
/// Gets a value indicating whether a Judge overruled the vote result.
/// </summary>
bool WasOverruled { get; }
/// <summary>
/// Gets the nonce of the winning Judge overrule, or 0 if the meeting wasn't overruled.
/// </summary>
ushort OverrideId { get; }
}
}
@@ -1,4 +1,4 @@
namespace Impostor.Api.Events.Player
namespace Impostor.Api.Events.Player
{
public interface IPlayerChatEvent : IPlayerEvent, IEventCancelable
{
@@ -6,5 +6,12 @@
/// Gets the message sent by the player.
/// </summary>
string Message { get; }
/// <summary>
/// Gets or sets a value indicating whether the message is sent to
/// all players (true) or only to the host (false).
/// To not send this message at all, cancel this event instead.
/// </summary>
bool SendToAllPlayers { get; set; }
}
}
+7 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Impostor.Api.Innersloth;
@@ -47,6 +48,11 @@ namespace Impostor.Api.Games
/// </remarks>
bool IsHostAuthoritive { get; }
/// <summary>
/// Gets the mod GUID that the game was registered with through the AMCI.
/// </summary>
Guid? ModGuid { get; }
IClientPlayer? GetClientPlayer(int clientId);
T? FindObjectByNetId<T>(uint netId)
+1 -1
View File
@@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />
<PackageReference Include="Impostor.Hazel.Abstractions" Version="1.0.0" />
<PackageReference Include="Impostor.Hazel.Abstractions" Version="1.0.1" />
<ProjectReference Include="..\Impostor.Api.Innersloth.Generator\Impostor.Api.Innersloth.Generator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
<AdditionalFiles Include="Innersloth/Data/**/*.json" />
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,17 @@
{
"Invalid": 0,
"VisualTasks": 1,
"GhostsDoTasks": 2,
"ConfirmImpostor": 3,
"AnonymousVotes": 4,
"IsDefaults": 5,
"UseFlashlight": 6,
"SeekerFinalVents": 7,
"SeekerFinalMap": 8,
"SeekerPings": 9,
"ShowCrewmateNames": 10,
"Roles": 11,
"ShapeshifterLeaveSkin": 1000,
"ImpostorsCanSeeProtect": 1100,
"NoisemakerImpostorAlert": 1300
}
@@ -0,0 +1,4 @@
{
"Invalid": 0,
"MapId": 1
}
@@ -0,0 +1,14 @@
{
"KillCooldown": 0,
"PlayerSpeed": 1,
"EmergencyMeetingCount": 2,
"EmergencyMeetingCooldown": 3,
"DiscussionTime": 4,
"VotingTime": 5,
"CrewmateVision": 6,
"HidingTime": 7,
"MaxTimeInVent": 8,
"ImposterVision": 9,
"FinalHideTime": 10,
"FinalHideImposterSpeed": 11
}
@@ -0,0 +1,4 @@
{
"Invalid": 0,
"KillDistances": 1
}
@@ -0,0 +1,32 @@
{
"Invalid": 0,
"KillCooldown": 1,
"PlayerSpeedMod": 2,
"ImpostorLightMod": 3,
"CrewLightMod": 4,
"CrewmateTimeInVent": 100,
"FinalEscapeTime": 101,
"EscapeTime": 102,
"SeekerFinalSpeed": 103,
"MaxPingTime": 104,
"CrewmateFlashlightSize": 105,
"ImpostorFlashlightSize": 106,
"ShapeshifterCooldown": 1000,
"ShapeshifterDuration": 1001,
"ProtectionDurationSeconds": 1100,
"GuardianAngelCooldown": 1101,
"ScientistCooldown": 1200,
"ScientistBatteryCharge": 1201,
"EngineerCooldown": 1300,
"EngineerInVentMaxTime": 1301,
"PhantomCooldown": 1500,
"PhantomDuration": 1501,
"TrackerCooldown": 1550,
"TrackerDuration": 1551,
"TrackerDelay": 1552,
"NoisemakerAlertDuration": 1600,
"ViperDissolveTime": 1601,
"DetectiveSuspectLimit": 1602,
"JudgeTaskRequirementPercentage": 1620,
"SpiritGuideCooldownSeconds": 1640
}
@@ -0,0 +1,5 @@
{
"Invalid": 0,
"MaxImpostors": 1,
"MinPlayers": 2
}
@@ -0,0 +1,21 @@
{
"Invalid": 0,
"NumImpostors": 1,
"KillDistance": 2,
"NumEmergencyMeetings": 3,
"EmergencyCooldown": 4,
"DiscussionTime": 5,
"VotingTime": 6,
"MaxImpostors": 7,
"MinPlayers": 8,
"MaxPlayers": 9,
"NumCommonTasks": 10,
"NumShortTasks": 11,
"NumLongTasks": 12,
"TaskBarMode": 13,
"Tag": 14,
"CrewmatesRemainingForVitals": 100,
"CrewmateVentUses": 101,
"ImpostorPlayerID": 102,
"RulePreset": 200
}
@@ -11,5 +11,7 @@
"Phantom": 9,
"Tracker": 10,
"Detective": 12,
"Viper": 18
"Viper": 18,
"Judge": 19,
"SpiritGuide": 21
}
@@ -59,5 +59,7 @@
"CheckVanish": 62,
"StartVanish": 63,
"CheckAppear": 64,
"StartAppear": 65
"StartAppear": 65,
"QueueOverruleVotes": 66,
"SpiritGuideMessage": 67
}
@@ -626,6 +626,14 @@
"SanctionSelfHarmPromotion": 1720,
"SanctionRepeatOffender10": 1721,
"SanctionUnknown": 1722,
"ReplicatorAbility": 1800,
"ReplicatorRole": 1801,
"ReplicatorBlurb": 1802,
"ReplicatorBlurbMed": 1803,
"ReplicatorBlurbLong": 1804,
"ReplicatorCooldown": 1805,
"ReplicatorDuration": 1806,
"ReplicatorQuantity": 1807,
"ScreenShakeOption": 1900,
"FeaturedItems": 1901,
"FeaturedBundles": 1902,
@@ -1576,6 +1584,14 @@
"XboxCommsCreateGameError": 3702,
"XboxCommsSearchError": 3703,
"XboxCommsJoinError": 3704,
"WatchAdBody": 3800,
"WatchAdTitle": 3801,
"WatchAd": 3802,
"RewardAdTitle": 3803,
"RewardAdBody1": 3804,
"RewardAdBody2": 3805,
"WatchAdErrorBody": 3806,
"WatchAdErrorTitle": 3807,
"DetectiveRole": 4000,
"DetectiveAbility": 4001,
"DetectiveAbilityNotes": 4002,
@@ -1623,5 +1639,50 @@
"DetectiveNotesPrepositionMaybe": 4079,
"DetectiveNotesTitlePostIt": 4080,
"DetectiveNotesPostIt": 4081,
"DetectiveNotesSuspectNumber": 4082
"DetectiveNotesSuspectNumber": 4082,
"AprilFoolsMode": 4083,
"MatchInfoGuideTitle": 4084,
"MatchInfoGuidePlayersLabel": 4085,
"MatchInfoGuideNoRolesMessage": 4086,
"JudgeRole": 4090,
"JudgeAbility": 4091,
"JudgeBlurb": 4092,
"JudgeBlurbMed": 4093,
"JudgeBlurbLong": 4094,
"JudgeTaskRequirement": 4095,
"RolesHelp_Judge_01": 4096,
"RolesHelp_Judge_02": 4097,
"JudgeAbilityUnlocked": 4098,
"JudgeAbilityProgress": 4099,
"JudgeCourtAdjourned": 4100,
"JudgeHasSpoken": 4101,
"StatsJudgeSuccessfulOverrules": 4102,
"StatsJudgeFailedOverrules": 4103,
"QCJudges": 4104,
"QCJudgeOverrule": 4105,
"QCJudgePlease": 4106,
"QCJudgeWillOverrule": 4107,
"JudgeBeatYouToIt": 4108,
"SpiritGuideRole": 4120,
"SpiritGuideAbility": 4121,
"SpiritGuideBlurb": 4122,
"SpiritGuideBlurbMed": 4123,
"SpiritGuideBlurbLong": 4124,
"SpiritGuideCooldown": 4125,
"SpiritGuideAbilityCancel": 4126,
"SpiritGuideAbilitySend": 4127,
"SpiritGuideAbilityRefresh": 4128,
"StatsSpiritGuideSentMessage": 4129,
"RolesHelp_SpiritGuide_01": 4130,
"RolesHelp_SpiritGuide_02": 4131,
"QCInfluencerMessage": 4132,
"QCBecauseInfluencer": 4133,
"QCAnyInfluencerMessage": 4134,
"QCInfluencers": 4135,
"RedeemCodeLabel": 4200,
"CodeLengthLabel": 4201,
"CodeErrorLabel": 4202,
"CodeAlreadyRedeemedLabel": 4203,
"CodeRedeemPopupLabel": 4204,
"HorseModeToggle": 4300
}
@@ -0,0 +1,4 @@
{
"Invalid": 0,
"Keywords": 1
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"dumpostorVersion": "1.0.1-dev",
"gameVersion": "2025.9.9",
"dumpostorVersion": "1.0.2-dev",
"gameVersion": "2026.9.29",
"platformType": "StandaloneItch"
}
@@ -0,0 +1,37 @@
{
"hatIds": [
"hat_pk04_beanie",
"hat_pk05_Cheese",
"hat_pk05_Cherry",
"hat_Chocolate",
"hat_Zipper",
"hat_pk02_Crown",
"hat_babybean",
"hat_Sorry",
"hat_mareLwyd",
"hat_fishingHat",
"hat_schnapp",
"hat_pk03_Traffic",
"hat_Pot",
"hat_AbominalHat",
"hat_tombstone",
"hat_pk04_MiniCrewmate",
"hat_pk05_Wizardhat",
"hat_pk05_Ellryhat",
"hat_pk05_EllieToppat"
],
"visorIds": [
"visor_pk01_AngeryVisor",
"visor_pk01_DumStickerVisor",
"visor_vr_Vr-Black",
"visor_pk01_PaperMaskVisor",
"visor_wash"
],
"petIds": [
"pet_YuleGoatPet",
"pet_HamPet",
"pet_Lava",
"pet_Snow",
"pet_Bush"
]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class BoolGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "bool";
[JsonPropertyName("AcceptedValues")]
public required List<bool> AcceptedValues { get; set; }
[JsonPropertyName("OptionEnum")]
public required BoolOptionNames OptionEnum { get; set; }
}
}
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class CategorizedGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "cat";
[JsonPropertyName("AcceptedValues")]
public required List<int> AcceptedValues { get; set; }
[JsonPropertyName("OptionEnum")]
public required CategorizedOptionNames OptionEnum { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class ChatModeGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "chat";
[JsonPropertyName("AcceptedValues")]
public required byte AcceptedValues { get; set; }
}
}
@@ -0,0 +1,87 @@
using System;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class GameFilter
{
[JsonConstructor]
public GameFilter(string optionType, string key, string subFilterString)
{
this.OptionType = optionType;
this.Key = key;
this.SubFilterString = subFilterString;
this.SubFilter = ResolveSubFilter(OptionType, SubFilterString);
}
[JsonIgnore]
public ISubFilter SubFilter { get; set; }
[JsonPropertyName("OptionType")]
public required string OptionType { get; set; }
[JsonPropertyName("Key")]
public required string Key { get; set; }
[JsonPropertyName("SubFilterString")]
public required string SubFilterString { get; set; }
public void ModifySubFilter(ISubFilter subFilter)
{
this.SubFilter = subFilter;
}
[OnSerializing]
internal void OnSerializing(StreamingContext context)
{
this.SubFilterString = JsonSerializer.Serialize(this.SubFilter);
}
[OnDeserialized]
internal void OnDeserialized(StreamingContext context)
{
this.SubFilter = this.ResolveSubFilter(this.OptionType, this.SubFilterString);
}
private ISubFilter ResolveSubFilter(string type, string filterString)
{
if (type != null)
{
switch (type.ToLower())
{
case "int":
return JsonSerializer.Deserialize<IntGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for IntGameFilter");
case "platform":
return JsonSerializer.Deserialize<PlatformGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for PlatformGameFilter");
case "cat":
return JsonSerializer.Deserialize<CategorizedGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for CategorizedGameFilter");
case "languages":
return JsonSerializer.Deserialize<LanguageFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for LanguageFilter");
case "chat":
return JsonSerializer.Deserialize<ChatModeGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for ChatModeGameFilter");
case "map":
return JsonSerializer.Deserialize<MapGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for MapGameFilter");
case "bool":
return JsonSerializer.Deserialize<BoolGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for BoolGameFilter");
case "mod":
return JsonSerializer.Deserialize<ModFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for ModFilter");
default:
throw new InvalidOperationException("No type matches subfilter");
}
}
throw new InvalidOperationException("Must provide type for sub filter");
}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class GameFilterSet
{
[JsonConstructor]
public GameFilterSet(GameModes gameMode, List<GameFilter> filters)
{
GameMode = gameMode;
Filters = filters;
}
[JsonPropertyName("GameMode")]
public required GameModes GameMode { get; set; }
[JsonPropertyName("Filters")]
public required List<GameFilter> Filters { get; set; }
}
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class GameFiltersList
{
[JsonPropertyName("FilterSets")]
public required List<GameFilterSet> FilterSets { get; set; }
}
}
@@ -0,0 +1,7 @@
namespace Impostor.Api.Innersloth.GameFilters
{
public interface ISubFilter
{
string FilterType { get; }
}
}
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class IntGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "int";
[JsonPropertyName("AcceptedValues")]
public required List<int> AcceptedValues { get; set; }
[JsonPropertyName("OptionEnum")]
public required Int32OptionNames OptionEnum { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class LanguageFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "languages";
[JsonPropertyName("AcceptedValues")]
public required uint AcceptedValues { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class MapGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "map";
[JsonPropertyName("AcceptedValues")]
public required byte AcceptedValues { get; set; }
}
}
@@ -0,0 +1,16 @@
using System;
using System.Text.Json.Serialization;
// https://github.com/Innersloth-LLC/AmongUsModdingInformation
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class ModFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "mod";
[JsonPropertyName("AcceptedValues")]
public required Guid AcceptedValues { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class PlatformGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "platform";
[JsonPropertyName("AcceptedValues")]
public required uint AcceptedValues { get; set; }
}
}
@@ -2,7 +2,7 @@ namespace Impostor.Api.Innersloth.GameOptions;
public class HideNSeekGameOptions : IGameOptions
{
public const int LatestVersion = 10;
public const int LatestVersion = 12;
public HideNSeekGameOptions(byte version = LatestVersion)
{
@@ -107,6 +107,11 @@ public class HideNSeekGameOptions : IGameOptions
public void Deserialize(IMessageReader reader)
{
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
}
if (Version >= 8)
{
SpecialMode = (SpecialGameModes)reader.ReadByte();
@@ -142,11 +147,6 @@ public class HideNSeekGameOptions : IGameOptions
{
Tag = (GameTags)reader.ReadByte();
}
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
}
}
public void Serialize(IMessageWriter writer)
@@ -49,6 +49,8 @@ public interface IGameOptions
/// </summary>
public bool IsDefaults { get; set; }
public GameTags Tag { get; set; }
public void Serialize(IMessageWriter writer);
public static void EnsureVersionIsModular<TCaller>(byte version)
@@ -145,6 +145,8 @@ public class LegacyGameOptionsData : IGameOptions
/// </summary>
public bool IsDefaults { get; set; } = true;
public GameTags Tag { get; set; } = 0;
public static LegacyGameOptionsData Deserialize(IMessageReader reader, byte version)
{
var options = new LegacyGameOptionsData(version);
@@ -4,7 +4,7 @@ namespace Impostor.Api.Innersloth.GameOptions;
public class NormalGameOptions : IGameOptions
{
public const int LatestVersion = 10;
public const int LatestVersion = 12;
public NormalGameOptions(byte version = LatestVersion)
{
@@ -144,6 +144,11 @@ public class NormalGameOptions : IGameOptions
public void Deserialize(IMessageReader reader)
{
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
}
if (Version >= 8)
{
SpecialMode = (SpecialGameModes)reader.ReadByte();
@@ -184,11 +189,6 @@ public class NormalGameOptions : IGameOptions
}
RoleOptions.Deserialize(reader);
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
}
}
public void Serialize(IMessageWriter writer)
@@ -0,0 +1,29 @@
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
public class JudgeRoleOptions : IRoleOptions
{
public JudgeRoleOptions(byte version)
{
Version = version;
}
public byte Version { get; }
public RoleTypes Type => RoleTypes.Judge;
public float JudgeTaskRequirementPercentage { get; set; } = 50f;
public static JudgeRoleOptions Deserialize(IMessageReader reader, byte version)
{
var options = new JudgeRoleOptions(version);
options.JudgeTaskRequirementPercentage = (int)reader.ReadByte();
return options;
}
public void Serialize(IMessageWriter writer)
{
writer.Write((byte)JudgeTaskRequirementPercentage);
}
}
@@ -34,6 +34,8 @@ public class RoleOptionsCollection
RoleTypes.Tracker => TrackerRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Detective => DetectiveRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Viper => ViperRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Judge => JudgeRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.SpiritGuide => SpiritGuideRoleOptions.Deserialize(roleOptionsReader, Version),
_ => throw new ArgumentOutOfRangeException(nameof(roleType), roleType, null),
};
@@ -0,0 +1,29 @@
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
public class SpiritGuideRoleOptions : IRoleOptions
{
public SpiritGuideRoleOptions(byte version)
{
Version = version;
}
public byte Version { get; }
public RoleTypes Type => RoleTypes.SpiritGuide;
public float SpiritGuideCooldownSeconds { get; set; } = 20f;
public static SpiritGuideRoleOptions Deserialize(IMessageReader reader, byte version)
{
var options = new SpiritGuideRoleOptions(version);
options.SpiritGuideCooldownSeconds = reader.ReadByte();
return options;
}
public void Serialize(IMessageWriter writer)
{
writer.Write((byte)SpiritGuideCooldownSeconds);
}
}
+16
View File
@@ -60,6 +60,22 @@ namespace Impostor.Api.Net
/// </remarks>
IDictionary<object, object> Items { get; }
/// <summary>
/// Gets or sets the friendcode of the player.
/// </summary>
/// <remarks>
/// Impostor's HTTP server does not resolve friendcodes, so this value will remain null unless a plugin has written a value in this field.
/// </remarks>
string? FriendCode { get; set; }
/// <summary>
/// Gets or sets the puid of the player.
/// </summary>
/// <remarks>
/// Impostor's HTTP server does not resolve puids, so this value will remain null unless a plugin has written a value in this field.
/// </remarks>
string? Puid { get; set; }
/// <summary>
/// Gets the current game data of the <see cref="IClient" />.
/// </summary>
+1 -1
View File
@@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Impostor.Api.Games;
using Impostor.Api.Net.Inner;
using Impostor.Api.Net.Inner.Objects;
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Impostor.Api.Events.Player;
@@ -6,6 +6,24 @@ namespace Impostor.Api.Net.Inner.Objects
{
public interface IInnerMeetingHud : IInnerNetObject
{
public interface IJudgeOverrule
{
/// <summary>
/// Gets the player id of the Judge that queued this overrule.
/// </summary>
byte JudgePlayerId { get; }
/// <summary>
/// Gets the player id of the player the Judge wants to overrule.
/// </summary>
byte OverruledPlayerId { get; }
/// <summary>
/// Gets the overrule nonce used to match this overrule to the voting result.
/// </summary>
ushort OverruleNonce { get; }
}
public interface IPlayerVoteArea
{
/// <summary>
@@ -55,5 +73,10 @@ namespace Impostor.Api.Net.Inner.Objects
/// Gets the player that started the meeting.
/// </summary>
IInnerPlayerInfo? Reporter { get; }
/// <summary>
/// Gets the Judge overrules queued in this meeting.
/// </summary>
IReadOnlyCollection<IJudgeOverrule> JudgeOverrules { get; }
}
}
@@ -43,5 +43,21 @@ namespace Impostor.Api.Net.Inner.Objects
DateTimeOffset LastMurder { get; }
uint PlayerLevel { get; }
/// <summary>
/// Gets the friendcode of the player.
/// </summary>
/// <remarks>
/// Impostor's HTTP server does not resolve friendcodes, so this value will remain null unless a plugin has written a value on the player's client.
/// </remarks>
string? FriendCode { get; }
/// <summary>
/// Gets the puid of the player.
/// </summary>
/// <remarks>
/// Impostor's HTTP server does not resolve puids, so this value will remain null unless a plugin has written a value on the player's client.
/// </remarks>
string? Puid { get; }
}
}
@@ -1,4 +1,4 @@
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameOptions;
namespace Impostor.Api.Net.Messages.C2S
@@ -0,0 +1,25 @@
using System;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameOptions;
namespace Impostor.Api.Net.Messages.C2S
{
/// <summary>
/// Special serialization rules for AU MCI HostGame messages.
/// <see href="https://github.com/Innersloth-LLC/AmongUsModdingInformation?tab=readme-ov-file#all-client-mod-client-identification"/>
/// for more information.
/// </summary>
public static class Message25HostModdedGameC2S
{
public static void Serialize(IMessageWriter writer, IGameOptions gameOptions, CrossplayFlags crossplayFlags, GameFilterOptions gameFilterOptions)
{
Message00HostGameC2S.Serialize(writer, gameOptions, crossplayFlags, gameFilterOptions);
}
public static void Deserialize(IMessageReader reader, out IGameOptions gameOptions, out CrossplayFlags crossplayFlags, out GameFilterOptions gameFilterOptions, out Guid modGuid)
{
Message00HostGameC2S.Deserialize(reader, out gameOptions, out crossplayFlags, out gameFilterOptions);
modGuid = new Guid(reader.ReadBytes(16).Span);
}
}
}
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -29,6 +29,8 @@ namespace Impostor.Api.Net.Messages
public const byte QueryPlatformIds = 22;
public const byte QueryLobbyInfo = 23;
public const byte EndGameHostMigration = 24;
public const byte HostModdedGame = 25;
public const byte PackedGameDataTo = 26;
private static readonly Dictionary<byte, string> FlagCache;
@@ -40,6 +42,11 @@ namespace Impostor.Api.Net.Messages
.ToDictionary(x => (byte)x.GetValue(null)!, y => y.Name);
}
/// <summary>
/// Convert a MessageFlag to the corresponding string.
/// </summary>
/// <param name="flag">The MessageFlag to convert.</param>
/// <returns>A string corresponding with the value of flag.</returns>
public static string FlagToString(byte flag)
{
return FlagCache.TryGetValue(flag, out var res) ? res : $"Unknown Flag {flag}";
@@ -2,6 +2,8 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc23VotingComplete
{
// Pre 18.0 version
// TODO clean up when 17.4 and below are unsupported
public static void Serialize(IMessageWriter writer, byte[] states, byte playerId, bool tie)
{
writer.WriteBytesAndSize(states);
@@ -9,6 +11,15 @@ namespace Impostor.Api.Net.Messages.Rpcs
writer.Write(tie);
}
public static void Serialize(IMessageWriter writer, byte[] states, byte playerId, bool tie, bool wasOverruled, ushort overrideId)
{
writer.WriteBytesAndSize(states);
writer.Write(playerId);
writer.Write(tie);
writer.Write(wasOverruled);
writer.Write(overrideId);
}
public static void Deserialize(IMessageReader reader, out IMessageReader[] states, out byte playerId, out bool tie)
{
var length = reader.ReadPackedInt32();
@@ -21,5 +32,21 @@ namespace Impostor.Api.Net.Messages.Rpcs
playerId = reader.ReadByte();
tie = reader.ReadBoolean();
}
public static void Deserialize(IMessageReader reader, out IMessageReader[] states, out byte playerId, out bool tie, out bool wasOverruled, out ushort overrideId)
{
var length = reader.ReadPackedInt32();
states = new IMessageReader[length];
for (var i = 0; i < length; i++)
{
states[i] = reader.ReadMessage();
}
playerId = reader.ReadByte();
tie = reader.ReadBoolean();
wasOverruled = reader.ReadBoolean();
overrideId = reader.ReadUInt16();
}
}
}
@@ -0,0 +1,19 @@
namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc66QueueOverruleVotes
{
public static void Serialize(IMessageWriter writer, byte judgePlayerId, byte targetPlayerId, ushort overruleNonce)
{
writer.Write(judgePlayerId);
writer.Write(targetPlayerId);
writer.Write(overruleNonce);
}
public static void Deserialize(IMessageReader reader, out byte judgePlayerId, out byte targetPlayerId, out ushort overruleNonce)
{
judgePlayerId = reader.ReadByte();
targetPlayerId = reader.ReadByte();
overruleNonce = reader.ReadUInt16();
}
}
}
@@ -0,0 +1,17 @@
using System;
namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc67SpiritGuideMessage
{
public static void Serialize(IMessageWriter writer, byte[] images)
{
writer.WriteBytesAndSize(images);
}
public static void Deserialize(IMessageReader reader, out ReadOnlyMemory<byte> images)
{
images = reader.ReadBytesAndSize();
}
}
}
@@ -19,10 +19,8 @@ namespace Impostor.Api.Net.Messages.S2C
writer.Write(player.Client.Name);
player.Client.PlatformSpecificData.Serialize(writer);
writer.WritePacked(player.Character?.PlayerInfo?.PlayerLevel ?? 1);
// ProductUserId and FriendCode are not yet known, so set them to an empty string
writer.Write(string.Empty);
writer.Write(string.Empty);
writer.Write(player.Client.FriendCode ?? string.Empty);
writer.Write(player.Client.Puid ?? string.Empty);
writer.EndMessage();
}
@@ -23,10 +23,8 @@ namespace Impostor.Api.Net.Messages.S2C
writer.Write(ply.Client.Name);
ply.Client.PlatformSpecificData.Serialize(writer);
writer.WritePacked(ply.Character?.PlayerInfo?.PlayerLevel ?? 1);
// ProductUserId and FriendCode are not yet known, so set them to an empty string
writer.Write(string.Empty);
writer.Write(string.Empty);
writer.Write(ply.Client.FriendCode ?? string.Empty);
writer.Write(ply.Client.Puid ?? string.Empty);
}
writer.EndMessage();
+11 -4
View File
@@ -4,9 +4,9 @@
"net8.0": {
"Impostor.Hazel.Abstractions": {
"type": "Direct",
"requested": "[1.0.0, )",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
"requested": "[1.0.1, )",
"resolved": "1.0.1",
"contentHash": "de2bMj78xDXOWwyqQVk8o4vbZKGfAgOjnWnkKB1IGGovNtxLeBF8jy3wH5moTZB6juTnLf0ptkCDkRi8c2OcjA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Direct",
@@ -40,6 +40,12 @@
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.30, )",
"resolved": "8.0.30",
"contentHash": "SfmwuZjUPBTEoBMAsXZ/hWkaEK9oL+Y/UtAx7N6RyWHfcMt9J0ExrNPuOgTyPbCXIyiTBFsxFNrPLEZNPmJcvA=="
},
"StyleCop.Analyzers": {
"type": "Direct",
"requested": "[1.2.0-beta.556, )",
@@ -117,6 +123,7 @@
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
}
}
},
"net8.0/win-x64": {}
}
}
+10 -10
View File
@@ -42,19 +42,19 @@
},
"Impostor.Hazel": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "FgVKuIWK+o6Q+wOPFoZZTeoSuVh4giAhrS8Zvs6O0++Bg8RxH4Lw51uSwnexLiwosXveUCIXICzgGHmRyguVSA==",
"resolved": "1.0.1",
"contentHash": "DFsZcOa9ptFEj8FCfK63L/bcJ9RUVRZUhUS0qkrmdSqkgeJRdvUOTWR1gKlqbaOJcSEvexAITC0QJEPnJUcI7Q==",
"dependencies": {
"Impostor.Hazel.Abstractions": "1.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.ObjectPool": "7.0.1",
"Impostor.Hazel.Abstractions": "1.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.ObjectPool": "8.0.0",
"Serilog": "2.12.0"
}
},
"Impostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
"resolved": "1.0.1",
"contentHash": "de2bMj78xDXOWwyqQVk8o4vbZKGfAgOjnWnkKB1IGGovNtxLeBF8jy3wH5moTZB6juTnLf0ptkCDkRi8c2OcjA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
@@ -588,7 +588,7 @@
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Impostor.Hazel.Abstractions": "[1.0.1, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
@@ -597,8 +597,8 @@
"impostor.server": {
"type": "Project",
"dependencies": {
"Impostor.Api": "[1.9.0-dev, )",
"Impostor.Hazel": "[1.0.0, )",
"Impostor.Api": "[1.10.7-dev, )",
"Impostor.Hazel": "[1.0.1, )",
"Microsoft.Extensions.FileSystemGlobbing": "[8.0.0, )",
"Microsoft.Extensions.Hosting": "[8.0.0, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",
+12 -12
View File
@@ -13,19 +13,19 @@
},
"Impostor.Hazel": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "FgVKuIWK+o6Q+wOPFoZZTeoSuVh4giAhrS8Zvs6O0++Bg8RxH4Lw51uSwnexLiwosXveUCIXICzgGHmRyguVSA==",
"resolved": "1.0.1",
"contentHash": "DFsZcOa9ptFEj8FCfK63L/bcJ9RUVRZUhUS0qkrmdSqkgeJRdvUOTWR1gKlqbaOJcSEvexAITC0QJEPnJUcI7Q==",
"dependencies": {
"Impostor.Hazel.Abstractions": "1.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.ObjectPool": "7.0.1",
"Impostor.Hazel.Abstractions": "1.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.ObjectPool": "8.0.0",
"Serilog": "2.12.0"
}
},
"Impostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
"resolved": "1.0.1",
"contentHash": "de2bMj78xDXOWwyqQVk8o4vbZKGfAgOjnWnkKB1IGGovNtxLeBF8jy3wH5moTZB6juTnLf0ptkCDkRi8c2OcjA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
@@ -97,8 +97,8 @@
},
"Microsoft.Extensions.ObjectPool": {
"type": "Transitive",
"resolved": "7.0.1",
"contentHash": "dDFItid/TGJljAnqbUQ0PkAl6ShIG0htwRR8J0cG/5Il6lIZPfkIZMwrZTWiF23AiQcqt6suPaUy2ih6dsCB9Q=="
"resolved": "8.0.0",
"contentHash": "4pm+XgxSukskwjzDDfSjG4KNUIOdFF2VaqZZDtTzoyQMOVSnlV6ZM8a9aVu5dg9LVZTB//utzSc8fOi0b0Mb2Q=="
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
@@ -132,7 +132,7 @@
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Impostor.Hazel.Abstractions": "[1.0.1, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
@@ -141,8 +141,8 @@
"impostor.client": {
"type": "Project",
"dependencies": {
"Impostor.Api": "[1.9.0-dev, )",
"Impostor.Hazel": "[1.0.0, )"
"Impostor.Api": "[1.10.7-dev, )",
"Impostor.Hazel": "[1.0.1, )"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<ItemGroup>
<ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
<PackageReference Include="Impostor.Hazel" Version="1.0.0" />
<PackageReference Include="Impostor.Hazel" Version="1.0.1" />
</ItemGroup>
</Project>
+11 -11
View File
@@ -4,20 +4,20 @@
"net8.0": {
"Impostor.Hazel": {
"type": "Direct",
"requested": "[1.0.0, )",
"resolved": "1.0.0",
"contentHash": "FgVKuIWK+o6Q+wOPFoZZTeoSuVh4giAhrS8Zvs6O0++Bg8RxH4Lw51uSwnexLiwosXveUCIXICzgGHmRyguVSA==",
"requested": "[1.0.1, )",
"resolved": "1.0.1",
"contentHash": "DFsZcOa9ptFEj8FCfK63L/bcJ9RUVRZUhUS0qkrmdSqkgeJRdvUOTWR1gKlqbaOJcSEvexAITC0QJEPnJUcI7Q==",
"dependencies": {
"Impostor.Hazel.Abstractions": "1.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.ObjectPool": "7.0.1",
"Impostor.Hazel.Abstractions": "1.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.ObjectPool": "8.0.0",
"Serilog": "2.12.0"
}
},
"Impostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
"resolved": "1.0.1",
"contentHash": "de2bMj78xDXOWwyqQVk8o4vbZKGfAgOjnWnkKB1IGGovNtxLeBF8jy3wH5moTZB6juTnLf0ptkCDkRi8c2OcjA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
@@ -89,8 +89,8 @@
},
"Microsoft.Extensions.ObjectPool": {
"type": "Transitive",
"resolved": "7.0.1",
"contentHash": "dDFItid/TGJljAnqbUQ0PkAl6ShIG0htwRR8J0cG/5Il6lIZPfkIZMwrZTWiF23AiQcqt6suPaUy2ih6dsCB9Q=="
"resolved": "8.0.0",
"contentHash": "4pm+XgxSukskwjzDDfSjG4KNUIOdFF2VaqZZDtTzoyQMOVSnlV6ZM8a9aVu5dg9LVZTB//utzSc8fOi0b0Mb2Q=="
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
@@ -124,7 +124,7 @@
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Impostor.Hazel.Abstractions": "[1.0.1, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
@@ -4,8 +4,8 @@
"net8.0": {
"Impostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
"resolved": "1.0.1",
"contentHash": "de2bMj78xDXOWwyqQVk8o4vbZKGfAgOjnWnkKB1IGGovNtxLeBF8jy3wH5moTZB6juTnLf0ptkCDkRi8c2OcjA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
@@ -102,7 +102,7 @@
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Impostor.Hazel.Abstractions": "[1.0.1, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
@@ -4,8 +4,8 @@
"net8.0": {
"Impostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
"resolved": "1.0.1",
"contentHash": "de2bMj78xDXOWwyqQVk8o4vbZKGfAgOjnWnkKB1IGGovNtxLeBF8jy3wH5moTZB6juTnLf0ptkCDkRi8c2OcjA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
@@ -102,7 +102,7 @@
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Impostor.Hazel.Abstractions": "[1.0.1, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
@@ -1,4 +1,4 @@
using Impostor.Api.Events.Meeting;
using Impostor.Api.Events.Meeting;
using Impostor.Api.Games;
using Impostor.Api.Net.Inner.Objects;
@@ -6,12 +6,14 @@ namespace Impostor.Server.Events.Meeting
{
public class MeetingEndedEvent : IMeetingEndedEvent
{
public MeetingEndedEvent(IGame game, IInnerMeetingHud meetingHud, IInnerPlayerControl? exiled, bool isTie)
public MeetingEndedEvent(IGame game, IInnerMeetingHud meetingHud, IInnerPlayerControl? exiled, bool isTie, bool wasOverruled = false, ushort overrideId = 0)
{
Game = game;
MeetingHud = meetingHud;
Exiled = exiled;
IsTie = isTie;
WasOverruled = wasOverruled;
OverrideId = overrideId;
}
public IGame Game { get; }
@@ -21,5 +23,9 @@ namespace Impostor.Server.Events.Meeting
public IInnerPlayerControl? Exiled { get; }
public bool IsTie { get; }
public bool WasOverruled { get; }
public ushort OverrideId { get; }
}
}
@@ -1,4 +1,4 @@
using Impostor.Api.Events.Player;
using Impostor.Api.Events.Player;
using Impostor.Api.Games;
using Impostor.Api.Net;
using Impostor.Api.Net.Inner.Objects;
@@ -24,5 +24,7 @@ namespace Impostor.Server.Events.Player
public string Message { get; }
public bool IsCancelled { get; set; }
public bool SendToAllPlayers { get; set; } = true;
}
}
@@ -0,0 +1,56 @@
using System;
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
namespace Impostor.Server.Http;
[Route("/api/filters")]
public sealed class FiltersController : ControllerBase
{
private static readonly string[] Value = new[]
{
"Tags",
// "PlayerSpeed",
// "Roles",
// "KillCooldown",
// "VotingTime",
"NumImposters",
// "VisualTasks",
// "AnonymousVotes",
// "ConfirmEjects",
// "DiscussionTime",
// "EmergencyCooldown",
// "NumEmergencyMeetings",
// "NumCommonTasks",
// "NumShortTasks",
// "NumLongTasks",
// "KillDistance",
};
[HttpGet]
public IActionResult GetFilters([FromHeader] AuthenticationHeaderValue authorization)
{
if (authorization.Scheme != "Bearer" || authorization.Parameter == null)
{
return BadRequest();
}
TokenController.Token? token = null;
try
{
token = JsonSerializer.Deserialize<TokenController.Token>(Convert.FromBase64String(authorization.Parameter));
}
catch
{
return BadRequest();
}
return Ok(new
{
filters = Value,
});
}
}
+55 -13
View File
@@ -5,12 +5,15 @@ using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Web;
using Impostor.Api.Config;
using Impostor.Api.Games;
using Impostor.Api.Games.Managers;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameFilters;
using Impostor.Server.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Impostor.Server.Http;
@@ -25,6 +28,7 @@ public sealed class GamesController : ControllerBase
private readonly IGameManager _gameManager;
private readonly ListingManager _listingManager;
private readonly HostServer _hostServer;
private readonly ILogger<GamesController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="GamesController"/> class.
@@ -32,12 +36,14 @@ public sealed class GamesController : ControllerBase
/// <param name="gameManager">GameManager containing a list of games.</param>
/// <param name="listingManager">ListingManager responsible for filtering.</param>
/// <param name="serverConfig">Impostor configuration section containing the public ip address of this server.</param>
public GamesController(IGameManager gameManager, ListingManager listingManager, IOptions<ServerConfig> serverConfig)
/// <param name="logger">DI injected logger.</param>
public GamesController(IGameManager gameManager, ListingManager listingManager, IOptions<ServerConfig> serverConfig, ILogger<GamesController> logger)
{
_gameManager = gameManager;
_listingManager = listingManager;
var config = serverConfig.Value;
_hostServer = HostServer.From(IPAddress.Parse(config.ResolvePublicIp()), config.PublicPort);
_logger = logger;
}
/// <summary>
@@ -116,20 +122,51 @@ public sealed class GamesController : ControllerBase
}
[HttpGet("filtered")]
public IActionResult ShowFilteredLobbies()
public IActionResult ShowFilteredLobbies([FromQuery] string filter)
{
// TODO: implement this stub
var response = new
if (string.IsNullOrEmpty(filter))
{
games = Array.Empty<GameListing>(),
metadata = new
{
allGamesCount = _gameManager.Games.Count(),
matchingGamesCount = 0,
},
};
return BadRequest(new MatchmakerResponse(new MatchmakerError(DisconnectReason.ServerError, "filter query parameter not provided")));
}
return Ok(response);
try
{
var decodedFilter = HttpUtility.UrlDecode(filter);
var filtersList = JsonSerializer.Deserialize<GameFiltersList>(decodedFilter);
// filterSets wont be null. It must at least have ChatFilter and LangFilter
// Vanilla game only builds one filterSet and InnerSloth officials only handles first one (though you can send multiple filter sets. sloths only handle the first)
if (filtersList == null || filtersList.FilterSets.Count != 1
|| filtersList.FilterSets[0].Filters.Count < 2
|| !filtersList.FilterSets[0].Filters.Any(x => x.OptionType == "languages")
|| !filtersList.FilterSets[0].Filters.Any(x => x.OptionType == "chat"))
{
return BadRequest(new MatchmakerResponse(new MatchmakerError(DisconnectReason.ServerError, "Invalid filterSets")));
}
var filteredGames = _listingManager.FindListingsV2(HttpContext, filtersList);
var gameListings = filteredGames.Select(GameListing.From).ToList();
var response = new
{
games = gameListings,
metadata = new
{
allGamesCount = _gameManager.Games.Count(),
matchingGamesCount = gameListings.Count,
},
};
return Ok(response);
}
catch (JsonException ex)
{
return BadRequest(new MatchmakerResponse(new MatchmakerError(DisconnectReason.ServerError, "Unable to deserialize filter json" + ex)));
}
catch (Exception ex)
{
return BadRequest(new MatchmakerResponse(new MatchmakerError(DisconnectReason.ServerError, "Unknown exception caught in filter" + ex)));
}
}
private static uint ConvertAddressToNumber(IPAddress address)
@@ -177,13 +214,18 @@ public sealed class GamesController : ControllerBase
private class MatchmakerError
{
[SetsRequiredMembers]
public MatchmakerError(DisconnectReason reason)
public MatchmakerError(DisconnectReason reason, string message = "")
{
Reason = reason;
Message = message;
}
[JsonPropertyName("Reason")]
public required DisconnectReason Reason { get; init; }
[JsonPropertyName("Message")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public required string Message { get; init; } = string.Empty;
}
private class FindGameByCodeResponse
+134
View File
@@ -5,6 +5,7 @@ using Impostor.Api.Games;
using Impostor.Api.Games.Managers;
using Impostor.Api.Http;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameFilters;
using Impostor.Api.Net.Manager;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
@@ -59,6 +60,11 @@ public sealed class ListingManager
continue;
}
if (game.ModGuid != null)
{
continue;
}
if (!_compatibilityConfig.AllowVersionMixing &&
game.Host != null &&
_compatibilityManager.CanJoinGame(game.Host.Client.GameVersion, gameVersion) != GameJoinError.None)
@@ -103,6 +109,134 @@ public sealed class ListingManager
}
}
public IEnumerable<IGame> FindListingsV2(HttpContext ctx, GameFiltersList filtersList, int maxListings = 10)
{
if (filtersList == null || filtersList.FilterSets.Count == 0)
{
return Enumerable.Empty<IGame>();
}
var result = new List<IGame>();
var resultCount = 0;
// Apply custom filters from API
var filters = _listingFilters.Select(f => f.GetFilter(ctx)).ToArray();
foreach (var game in _gameManager.Games)
{
if (!game.IsPublic || game.GameState != GameStates.NotStarted || game.PlayerCount >= game.Options.MaxPlayers)
{
continue;
}
if (!filters.All(filter => filter(game)))
{
continue;
}
var matchesAnyFilterSet = false;
foreach (var filterSet in filtersList.FilterSets)
{
// Not sure how to handle normal fools and seek fools
if (game.Options.GameMode != filterSet.GameMode)
{
continue;
}
if (game.ModGuid != null && !filterSet.Filters.Any(f => f.OptionType == "mod"))
{
continue;
}
var matchesAllFilters = true;
// Hard coded to only check map, languages, impostornum, chatmode, tags
foreach (var filter in filterSet.Filters)
{
switch (filter.OptionType)
{
case "map":
if (filter.SubFilter is MapGameFilter mapFilter && ((1 << (int)game.Options.Map) & mapFilter.AcceptedValues) == 0)
{
matchesAllFilters = false;
}
break;
case "languages":
if (filter.SubFilter is LanguageFilter langFilter && game.Options.Keywords != (GameKeywords)langFilter.AcceptedValues)
{
matchesAllFilters = false;
}
break;
case "int":
if (filter.SubFilter is IntGameFilter intFilter)
{
switch (intFilter.OptionEnum)
{
case Int32OptionNames.NumImpostors:
if (!intFilter.AcceptedValues.Contains(game.Options.NumImpostors))
{
matchesAllFilters = false;
}
break;
case Int32OptionNames.Tag:
if (!intFilter.AcceptedValues.Contains((int)game.Options.Tag))
{
matchesAllFilters = false;
}
break;
}
}
break;
case "chat":
if (filter.SubFilter is ChatModeGameFilter chatFilter && game.Host != null)
{
if (((byte)game.Host.Client.ChatMode & chatFilter.AcceptedValues) == 0)
{
matchesAllFilters = false;
}
}
break;
case "mod":
if (filter.SubFilter is ModFilter modFilter && game.ModGuid != modFilter.AcceptedValues)
{
matchesAllFilters = false;
}
break;
}
if (!matchesAllFilters)
{
break;
}
}
if (matchesAllFilters)
{
matchesAnyFilterSet = true;
break;
}
}
if (matchesAnyFilterSet)
{
result.Add(game);
if (++resultCount >= maxListings)
{
break;
}
}
}
return result;
}
private static bool IsGameDesired(IGame game, int map, int impostorCount, GameKeywords language)
{
if ((map & (1 << (int)game.Options.Map)) == 0)
+1 -1
View File
@@ -20,7 +20,7 @@
<ItemGroup>
<ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
<PackageReference Include="Impostor.Hazel" Version="1.0.0" />
<PackageReference Include="Impostor.Hazel" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
+122 -12
View File
@@ -5,6 +5,7 @@ using Impostor.Api;
using Impostor.Api.Config;
using Impostor.Api.Games;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameOptions;
using Impostor.Api.Net;
using Impostor.Api.Net.Custom;
using Impostor.Api.Net.Messages;
@@ -42,6 +43,11 @@ namespace Impostor.Server.Net
return false;
}
if (Player != null && Player.Game.ModGuid != null)
{
return false;
}
if (Player != null && Player.IsHost)
{
var isHostCheatingAllowed = _antiCheatConfig.AllowCheatingHosts switch {
@@ -74,6 +80,15 @@ namespace Impostor.Server.Net
CheatCategory.Ownership => _antiCheatConfig.EnableOwnershipChecks,
CheatCategory.Role => _antiCheatConfig.EnableRoleChecks,
CheatCategory.Target => _antiCheatConfig.EnableTargetChecks,
CheatCategory.Identifiers => _antiCheatConfig.EnableIdentifierChecks,
CheatCategory.HostOnlyExtension => _antiCheatConfig.AllowHostOnlyExtensions switch {
CheatingHostMode.Always => false,
CheatingHostMode.IfRequested => !GameVersion.HasDisableServerAuthorityFlag,
CheatingHostMode.Never => true,
_ => true,
},
CheatCategory.PacketSize => _antiCheatConfig.EnablePacketSizeChecks,
CheatCategory.ItemLimits => _antiCheatConfig.EnableItemLimitChecks,
CheatCategory.Other => true,
_ => LogUnknownCategory(category),
};
@@ -117,12 +132,25 @@ namespace Impostor.Server.Net
switch (flag)
{
case MessageFlags.HostGame:
case MessageFlags.HostModdedGame:
{
// Read game settings.
Message00HostGameC2S.Deserialize(reader, out var gameOptions, out _, out var gameFilterOptions);
IGameOptions gameOptions;
GameFilterOptions gameFilterOptions;
Guid? modGuid = null;
if (flag == MessageFlags.HostModdedGame)
{
Message25HostModdedGameC2S.Deserialize(reader, out gameOptions, out _, out gameFilterOptions, out var parsedModGuid);
modGuid = parsedModGuid;
}
else
{
// Read game settings.
Message00HostGameC2S.Deserialize(reader, out gameOptions, out _, out gameFilterOptions);
}
// Create game.
var game = await _gameManager.CreateAsync(this, gameOptions, gameFilterOptions);
var game = await _gameManager.CreateAsync(this, gameOptions, gameFilterOptions, modGuid);
if (game == null)
{
@@ -130,6 +158,11 @@ namespace Impostor.Server.Net
return;
}
if (modGuid != null)
{
_logger.LogInformation("Client {Name} ({Id}) hosted a modded game with mod GUID {ModGuid}.", Name, Id, modGuid);
}
// Code in the packet below will be used in JoinGame.
using (var writer = MessageWriter.Get(MessageType.Reliable))
{
@@ -194,7 +227,7 @@ namespace Impostor.Server.Net
case MessageFlags.StartGame:
{
if (!IsPacketAllowed(reader, true))
if (!IsPacketAllowed(reader, true, flag))
{
return;
}
@@ -209,7 +242,7 @@ namespace Impostor.Server.Net
case MessageFlags.RemovePlayer:
{
if (!IsPacketAllowed(reader, true))
if (!IsPacketAllowed(reader, true, flag))
{
return;
}
@@ -226,7 +259,7 @@ namespace Impostor.Server.Net
case MessageFlags.GameData:
case MessageFlags.GameDataTo:
{
if (!IsPacketAllowed(reader, false))
if (!IsPacketAllowed(reader, false, flag))
{
return;
}
@@ -259,9 +292,80 @@ namespace Impostor.Server.Net
break;
}
case MessageFlags.PackedGameDataTo:
{
if (Player == null)
{
return;
}
var game = Player.Game;
// Innersloth Special: this message uses PackedInt32 instead of a normal int32
var code = reader.ReadPackedInt32();
if (code != game.Code.Value)
{
_logger.LogWarning("gcm2 {0} {1}", code, game.Code.Value);
return;
}
// We're limiting this to hosts right now. If you have a use case for this for
// players to use this feature, we're open to changing this.
if (game.HostId != Id)
{
await ReportCheatAsync(
new CheatContext(MessageFlags.FlagToString(flag)),
CheatCategory.MustBeHost,
"Client sent a PackedGameDataTo message");
return;
}
if (await ReportCheatAsync(
new CheatContext(MessageFlags.FlagToString(flag)),
CheatCategory.HostOnlyExtension,
"Client sent a PackedGameDataTo message"))
{
return;
}
while (reader.Position < reader.Length)
{
using var packed = reader.ReadMessage();
if (packed.Tag != MessageFlags.GameDataTo)
{
_logger.LogWarning("PackedGameDataTo contained non-GameDataTo flag {0}.", packed.Tag);
return;
}
if (packed.ReadInt32() != game.Code.Value)
{
_logger.LogWarning("PackedGameDataTo contained GameDataTo for the wrong game.");
return;
}
var position = packed.Position;
var verified = await Player.Game.HandleGameDataAsync(packed, Player, true);
packed.Seek(position);
if (!verified || Player == null)
{
return;
}
using var writer = MessageWriter.Get(messageType);
var target = packed.ReadPackedInt32();
packed.CopyTo(writer);
await Player.Game.SendToAsync(writer, target);
}
break;
}
case MessageFlags.EndGame:
{
if (!IsPacketAllowed(reader, true))
if (!IsPacketAllowed(reader, true, flag))
{
return;
}
@@ -276,7 +380,7 @@ namespace Impostor.Server.Net
case MessageFlags.AlterGame:
{
if (!IsPacketAllowed(reader, true))
if (!IsPacketAllowed(reader, true, flag))
{
return;
}
@@ -297,7 +401,7 @@ namespace Impostor.Server.Net
case MessageFlags.KickPlayer:
{
if (!IsPacketAllowed(reader, true))
if (!IsPacketAllowed(reader, true, flag))
{
return;
}
@@ -344,6 +448,7 @@ namespace Impostor.Server.Net
#if DEBUG
if (flag != MessageFlags.GameData &&
flag != MessageFlags.GameDataTo &&
flag != MessageFlags.PackedGameDataTo &&
flag != MessageFlags.EndGame &&
reader.Position < reader.Length)
{
@@ -377,7 +482,7 @@ namespace Impostor.Server.Net
await _gameManager.OnClientDisconnectAsync(this);
}
private bool IsPacketAllowed(IMessageReader message, bool hostOnly)
private bool IsPacketAllowed(IMessageReader message, bool hostOnly, byte flag)
{
if (Player == null)
{
@@ -387,7 +492,8 @@ namespace Impostor.Server.Net
var game = Player.Game;
// GameCode must match code of the current game assigned to the player.
if (message.ReadInt32() != game.Code)
var code = message.ReadInt32();
if (code != game.Code.Value)
{
return false;
}
@@ -400,7 +506,11 @@ namespace Impostor.Server.Net
return true;
}
_logger.LogWarning("[{0}] Client sent packet only allowed by the host ({1}).", Id, game.HostId);
_logger.LogWarning(
"[{0}] Client sent packet {1} only allowed by the host ({2}).",
Id,
MessageFlags.FlagToString(flag),
game.HostId);
return false;
}
+4
View File
@@ -40,6 +40,10 @@ namespace Impostor.Server.Net
public ClientPlayer? Player { get; set; }
public string? FriendCode { get; set; }
public string? Puid { get; set; }
public ColorType? PreviousColor { get; set; } = null;
IClientPlayer? IClient.Player => Player;
@@ -1,18 +1,23 @@
using System.Net;
using System.Threading.Tasks;
using Impostor.Api;
using Impostor.Api.Config;
using Impostor.Api.Net;
using Impostor.Hazel;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Impostor.Server.Net.Hazel
{
internal class HazelConnection : IHazelConnection
{
private readonly ILogger<HazelConnection> _logger;
private readonly AntiCheatConfig _antiCheatConfig;
public HazelConnection(Connection innerConnection, ILogger<HazelConnection> logger)
public HazelConnection(Connection innerConnection, ILogger<HazelConnection> logger, IOptions<AntiCheatConfig> antiCheatOptions)
{
_logger = logger;
_antiCheatConfig = antiCheatOptions.Value;
InnerConnection = innerConnection;
innerConnection.DataReceived = ConnectionOnDataReceived;
innerConnection.Disconnected = ConnectionOnDisconnected;
@@ -58,6 +63,19 @@ namespace Impostor.Server.Net.Hazel
return;
}
// Check raw message size against the configured limit.
// Innersloth requires full packet ≤ 1200 bytes (1168 bytes payload after 32 bytes IP+UDP headers).
if (e.Message.Length > _antiCheatConfig.PacketSizeLimit)
{
if (await Client.ReportCheatAsync(
new CheatContext("RootMessage"),
CheatCategory.PacketSize,
$"Received a message that is too large, length: {e.Message.Length}"))
{
return;
}
}
while (true)
{
if (e.Message.Position >= e.Message.Length)
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Impostor.Api.Net.Inner.Objects;
@@ -9,5 +9,7 @@ namespace Impostor.Server.Net.Inner.Objects
IReadOnlyCollection<IInnerMeetingHud.IPlayerVoteArea> IInnerMeetingHud.PlayerStates => Array.AsReadOnly(_playerStates);
IInnerPlayerInfo? IInnerMeetingHud.Reporter => Reporter;
IReadOnlyCollection<IInnerMeetingHud.IJudgeOverrule> IInnerMeetingHud.JudgeOverrules => JudgeOverrules;
}
}
@@ -0,0 +1,29 @@
using Impostor.Api.Net.Inner.Objects;
namespace Impostor.Server.Net.Inner.Objects
{
internal partial class InnerMeetingHud
{
public sealed class JudgeOverrule : IInnerMeetingHud.IJudgeOverrule
{
public JudgeOverrule(byte judgePlayerId, byte overruledPlayerId, ushort overruleNonce)
{
JudgePlayerId = judgePlayerId;
OverruledPlayerId = overruledPlayerId;
OverruleNonce = overruleNonce;
}
public byte JudgePlayerId { get; set; }
public byte OverruledPlayerId { get; set; }
public ushort OverruleNonce { get; set; }
public static JudgeOverrule Deserialize(IMessageReader reader)
{
var inner = reader.ReadMessage();
return new JudgeOverrule(inner.Tag, inner.ReadByte(), inner.ReadUInt16());
}
}
}
}
@@ -12,6 +12,7 @@ using Impostor.Api.Innersloth.GameOptions;
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.Events.Meeting;
using Impostor.Server.Events.Player;
@@ -22,10 +23,12 @@ namespace Impostor.Server.Net.Inner.Objects
{
internal partial class InnerMeetingHud : InnerNetObject
{
private static readonly GameVersion JudgeMinVersion = new GameVersion(2026, 7, 15); // 18.0
private readonly ILogger<InnerMeetingHud> _logger;
private readonly IEventManager _eventManager;
private readonly CancellationTokenSource _timerToken;
private readonly List<JudgeOverrule> _judgeOverrules = new();
[AllowNull]
private PlayerVoteArea[] _playerStates;
@@ -65,6 +68,8 @@ namespace Impostor.Server.Net.Inner.Objects
public InnerPlayerInfo? Reporter { get; private set; }
public IReadOnlyCollection<IInnerMeetingHud.IJudgeOverrule> JudgeOverrules => _judgeOverrules;
public override ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
{
throw new NotImplementedException();
@@ -108,6 +113,19 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
}
if (sender.Client.GameVersion >= JudgeMinVersion)
{
var overruleQueueListLength = reader.ReadPackedInt32();
if (overruleQueueListLength > 0)
{
_judgeOverrules.Clear();
for (var i = 0; i < overruleQueueListLength; i++)
{
_judgeOverrules.Add(JudgeOverrule.Deserialize(reader));
}
}
}
}
public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
@@ -122,6 +140,7 @@ namespace Impostor.Server.Net.Inner.Objects
}
Rpc22Close.Deserialize(reader);
_judgeOverrules.Clear();
break;
}
@@ -132,10 +151,23 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
Rpc23VotingComplete.Deserialize(reader, out var states, out var playerId, out var tie);
foreach (var messageReader in states)
if (sender.Client.GameVersion >= JudgeMinVersion)
{
messageReader.Dispose();
Rpc23VotingComplete.Deserialize(reader, out var states, out var playerId, out var tie, out var wasOverruled, out var overrideId);
foreach (var messageReader in states)
{
messageReader.Dispose();
}
}
else
{
Rpc23VotingComplete.Deserialize(reader, out var states, out var playerId, out var tie);
foreach (var messageReader in states)
{
messageReader.Dispose();
}
}
// This would be a nice place to implement an anti cheat.
@@ -161,6 +193,12 @@ namespace Impostor.Server.Net.Inner.Objects
break;
}
case RpcCalls.QueueOverruleVotes:
{
Rpc66QueueOverruleVotes.Deserialize(reader, out var judgePlayerId, out var targetPlayerId, out var overruleNonce);
return await HandleQueueOverruleVotesAsync(sender, target, judgePlayerId, targetPlayerId, overruleNonce);
}
default:
return await base.HandleRpcAsync(sender, target, call, reader);
}
@@ -221,6 +259,87 @@ namespace Impostor.Server.Net.Inner.Objects
return true;
}
private async ValueTask<bool> HandleQueueOverruleVotesAsync(ClientPlayer sender, ClientPlayer? target, byte judgePlayerId, byte targetPlayerId, ushort overruleNonce)
{
if (!await ValidateRole(RpcCalls.QueueOverruleVotes, sender, sender.Character?.PlayerInfo, RoleTypes.Judge) ||
!await ValidateTarget(RpcCalls.QueueOverruleVotes, sender, target) ||
!await ValidateHost(RpcCalls.QueueOverruleVotes, target!))
{
return false;
}
// A Judge can only queue an overrule for themself.
if (judgePlayerId != sender.Character!.PlayerId)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.QueueOverruleVotes, CheatCategory.Ownership, "Client sent a Judge overrule for another player"))
{
return false;
}
}
// The client only generates nonces in the 1..65535 range.
if (overruleNonce == 0)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.QueueOverruleVotes, CheatCategory.GameFlow, "Client sent a Judge overrule with an uninitialized nonce"))
{
return false;
}
}
// A Judge can only overrule once per meeting.
if (_judgeOverrules.Any(overrule => overrule.JudgePlayerId == judgePlayerId))
{
if (await sender.Client.ReportCheatAsync(RpcCalls.QueueOverruleVotes, CheatCategory.GameFlow, "Client sent more than one Judge overrule in the same meeting"))
{
return false;
}
}
// The overruled player must exist.
if (Game.GameNet.GameData!.GetPlayerById(targetPlayerId) == null)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.QueueOverruleVotes, CheatCategory.InvalidObject, "Client sent a Judge overrule for an unknown player"))
{
return false;
}
}
AddOrUpdateJudgeOverrule(judgePlayerId, targetPlayerId, overruleNonce);
return true;
}
private void AddOrUpdateJudgeOverrule(byte judgePlayerId, byte targetPlayerId, ushort overruleNonce)
{
var existing = _judgeOverrules.Find(overrule => overrule.JudgePlayerId == judgePlayerId);
if (existing != null)
{
existing.OverruledPlayerId = targetPlayerId;
existing.OverruleNonce = overruleNonce;
}
else
{
_judgeOverrules.Add(new JudgeOverrule(judgePlayerId, targetPlayerId, overruleNonce));
}
}
private JudgeOverrule? GetWinningOverrule()
{
var gameData = Game.GameNet.GameData!;
foreach (var overrule in _judgeOverrules)
{
var judge = gameData.GetPlayerById(overrule.JudgePlayerId);
var target = gameData.GetPlayerById(overrule.OverruledPlayerId);
if (judge != null && target != null && !judge.Disconnected && !target.Disconnected)
{
return overrule;
}
}
return null;
}
private async ValueTask CheckForEndVotingAsync()
{
if (_playerStates.All(ps => ps.IsDead || ps.DidVote))
@@ -287,13 +406,35 @@ namespace Impostor.Server.Net.Inner.Objects
var max = MaxPair(self, out var tie);
var exiled = tie ? null : Game.GameNet.GameData!.GetPlayerById(max.Key)?.Controller;
// exile or others?
var wasOverruled = false;
ushort overrideId = 0;
var winningOverrule = GetWinningOverrule();
if (winningOverrule != null)
{
wasOverruled = true;
overrideId = winningOverrule.OverruleNonce;
var overruled = Game.GameNet.GameData!.GetPlayerById(winningOverrule.OverruledPlayerId);
if (overruled != null && overruled.IsImpostor)
{
exiled = overruled.Controller;
}
else
{
var judge = Game.GameNet.GameData!.GetPlayerById(winningOverrule.JudgePlayerId);
exiled = judge?.Controller;
}
}
if (exiled != null && exiled.PlayerInfo != null)
{
exiled.PlayerInfo.LastDeathReason = DeathReason.Exile;
await _eventManager.CallAsync(new PlayerExileEvent(Game, Game.GetClientPlayer(exiled!.OwnerId)!, exiled));
}
await _eventManager.CallAsync(new MeetingEndedEvent(Game, this, exiled, tie));
await _eventManager.CallAsync(new MeetingEndedEvent(Game, this, exiled, tie, wasOverruled, overrideId));
}
}
}
@@ -392,7 +392,7 @@ namespace Impostor.Server.Net.Inner.Objects
}
else
{
if (role is RoleTypes.ImpostorGhost or RoleTypes.CrewmateGhost or RoleTypes.GuardianAngel)
if (role is RoleTypes.ImpostorGhost or RoleTypes.CrewmateGhost or RoleTypes.GuardianAngel or RoleTypes.SpiritGuide)
{
PlayerInfo.RoleWhenAlive = PlayerInfo.RoleType;
PlayerInfo.IsDead = true;
@@ -1183,9 +1183,35 @@ namespace Impostor.Server.Net.Inner.Objects
private async ValueTask<bool> HandleSendChat(ClientPlayer sender, string message)
{
var @event = new PlayerChatEvent(Game, sender, this, message);
// See https://github.com/Innersloth-LLC/AmongUsModdingInformation?tab=readme-ov-file#chat-commands
// Details not mentioned:
// - If the host sends a message that starts with /cmd, it is sent to all players
// - If the message starts with " /cmd" (note the space) it is sent to all players
if (Game.IsHostAuthoritive && !sender.IsHost && message.StartsWith("/cmd"))
{
@event.SendToAllPlayers = false;
}
await _eventManager.CallAsync(@event);
return !@event.IsCancelled;
if (@event.IsCancelled)
{
return false;
}
else if (@event.SendToAllPlayers == false)
{
if (Game.Host != null)
{
await SendChatToPlayerAsync(message, Game.Host.Character);
}
return false;
}
else
{
return true;
}
}
private async ValueTask HandleStartMeeting(byte targetId)
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Impostor.Api;
using Impostor.Api.Events.Managers;
using Impostor.Api.Games;
using Impostor.Api.Innersloth;
@@ -51,14 +52,18 @@ namespace Impostor.Server.Net.Inner.Objects
public bool Disconnected { get; internal set; }
public bool IsImpostor => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.ImpostorGhost or RoleTypes.Phantom;
public bool IsImpostor => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.ImpostorGhost or RoleTypes.Phantom or RoleTypes.Viper;
public bool CanVent => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Phantom or RoleTypes.Engineer;
public bool CanVent => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Phantom or RoleTypes.Viper or RoleTypes.Engineer;
public bool IsDead { get; internal set; }
public DeathReason LastDeathReason { get; internal set; }
public string? FriendCode { get; internal set; }
public string? Puid { get; internal set; }
public List<TaskInfo> Tasks { get; internal set; } = new List<TaskInfo>(0);
public DateTimeOffset LastMurder { get; set; }
@@ -128,12 +133,13 @@ namespace Impostor.Server.Net.Inner.Objects
Tasks[i].Serialize(writer);
}
writer.Write(string.Empty); // FriendCode
writer.Write(string.Empty); // PUID
// Resolved by the matchmaker and pushed onto the player by a plugin
writer.Write(FriendCode ?? string.Empty);
writer.Write(Puid ?? string.Empty);
return new ValueTask<bool>(true);
}
public override ValueTask DeserializeAsync(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
public override async ValueTask DeserializeAsync(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
{
PlayerId = reader.ReadByte();
ClientId = reader.ReadPackedInt32();
@@ -175,11 +181,29 @@ 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
var friendCode = reader.ReadString();
var puid = reader.ReadString();
return ValueTask.CompletedTask;
if (Game.GetClientPlayer(ClientId) is { } owner)
{
FriendCode ??= owner.Client.FriendCode;
Puid ??= owner.Client.Puid;
}
await ValidateIdentifiersAsync(sender, friendCode, puid);
}
private async ValueTask ValidateIdentifiersAsync(IClientPlayer sender, string? friendCode, string? puid)
{
var mismatch = (!string.IsNullOrEmpty(friendCode) && FriendCode != null && FriendCode != friendCode)
|| (!string.IsNullOrEmpty(puid) && Puid != null && Puid != puid);
if (!mismatch)
{
return;
}
await sender.Client.ReportCheatAsync(CheatContext.Deserialize, CheatCategory.Identifiers, $"Claimed identifiers for player {ClientId} do not match the values known for that player");
}
public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
@@ -79,7 +79,7 @@ namespace Impostor.Server.Net.Inner.Objects.ShipStatus
case RpcCalls.CloseDoorsOfType:
{
if (!await ValidateCmd(call, sender, target) ||
!await ValidateImpostor(call, sender, sender.Character!.PlayerInfo))
!await ValidateImpostor(call, sender, sender.Character?.PlayerInfo))
{
return false;
}
@@ -100,6 +100,30 @@ namespace Impostor.Server.Net.Inner.Objects.ShipStatus
break;
}
case RpcCalls.SpiritGuideMessage:
{
if (!await ValidateTarget(call, sender, target) ||
!await ValidateRole(call, sender, sender.Character?.PlayerInfo, RoleTypes.SpiritGuide))
{
return false;
}
Rpc67SpiritGuideMessage.Deserialize(reader, out var images);
if (images.Length == 0 || images.Length > 3)
{
if (await sender.Client.ReportCheatAsync(
RpcCalls.SpiritGuideMessage,
CheatCategory.ItemLimits,
$"Client sent {images.Length} images (allowed: 1, 2 or 3)"))
{
return false;
}
}
break;
}
default:
return await base.HandleRpcAsync(sender, target, call, reader);
}
@@ -101,8 +101,8 @@ 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
// Warn when players connect using the +25 flag that disables server authority.
// This changes game behaviour, so we'd like to know if it's in use.
if (clientVersion.HasDisableServerAuthorityFlag)
{
if (!_compatibilityConfig.AllowHostAuthority)
@@ -112,7 +112,7 @@ namespace Impostor.Server.Net.Manager
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);
_logger.LogInformation("Player {Name} connected with server authority disabled, please mention that this mode is in use when asking for support.", name);
}
if (name.Length > 10)
@@ -28,11 +28,32 @@ internal class CompatibilityManager : ICompatibilityManager
},
new[]
{
new GameVersion(2025, 4, 15), // 16.0.5 (2025-05-20)
new GameVersion(2025, 4, 15), // 16.0.5 (2025-05-20) & 16.1.0 (2025-06-10)
},
new[]
{
new GameVersion(2025, 7, 15), // 17.0.0 (2025-09-09)
new GameVersion(2025, 9, 12), // 17.0.1 (2025-10-14)
new GameVersion(2025, 10, 9), // 17.1 (2025-11-18)
new GameVersion(2025, 11, 6), // 17.1.1 (2025-12-03, mobile only)
new GameVersion(2025, 12, 8), // 17.1.2 (2025-12-11, mobile only)
new GameVersion(2025, 11, 5), // 17.2 (2026-02-17, build 6630, not a typo, Innersloth went backwards)
new GameVersion(2026, 1, 22), // 17.2 (2026-02-19, build 6686, hotfix)
new GameVersion(2026, 2, 2), // 17.2.2 (2026-03-17, build 6768)
new GameVersion(2026, 1, 12), // 17.3 (2026-03-31, build 6803)
new GameVersion(2026, 3, 17), // 17.3.1 (2026-04-08, build 6841, mobile only)
new GameVersion(2026, 3, 18), // 17.4 (2026-06-05, build 7044, pc only)
new GameVersion(2026, 4, 23), // 17.4 (2026-06-05, build 7045, mobile only)
},
new[]
{
new GameVersion(2026, 7, 15), // 18.0 (2026-08-18, build 7238, pc only)
new GameVersion(2026, 7, 16), // 18.0 (2026-08-18, build 7238, mobile only)
},
new[]
{
new GameVersion(2026, 7, 20), // 19.0 (2026-09-29, build 7489, pc only)
new GameVersion(2026, 7, 21), // 19.0 (2026-09-29, build 7492, mobile only)
},
};
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -85,7 +85,7 @@ namespace Impostor.Server.Net.Manager
await _eventManager.CallAsync(new GameDestroyedEvent(game));
}
public async ValueTask<IGame?> CreateAsync(IClient? owner, IGameOptions options, GameFilterOptions filterOptions)
public async ValueTask<IGame?> CreateAsync(IClient? owner, IGameOptions options, GameFilterOptions filterOptions, Guid? modGuid = null)
{
if (owner != null && !_gamesCreatedBy.TryAdd(owner, null))
{
@@ -101,11 +101,11 @@ namespace Impostor.Server.Net.Manager
return null;
}
var (success, game) = await TryCreateAsync(options, filterOptions, owner, @event.GameCode);
var (success, game) = await TryCreateAsync(options, filterOptions, owner, @event.GameCode, modGuid);
for (var i = 0; i < 10 && !success; i++)
{
(success, game) = await TryCreateAsync(options, filterOptions, owner);
(success, game) = await TryCreateAsync(options, filterOptions, owner, modGuid: modGuid);
}
if (owner != null)
@@ -126,10 +126,12 @@ namespace Impostor.Server.Net.Manager
return CreateAsync(null, options, filterOptions);
}
private async ValueTask<(bool Success, Game? Game)> TryCreateAsync(IGameOptions options, GameFilterOptions filterOptions, IClient? owner, GameCode? desiredGameCode = null)
private async ValueTask<(bool Success, Game? Game)> TryCreateAsync(IGameOptions options, GameFilterOptions filterOptions, IClient? owner, GameCode? desiredGameCode = null, Guid? modGuid = null)
{
var gameCode = desiredGameCode ?? _gameCodeFactory.Create();
var game = ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options, filterOptions);
var game = modGuid.HasValue
? ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options, filterOptions, modGuid.Value)
: ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options, filterOptions);
if (!_games.TryAdd(gameCode, game))
{
+7 -2
View File
@@ -2,6 +2,7 @@
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Impostor.Api.Config;
using Impostor.Api.Events.Managers;
using Impostor.Api.Net.Messages.C2S;
using Impostor.Hazel;
@@ -11,6 +12,7 @@ using Impostor.Server.Net.Hazel;
using Impostor.Server.Net.Manager;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
namespace Impostor.Server.Net
{
@@ -20,18 +22,21 @@ namespace Impostor.Server.Net
private readonly ClientManager _clientManager;
private readonly ObjectPool<MessageReader> _readerPool;
private readonly ILogger<HazelConnection> _connectionLogger;
private readonly IOptions<AntiCheatConfig> _antiCheatOptions;
private UdpConnectionListener? _connection;
public Matchmaker(
IEventManager eventManager,
ClientManager clientManager,
ObjectPool<MessageReader> readerPool,
ILogger<HazelConnection> connectionLogger)
ILogger<HazelConnection> connectionLogger,
IOptions<AntiCheatConfig> antiCheatOptions)
{
_eventManager = eventManager;
_clientManager = clientManager;
_readerPool = readerPool;
_connectionLogger = connectionLogger;
_antiCheatOptions = antiCheatOptions;
}
public async ValueTask StartAsync(IPEndPoint ipEndPoint)
@@ -64,7 +69,7 @@ namespace Impostor.Server.Net
// Handshake.
HandshakeC2S.Deserialize(e.HandshakeData, out var clientVersion, out var name, out var language, out var chatMode, out var platformSpecificData);
var connection = new HazelConnection(e.Connection, _connectionLogger);
var connection = new HazelConnection(e.Connection, _connectionLogger, _antiCheatOptions);
await _eventManager.CallAsync(new ClientConnectionEvent(connection, e.HandshakeData));
+11 -23
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -60,16 +61,14 @@ namespace Impostor.Server.Net.State
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 readonly ConcurrentDictionary<uint, InnerNetObject> _allObjects = new ConcurrentDictionary<uint, InnerNetObject>();
private uint _nextNetId = MinServerNetId;
public T? FindObjectByNetId<T>(uint netId)
where T : IInnerNetObject
{
if (_allObjectsFast.TryGetValue(netId, out var obj))
if (_allObjects.TryGetValue(netId, out var obj))
{
return (T)(IInnerNetObject)obj;
}
@@ -104,7 +103,7 @@ namespace Impostor.Server.Net.State
case GameDataTag.DataFlag:
{
var netId = reader.ReadPackedUInt32();
if (_allObjectsFast.TryGetValue(netId, out var obj))
if (_allObjects.TryGetValue(netId, out var obj))
{
await obj.DeserializeAsync(sender, target, reader, false);
}
@@ -119,7 +118,7 @@ namespace Impostor.Server.Net.State
case GameDataTag.RpcFlag:
{
var netId = reader.ReadPackedUInt32();
if (_allObjectsFast.TryGetValue(netId, out var obj))
if (_allObjects.TryGetValue(netId, out var obj))
{
if (!await obj.HandleRpcAsync(sender, target, (RpcCalls)reader.ReadByte(), reader))
{
@@ -214,7 +213,7 @@ namespace Impostor.Server.Net.State
case GameDataTag.DespawnFlag:
{
var netId = reader.ReadPackedUInt32();
if (_allObjectsFast.TryGetValue(netId, out var obj))
if (_allObjects.TryGetValue(netId, out var obj))
{
if (sender.Client.Id != obj.OwnerId && !sender.IsHost)
{
@@ -468,7 +467,7 @@ namespace Impostor.Server.Net.State
private async ValueTask SyncServerObjectsAsync(ClientPlayer sender)
{
foreach (var obj in _allObjectsFast.Values)
foreach (var obj in _allObjects.Values)
{
if (obj.OwnerId == ServerOwned)
{
@@ -497,6 +496,8 @@ namespace Impostor.Server.Net.State
playerInfo.NetId = _nextNetId++;
playerInfo.OwnerId = ServerOwned;
playerInfo.ClientId = sender.Client.Id;
playerInfo.FriendCode = sender.Client.FriendCode;
playerInfo.Puid = sender.Client.Puid;
playerInfo.PlayerId = GameNet.GameData.GetNextAvailablePlayerId();
// If player played a previous game, restore their color
@@ -533,25 +534,12 @@ namespace Impostor.Server.Net.State
private bool AddNetObject(InnerNetObject obj)
{
if (_allObjectsFast.ContainsKey(obj.NetId))
{
return false;
}
_allObjects.Add(obj);
_allObjectsFast.Add(obj.NetId, obj);
return true;
return _allObjects.TryAdd(obj.NetId, obj);
}
private void RemoveNetObject(InnerNetObject obj)
{
var index = _allObjects.IndexOf(obj);
if (index > -1)
{
_allObjects.RemoveAt(index);
}
_allObjectsFast.Remove(obj.NetId);
_allObjects.TryRemove(obj.NetId, out _);
}
}
}
@@ -210,12 +210,6 @@ namespace Impostor.Server.Net.State
return GameJoinResult.FromError(GameJoinError.InvalidLimbo);
}
if (GameState == GameStates.Ended)
{
await HandleJoinGameNext(player, isNew);
return GameJoinResult.CreateSuccess(player);
}
var @event = new GamePlayerJoiningEvent(this, player);
await _eventManager.CallAsync(@event);
@@ -224,6 +218,12 @@ namespace Impostor.Server.Net.State
return @event.JoinResult.Value;
}
if (GameState == GameStates.Ended)
{
await HandleJoinGameNext(player, isNew);
return GameJoinResult.CreateSuccess(player);
}
await HandleJoinGameNew(player, isNew);
return GameJoinResult.CreateSuccess(player);
}
+5 -1
View File
@@ -47,7 +47,8 @@ namespace Impostor.Server.Net.State
IEventManager eventManager,
ICompatibilityManager compatibilityManager,
IOptions<CompatibilityConfig> compatibilityConfig,
IOptions<TimeoutConfig> timeoutConfig)
IOptions<TimeoutConfig> timeoutConfig,
Guid? modGuid = null)
{
_logger = logger;
_serviceProvider = serviceProvider;
@@ -62,6 +63,7 @@ namespace Impostor.Server.Net.State
GameNet = new GameNet();
Options = options;
FilterOptions = filterOptions;
ModGuid = modGuid;
_clientManager = clientManager;
_eventManager = eventManager;
_compatibilityManager = compatibilityManager;
@@ -96,6 +98,8 @@ namespace Impostor.Server.Net.State
public bool IsHostAuthoritive => Host != null && Host.Client.GameVersion.HasDisableServerAuthorityFlag;
public Guid? ModGuid { get; }
internal GameNet GameNet { get; }
public bool TryGetPlayer(int id, [MaybeNullWhen(false)] out ClientPlayer player)
@@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Impostor.Api.Config;
using Impostor.Api.Innersloth;
using Impostor.Api.Net.Custom;
@@ -52,7 +52,7 @@ namespace Impostor.Server.Recorder
await base.HandleMessageAsync(reader, messageType);
// Player created a game.
if (reader.Tag == MessageFlags.HostGame)
if (reader.Tag is MessageFlags.HostGame or MessageFlags.HostModdedGame)
{
_createdGame = true;
}
+4 -1
View File
@@ -21,7 +21,10 @@
"EnableOwnershipChecks": true,
"EnableRoleChecks": true,
"EnableTargetChecks": true,
"ForbidProtocolExtensions": true
"EnableIdentifierChecks": true,
"ForbidProtocolExtensions": true,
"EnablePacketSizeChecks": true,
"PacketSizeLimit": 1203
},
"Timeout": {
"SpawnTimeout": 2500,
+15 -9
View File
@@ -4,13 +4,13 @@
"net8.0": {
"Impostor.Hazel": {
"type": "Direct",
"requested": "[1.0.0, )",
"resolved": "1.0.0",
"contentHash": "FgVKuIWK+o6Q+wOPFoZZTeoSuVh4giAhrS8Zvs6O0++Bg8RxH4Lw51uSwnexLiwosXveUCIXICzgGHmRyguVSA==",
"requested": "[1.0.1, )",
"resolved": "1.0.1",
"contentHash": "DFsZcOa9ptFEj8FCfK63L/bcJ9RUVRZUhUS0qkrmdSqkgeJRdvUOTWR1gKlqbaOJcSEvexAITC0QJEPnJUcI7Q==",
"dependencies": {
"Impostor.Hazel.Abstractions": "1.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.ObjectPool": "7.0.1",
"Impostor.Hazel.Abstractions": "1.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.ObjectPool": "8.0.0",
"Serilog": "2.12.0"
}
},
@@ -56,6 +56,12 @@
"resolved": "8.0.1",
"contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA=="
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.30, )",
"resolved": "8.0.30",
"contentHash": "SfmwuZjUPBTEoBMAsXZ/hWkaEK9oL+Y/UtAx7N6RyWHfcMt9J0ExrNPuOgTyPbCXIyiTBFsxFNrPLEZNPmJcvA=="
},
"Serilog.Extensions.Hosting": {
"type": "Direct",
"requested": "[8.0.0, )",
@@ -97,8 +103,8 @@
},
"Impostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
"resolved": "1.0.1",
"contentHash": "de2bMj78xDXOWwyqQVk8o4vbZKGfAgOjnWnkKB1IGGovNtxLeBF8jy3wH5moTZB6juTnLf0ptkCDkRi8c2OcjA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
@@ -414,7 +420,7 @@
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Impostor.Hazel.Abstractions": "[1.0.1, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
+10 -10
View File
@@ -46,19 +46,19 @@
},
"Impostor.Hazel": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "FgVKuIWK+o6Q+wOPFoZZTeoSuVh4giAhrS8Zvs6O0++Bg8RxH4Lw51uSwnexLiwosXveUCIXICzgGHmRyguVSA==",
"resolved": "1.0.1",
"contentHash": "DFsZcOa9ptFEj8FCfK63L/bcJ9RUVRZUhUS0qkrmdSqkgeJRdvUOTWR1gKlqbaOJcSEvexAITC0QJEPnJUcI7Q==",
"dependencies": {
"Impostor.Hazel.Abstractions": "1.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.ObjectPool": "7.0.1",
"Impostor.Hazel.Abstractions": "1.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.ObjectPool": "8.0.0",
"Serilog": "2.12.0"
}
},
"Impostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
"resolved": "1.0.1",
"contentHash": "de2bMj78xDXOWwyqQVk8o4vbZKGfAgOjnWnkKB1IGGovNtxLeBF8jy3wH5moTZB6juTnLf0ptkCDkRi8c2OcjA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
@@ -529,7 +529,7 @@
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Impostor.Hazel.Abstractions": "[1.0.1, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
@@ -538,8 +538,8 @@
"impostor.server": {
"type": "Project",
"dependencies": {
"Impostor.Api": "[1.9.0-dev, )",
"Impostor.Hazel": "[1.0.0, )",
"Impostor.Api": "[1.10.7-dev, )",
"Impostor.Hazel": "[1.0.1, )",
"Microsoft.Extensions.FileSystemGlobbing": "[8.0.0, )",
"Microsoft.Extensions.Hosting": "[8.0.0, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",
@@ -12,7 +12,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Impostor.Hazel" Version="1.0.0" />
<PackageReference Include="Impostor.Hazel" Version="1.0.1" />
</ItemGroup>
</Project>
+1
View File
@@ -34,6 +34,7 @@ namespace Impostor.Tools.Proxy
{ 13, "Redirect" },
{ 14, "ReselectServer" },
{ 16, "GetGameListV2" },
{ 26, "PackedGameDataTo" },
};
private static IServiceProvider _serviceProvider;
+10 -10
View File
@@ -4,13 +4,13 @@
"net8.0": {
"Impostor.Hazel": {
"type": "Direct",
"requested": "[1.0.0, )",
"resolved": "1.0.0",
"contentHash": "FgVKuIWK+o6Q+wOPFoZZTeoSuVh4giAhrS8Zvs6O0++Bg8RxH4Lw51uSwnexLiwosXveUCIXICzgGHmRyguVSA==",
"requested": "[1.0.1, )",
"resolved": "1.0.1",
"contentHash": "DFsZcOa9ptFEj8FCfK63L/bcJ9RUVRZUhUS0qkrmdSqkgeJRdvUOTWR1gKlqbaOJcSEvexAITC0QJEPnJUcI7Q==",
"dependencies": {
"Impostor.Hazel.Abstractions": "1.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.ObjectPool": "7.0.1",
"Impostor.Hazel.Abstractions": "1.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.ObjectPool": "8.0.0",
"Serilog": "2.12.0"
}
},
@@ -31,8 +31,8 @@
},
"Impostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
"resolved": "1.0.1",
"contentHash": "de2bMj78xDXOWwyqQVk8o4vbZKGfAgOjnWnkKB1IGGovNtxLeBF8jy3wH5moTZB6juTnLf0ptkCDkRi8c2OcjA=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
@@ -41,8 +41,8 @@
},
"Microsoft.Extensions.ObjectPool": {
"type": "Transitive",
"resolved": "7.0.1",
"contentHash": "dDFItid/TGJljAnqbUQ0PkAl6ShIG0htwRR8J0cG/5Il6lIZPfkIZMwrZTWiF23AiQcqt6suPaUy2ih6dsCB9Q=="
"resolved": "8.0.0",
"contentHash": "4pm+XgxSukskwjzDDfSjG4KNUIOdFF2VaqZZDtTzoyQMOVSnlV6ZM8a9aVu5dg9LVZTB//utzSc8fOi0b0Mb2Q=="
},
"Serilog": {
"type": "Transitive",
@@ -13,19 +13,19 @@
},
"Impostor.Hazel": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "FgVKuIWK+o6Q+wOPFoZZTeoSuVh4giAhrS8Zvs6O0++Bg8RxH4Lw51uSwnexLiwosXveUCIXICzgGHmRyguVSA==",
"resolved": "1.0.1",
"contentHash": "DFsZcOa9ptFEj8FCfK63L/bcJ9RUVRZUhUS0qkrmdSqkgeJRdvUOTWR1gKlqbaOJcSEvexAITC0QJEPnJUcI7Q==",
"dependencies": {
"Impostor.Hazel.Abstractions": "1.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.ObjectPool": "7.0.1",
"Impostor.Hazel.Abstractions": "1.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.ObjectPool": "8.0.0",
"Serilog": "2.12.0"
}
},
"Impostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
"resolved": "1.0.1",
"contentHash": "de2bMj78xDXOWwyqQVk8o4vbZKGfAgOjnWnkKB1IGGovNtxLeBF8jy3wH5moTZB6juTnLf0ptkCDkRi8c2OcjA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
@@ -402,7 +402,7 @@
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Impostor.Hazel.Abstractions": "[1.0.1, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )"
@@ -411,8 +411,8 @@
"impostor.server": {
"type": "Project",
"dependencies": {
"Impostor.Api": "[1.9.0-dev, )",
"Impostor.Hazel": "[1.0.0, )",
"Impostor.Api": "[1.10.7-dev, )",
"Impostor.Hazel": "[1.0.1, )",
"Microsoft.Extensions.FileSystemGlobbing": "[8.0.0, )",
"Microsoft.Extensions.Hosting": "[8.0.0, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",