Compare commits

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

Keep the access modifiers intact for now.
2024-08-13 21:00:51 +02:00
miniduikboot 4dc4c90e8b Add sequence id's to playeroutfit 2024-08-13 21:00:51 +02:00
miniduikboot 9379c05676 Add canOverrideRole param to SetRole
This is currently unused by Innersloth
2024-08-13 21:00:51 +02:00
miniduikboot ef98f4d20e Implement GameOptions V8
This adds a field for RulesPresets and SpecialGameModes, as well as
RoleOptions for the new roles.
2024-08-13 21:00:51 +02:00
miniduikboot 9f6947b484 VoteBanSystem now contains itself as a component
This makes it consistent with the other spawnables.
2024-08-13 21:00:51 +02:00
miniduikboot 76ae1a3385 Update Dumpostor to v1.0.1 and run for 6.18
Add the RulesPresets and SpecialGamemodes enums
2024-08-13 21:00:51 +02:00
miniduikboot 3c8a002d6e Prepare for 2024.6.18 update
Major changes are incoming, this doesn't yet work.
2024-08-13 21:00:51 +02:00
36 changed files with 682 additions and 190 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>11</LangVersion>
<VersionPrefix>1.9.3</VersionPrefix>
<VersionPrefix>1.10.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<EnforceCodeStyleInBuild Condition="$(MSBuildProjectName)!='Hazel'">true</EnforceCodeStyleInBuild>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
@@ -79,6 +79,8 @@ public sealed class SourceGenerator : IIncrementalGenerator
enumGenerator.Generate("GameOverReason", underlyingType: CSharpEnumUnderlyingType.Byte);
enumGenerator.Generate("Platforms");
enumGenerator.Generate("RoleTypes", underlyingType: CSharpEnumUnderlyingType.UnsignedShort);
enumGenerator.Generate("RulesPresets", underlyingType: CSharpEnumUnderlyingType.Byte);
enumGenerator.Generate("SpecialGameModes", underlyingType: CSharpEnumUnderlyingType.Byte);
enumGenerator.Generate("StringNames");
enumGenerator.Generate("SystemTypes", underlyingType: CSharpEnumUnderlyingType.Byte);
enumGenerator.Generate("Language", sourceName: "SupportedLangs");
@@ -2,42 +2,102 @@ namespace Impostor.Api.Innersloth.Customization
{
public class PlayerOutfit
{
public ColorType Color { get; internal set; } = (ColorType)(-1);
// Magic value that is assigned by default when the outfit is initially created. This is not
// `null` because existing API already exposed this magic value. It's also not 0 (which is
// the value exposed when serializing) because SetColor anticheat checks exist.
private const ColorType UnknownColor = (ColorType)(-1);
public string HatId { get; internal set; } = "missing";
private string _playerName = string.Empty;
private ColorType _color = UnknownColor;
private string _hatId = "missing";
private string _petId = "missing";
private string _skinId = "missing";
private string _visorId = "missing";
private string _namePlateId = "missing";
public string PetId { get; internal set; } = "missing";
public bool IsDirty { get; internal set; }
public string SkinId { get; internal set; } = "missing";
public string VisorId { get; internal set; } = "missing";
public string NamePlateId { get; internal set; } = "missing";
public string PlayerName { get; internal set; } = "missing";
public bool IsIncomplete
public string PlayerName
{
get
{
if (!string.IsNullOrEmpty(PlayerName) && Color != (ColorType)(-1) && HatId != "missing" && PetId != "missing" && SkinId != "missing" && VisorId != "missing")
{
return NamePlateId == "missing";
}
return true;
}
get => _playerName;
internal set => SetField(ref _playerName, value);
}
public ColorType Color
{
get => _color;
internal set => SetField(ref _color, value);
}
public string HatId
{
get => _hatId;
internal set => SetField(ref _hatId, value);
}
public string PetId
{
get => _petId;
internal set => SetField(ref _petId, value);
}
public string SkinId
{
get => _skinId;
internal set => SetField(ref _skinId, value);
}
public string VisorId
{
get => _visorId;
internal set => SetField(ref _visorId, value);
}
public string NamePlateId
{
get => _namePlateId;
internal set => SetField(ref _namePlateId, value);
}
private byte HatSequenceId { get; set; } = 0;
private byte PetSequenceId { get; set; } = 0;
private byte SkinSequenceId { get; set; } = 0;
private byte VisorSequenceId { get; set; } = 0;
private byte NamePlateSequenceId { get; set; } = 0;
/// <summary>
/// Gets a value indicating whether a player outfit is complete or whether it needs to receive additional cosmetics.
/// </summary>
/// <returns>true if the outfit is incomplete, false if the outfit is complete.</returns>
public bool IsIncomplete => string.IsNullOrEmpty(PlayerName)
|| Color == UnknownColor
|| HatId == "missing"
|| PetId == "missing"
|| SkinId == "missing"
|| VisorId == "missing"
|| NamePlateId == "missing";
public void Serialize(IMessageWriter writer)
{
writer.Write(PlayerName);
writer.WritePacked((int)Color);
// Follow officials by not sending a color value that is out of range. Sending -1 can cause exceptions on the client side.
writer.WritePacked(Color == UnknownColor ? 0 : (int)Color);
writer.Write(HatId);
writer.Write(PetId);
writer.Write(SkinId);
writer.Write(VisorId);
writer.Write(NamePlateId);
writer.Write(HatSequenceId);
writer.Write(PetSequenceId);
writer.Write(SkinSequenceId);
writer.Write(VisorSequenceId);
writer.Write(NamePlateSequenceId);
}
public void Deserialize(IMessageReader reader)
@@ -49,6 +109,17 @@ namespace Impostor.Api.Innersloth.Customization
SkinId = reader.ReadString();
VisorId = reader.ReadString();
NamePlateId = reader.ReadString();
HatSequenceId = reader.ReadByte();
PetSequenceId = reader.ReadByte();
SkinSequenceId = reader.ReadByte();
VisorSequenceId = reader.ReadByte();
NamePlateSequenceId = reader.ReadByte();
}
private void SetField<T>(ref T field, T value)
{
field = value;
IsDirty = true;
}
}
}
@@ -2,7 +2,6 @@
"0": "SkeldShipStatus",
"1": "MeetingHud",
"2": "LobbyBehaviour",
"3": "GameData",
"4": "PlayerControl",
"5": "MiraShipStatus",
"6": "PolusShipStatus",
@@ -10,5 +9,7 @@
"8": "AirshipStatus",
"9": "HideAndSeekManager",
"10": "NormalGameManager",
"11": "NetworkedPlayerInfo",
"12": "VoteBanSystem",
"13": "FungleShipStatus"
}
@@ -6,5 +6,8 @@
"GuardianAngel": 4,
"Shapeshifter": 5,
"CrewmateGhost": 6,
"ImpostorGhost": 7
"ImpostorGhost": 7,
"Noisemaker": 8,
"Phantom": 9,
"Tracker": 10
}
@@ -55,5 +55,9 @@
"CheckShapeshift": 55,
"RejectShapeshift": 56,
"LobbyTimeExpiring": 60,
"ExtendLobbyTimer": 61
"ExtendLobbyTimer": 61,
"CheckVanish": 62,
"StartVanish": 63,
"CheckAppear": 64,
"StartAppear": 65
}
@@ -0,0 +1,6 @@
{
"Standard": 0,
"StandardRoles": 1,
"Flashlight": 2,
"Custom": 100
}
@@ -0,0 +1,23 @@
{
"None": 0,
"ImpersonationCeleb": 1,
"Spamming": 2,
"InappropriateNameUnsportsmanlike": 3,
"UnsportsmanlikeConduct": 4,
"ImpersonationDevelopers": 5,
"InappropriateChatPersonalInfo": 6,
"InappropriateNameDerogatory": 7,
"InappropriateNameNsfw": 8,
"Bullying": 9,
"CheatingHacking": 10,
"InappropriateChatDating": 11,
"WeaponizingRules": 12,
"RepeatOffender3": 13,
"SexualMisconduct": 14,
"Doxing": 15,
"IllegalActivity": 16,
"Harassment": 17,
"SelfHarmPromotion": 18,
"RepeatOffender10": 19,
"InvalidUser": 20
}
@@ -0,0 +1,4 @@
{
"None": 0,
"AprilFools": 1
}
@@ -570,10 +570,39 @@
"RolesHelp_Engineer_02": 1563,
"RolesHelp_GuardianAngel_01": 1564,
"RolesHelp_GuardianAngel_02": 1565,
"RolesHelp_Noisemaker_01": 1566,
"RolesHelp_Noisemaker_02": 1567,
"RolesHelp_Tracker_01": 1568,
"RolesHelp_Tracker_02": 1569,
"RolesHelp_Shapeshifter_01": 1600,
"RolesHelp_Shapeshifter_02": 1601,
"RolesHelp_Phantom_01": 1602,
"RolesHelp_Phantom_02": 1603,
"RolesHelpOutro_01": 1650,
"RolesHelpOutro_02": 1651,
"NoisemakerRole": 1660,
"NoisemakerBlurb": 1661,
"NoisemakerBlurbMed": 1662,
"NoisemakerBlurbLong": 1663,
"NoisemakerImpostorAlert": 1664,
"NoisemakerAlertDuration": 1665,
"PhantomAbility": 1670,
"PhantomRole": 1671,
"PhantomBlurb": 1672,
"PhantomBlurbMed": 1673,
"PhantomBlurbLong": 1674,
"PhantomCooldown": 1675,
"PhantomDuration": 1676,
"PhantomAbilityUndo": 1677,
"TrackerAbility": 1680,
"TrackerRole": 1681,
"TrackerBlurb": 1682,
"TrackerBlurbMed": 1683,
"TrackerBlurbLong": 1684,
"TrackerCooldown": 1685,
"TrackerDuration": 1686,
"TrackerAbilityUndo": 1687,
"TrackerDelay": 1688,
"SanctionDuration": 1700,
"SanctionPermanent": 1701,
"SanctionConduct": 1702,
@@ -1304,6 +1333,24 @@
"ConfirmDeleteAccountsEmpty": 3209,
"AccountRequestDelete": 3210,
"HasBeenKilled": 3211,
"RoomSettingsLabel": 3280,
"ViewLabel": 3281,
"RoomCodeLabel": 3282,
"ModeLabel": 3283,
"PrivacyLabel": 3284,
"CapacityLabel": 3285,
"GameSettingsLabel": 3286,
"RoleSettingsLabel": 3287,
"GamePresetsLabel": 3288,
"GamePresetsDescription": 3289,
"StandardRulesPresetDescription": 3290,
"GameModeStandard": 3291,
"GameModeStandardRoles": 3292,
"GameModeCustom": 3293,
"ReturnToLobby": 3294,
"GameModeFlashlight": 3295,
"HostNounLabel": 3296,
"RoleSettingsAll": 3297,
"ScrollList": 3300,
"ScrollNews": 3301,
"AmongUsAnnouncements": 3302,
@@ -1331,6 +1378,33 @@
"ReportNotificationBody": 3324,
"MissingGuardianEmailText": 3325,
"AprilFoolsModeToggle": 3326,
"LobbyChangeSettingNotification": 3327,
"ImpostorsCategory": 3350,
"RolesCategory": 3351,
"MeetingsCategory": 3352,
"PlayersCategory": 3353,
"TasksCategory": 3354,
"GameSettingsDescription": 3355,
"RoleSettingsDescription": 3356,
"InfoLabel": 3357,
"RoleQuotaLabel": 3358,
"OverviewCategory": 3359,
"CrewmateRolesHeader": 3360,
"ImpostorRolesHeader": 3361,
"ViewingLabel": 3362,
"LocalLobbyLabel": 3363,
"NumberRoleHeader": 3364,
"ChanceRoleHeader": 3365,
"WaitingForPlayers": 3366,
"HideAndSeekModeDescription": 3367,
"RolesRulesPresetDescription": 3368,
"FlashlightRulesPresetDescription": 3369,
"RoomCodeCopied": 3370,
"FinalHideCategory": 3371,
"LobbyChangeSettingNotificationRole": 3372,
"StandardRulesPresetDescriptionHideAndSeek": 3373,
"GameModeStandardSeek": 3374,
"HostYouLabel": 3375,
"MiningPit": 3400,
"Jungle": 3401,
"BuildSandcastle": 3402,
@@ -1399,5 +1473,11 @@
"HttpErrorBadGateway": 3531,
"HttpErrorServiceUnavailable": 3532,
"HttpErrorGatewayTimeout": 3533,
"UserVersionError": 3534
"UserVersionError": 3534,
"QCLobbyRoleNoisemakers": 3535,
"QCLobbyRoleTrackers": 3536,
"QCLobbyRolePhantoms": 3537,
"QCSelfWasTracking": 3538,
"QCSelfGoingToNoisemaker": 3539,
"QCSelfSawRoleInLocation": 3540
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"dumpostorVersion": "1.0.0",
"gameVersion": "2024.3.5",
"dumpostorVersion": "1.0.1",
"gameVersion": "2024.6.18",
"platformType": "StandaloneItch"
}
@@ -2,7 +2,7 @@ namespace Impostor.Api.Innersloth.GameOptions;
public class HideNSeekGameOptions : IGameOptions
{
public const int LatestVersion = 7;
public const int LatestVersion = 8;
public HideNSeekGameOptions(byte version = LatestVersion)
{
@@ -16,6 +16,12 @@ public class HideNSeekGameOptions : IGameOptions
/// <inheritdoc />
public GameModes GameMode => GameModes.HideNSeek;
/// <inheritdoc />
public SpecialGameModes SpecialMode { get; set; } = SpecialGameModes.None;
/// <inheritdoc />
public RulesPresets RulesPreset { get; set; } = RulesPresets.Custom;
/// <inheritdoc />
public byte MaxPlayers { get; set; } = 15;
@@ -96,6 +102,12 @@ public class HideNSeekGameOptions : IGameOptions
public void Deserialize(IMessageReader reader)
{
if (Version >= 8)
{
SpecialMode = (SpecialGameModes)reader.ReadByte();
RulesPreset = (RulesPresets)reader.ReadByte();
}
MaxPlayers = reader.ReadByte();
Keywords = (GameKeywords)reader.ReadInt32();
Map = (MapTypes)reader.ReadByte();
@@ -121,7 +133,7 @@ public class HideNSeekGameOptions : IGameOptions
MaxPingTime = reader.ReadSingle();
CrewmateTimeInVent = reader.ReadSingle();
if (Version > 7)
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
}
@@ -129,6 +141,12 @@ public class HideNSeekGameOptions : IGameOptions
public void Serialize(IMessageWriter writer)
{
if (Version >= 8)
{
writer.Write((byte)SpecialMode);
writer.Write((byte)RulesPreset);
}
writer.Write(MaxPlayers);
writer.Write((uint)Keywords);
writer.Write((byte)Map);
@@ -154,7 +172,7 @@ public class HideNSeekGameOptions : IGameOptions
writer.Write(MaxPingTime);
writer.Write(CrewmateTimeInVent);
if (Version > 7)
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
}
@@ -10,10 +10,20 @@ public interface IGameOptions
public byte Version { get; }
/// <summary>
/// Gets the currently active gamemode.
/// Gets the currently active gamemode. This is currently used for the Normal and HideAndSeek gamemodes.
/// </summary>
public GameModes GameMode { get; }
/// <summary>
/// Gets the currently active special gamemode. This is currently used for the AprilFools gamemode.
/// </summary>
public SpecialGameModes SpecialMode { get; }
/// <summary>
/// Gets the rule preset.
/// </summary>
public RulesPresets RulesPreset { get; }
/// <summary>
/// Gets or sets the maximum amount of players for this lobby.
/// </summary>
@@ -21,6 +21,12 @@ public class LegacyGameOptionsData : IGameOptions
public GameModes GameMode => GameModes.Normal;
/// <inheritdoc />
public SpecialGameModes SpecialMode { get; } = SpecialGameModes.None;
/// <inheritdoc />
public RulesPresets RulesPreset { get; } = RulesPresets.Custom;
/// <summary>
/// Gets or sets the maximum amount of players for this lobby.
/// </summary>
@@ -4,7 +4,7 @@ namespace Impostor.Api.Innersloth.GameOptions;
public class NormalGameOptions : IGameOptions
{
public const int LatestVersion = 7;
public const int LatestVersion = 8;
public NormalGameOptions(byte version = LatestVersion)
{
@@ -19,6 +19,12 @@ public class NormalGameOptions : IGameOptions
/// <inheritdoc />
public GameModes GameMode => GameModes.Normal;
/// <inheritdoc />
public SpecialGameModes SpecialMode { get; set; } = SpecialGameModes.None;
/// <inheritdoc />
public RulesPresets RulesPreset { get; set; } = RulesPresets.Custom;
/// <inheritdoc />
public byte MaxPlayers { get; set; } = 10;
@@ -133,6 +139,12 @@ public class NormalGameOptions : IGameOptions
public void Deserialize(IMessageReader reader)
{
if (Version >= 8)
{
SpecialMode = (SpecialGameModes)reader.ReadByte();
RulesPreset = (RulesPresets)reader.ReadByte();
}
MaxPlayers = reader.ReadByte();
Keywords = (GameKeywords)reader.ReadUInt32();
Map = (MapTypes)reader.ReadByte();
@@ -163,7 +175,7 @@ public class NormalGameOptions : IGameOptions
RoleOptions.Deserialize(reader);
if (Version > 7)
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
}
@@ -171,6 +183,12 @@ public class NormalGameOptions : IGameOptions
public void Serialize(IMessageWriter writer)
{
if (Version >= 8)
{
writer.Write((byte)SpecialMode);
writer.Write((byte)RulesPreset);
}
writer.Write(MaxPlayers);
writer.Write((uint)Keywords);
writer.Write((byte)Map);
@@ -201,7 +219,7 @@ public class NormalGameOptions : IGameOptions
RoleOptions.Serialize(writer);
if (Version > 7)
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
}
@@ -0,0 +1,33 @@
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
public class NoisemakerRoleOptions : IRoleOptions
{
public NoisemakerRoleOptions(byte version)
{
Version = version;
}
public byte Version { get; }
public RoleTypes Type => RoleTypes.Noisemaker;
public bool ImpostorAlert { get; set; } = true;
public byte AlertDuration { get; set; } = 10;
public static NoisemakerRoleOptions Deserialize(IMessageReader reader, byte version)
{
var options = new NoisemakerRoleOptions(version);
options.AlertDuration = reader.ReadByte();
options.ImpostorAlert = reader.ReadBoolean();
return options;
}
public void Serialize(IMessageWriter writer)
{
writer.Write(AlertDuration);
writer.Write(ImpostorAlert);
}
}
@@ -0,0 +1,33 @@
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
public class PhantomRoleOptions : IRoleOptions
{
public PhantomRoleOptions(byte version)
{
Version = version;
}
public byte Version { get; }
public RoleTypes Type => RoleTypes.Phantom;
public byte Cooldown { get; set; } = 15;
public byte Duration { get; set; } = 30;
public static PhantomRoleOptions Deserialize(IMessageReader reader, byte version)
{
var options = new PhantomRoleOptions(version);
options.Cooldown = reader.ReadByte();
options.Duration = reader.ReadByte();
return options;
}
public void Serialize(IMessageWriter writer)
{
writer.Write(Cooldown);
writer.Write(Duration);
}
}
@@ -31,6 +31,9 @@ public class RoleOptionsCollection
RoleTypes.Engineer => EngineerRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.GuardianAngel => GuardianAngelRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Shapeshifter => ShapeshifterRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Noisemaker => NoisemakerRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Phantom => PhantomRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Tracker => TrackerRoleOptions.Deserialize(roleOptionsReader, Version),
_ => throw new ArgumentOutOfRangeException(nameof(roleType), roleType, null),
};
@@ -0,0 +1,37 @@
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
public class TrackerRoleOptions : IRoleOptions
{
public TrackerRoleOptions(byte version)
{
Version = version;
}
public byte Version { get; }
public RoleTypes Type => RoleTypes.Tracker;
public byte Cooldown { get; set; } = 15;
public byte Duration { get; set; } = 30;
public byte Delay { get; set; } = 1;
public static TrackerRoleOptions Deserialize(IMessageReader reader, byte version)
{
var options = new TrackerRoleOptions(version);
options.Cooldown = reader.ReadByte();
options.Duration = reader.ReadByte();
options.Delay = reader.ReadByte();
return options;
}
public void Serialize(IMessageWriter writer)
{
writer.Write(Cooldown);
writer.Write(Duration);
writer.Write(Delay);
}
}
@@ -1,6 +1,6 @@
namespace Impostor.Api.Net.Inner.Objects
{
public interface IInnerGameData : IInnerNetObject
public interface IInnerGameData
{
}
}
@@ -2,13 +2,15 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc06SetName
{
public static void Serialize(IMessageWriter writer, string name)
public static void Serialize(IMessageWriter writer, uint netId, string name)
{
writer.Write(netId);
writer.Write(name);
}
public static void Deserialize(IMessageReader reader, out string name)
public static void Deserialize(IMessageReader reader, out uint netId, out string name)
{
netId = reader.ReadUInt32();
name = reader.ReadString();
}
}
@@ -4,13 +4,15 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc08SetColor
{
public static void Serialize(IMessageWriter writer, ColorType color)
public static void Serialize(IMessageWriter writer, uint netId, ColorType color)
{
writer.Write(netId);
writer.Write((byte)color);
}
public static void Deserialize(IMessageReader reader, out ColorType color)
public static void Deserialize(IMessageReader reader, out uint netId, out ColorType color)
{
netId = reader.ReadUInt32();
color = (ColorType)reader.ReadByte();
}
}
@@ -4,14 +4,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc44SetRole
{
public static void Serialize(IMessageWriter writer, RoleTypes role)
public static void Serialize(IMessageWriter writer, RoleTypes role, bool canOverrideRole)
{
writer.Write((ushort)role);
writer.Write(canOverrideRole);
}
public static void Deserialize(IMessageReader reader, out RoleTypes role)
public static void Deserialize(IMessageReader reader, out RoleTypes role, out bool canOverrideRole)
{
role = (RoleTypes)reader.ReadUInt16();
canOverrideRole = reader.ReadBoolean();
}
}
}
@@ -29,6 +29,8 @@ namespace Impostor.Server.Net.Inner
public SpawnFlags SpawnFlags { get; internal set; }
public virtual bool IsDirty { get; internal set; }
public bool IsOwnedBy(IClientPlayer player)
{
return OwnerId == player.Client.Id ||
@@ -21,6 +21,7 @@ namespace Impostor.Server.Net.Inner.Objects.Components
{
_logger = logger;
_votes = new Dictionary<int, int[]>();
Components.Add(this);
}
public override ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
@@ -1,41 +1,20 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using Impostor.Api;
using Impostor.Api.Events.Managers;
using Impostor.Api.Net;
using Impostor.Api.Net.Custom;
using Impostor.Api.Net.Inner;
using Impostor.Api.Net.Inner.Objects;
using Impostor.Api.Net.Messages.Rpcs;
using Impostor.Server.Net.Inner.Objects.Components;
using Impostor.Server.Net.State;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Impostor.Server.Net.Inner.Objects
{
internal partial class InnerGameData : InnerNetObject, IInnerGameData
internal partial class InnerGameData : IInnerGameData
{
private readonly ILogger<InnerGameData> _logger;
private readonly IEventManager _eventManager;
private readonly ConcurrentDictionary<byte, InnerPlayerInfo> _allPlayers;
public InnerGameData(ICustomMessageManager<ICustomRpc> customMessageManager, Game game, ILogger<InnerGameData> logger, IEventManager eventManager, IServiceProvider serviceProvider) : base(customMessageManager, game)
{
_logger = logger;
_eventManager = eventManager;
_allPlayers = new ConcurrentDictionary<byte, InnerPlayerInfo>();
Components.Add(this);
Components.Add(ActivatorUtilities.CreateInstance<InnerVoteBanSystem>(serviceProvider, game));
}
private readonly ConcurrentDictionary<byte, InnerPlayerInfo> _allPlayers = new();
private readonly ConcurrentDictionary<int, InnerPlayerInfo> _allPlayersByClientId = new();
public int PlayerCount => _allPlayers.Count;
public IReadOnlyDictionary<byte, InnerPlayerInfo> Players => _allPlayers;
internal IReadOnlyDictionary<int, InnerPlayerInfo> PlayersByClientId => _allPlayersByClientId;
public InnerPlayerInfo? GetPlayerById(byte id)
{
if (id == byte.MaxValue)
@@ -46,103 +25,33 @@ namespace Impostor.Server.Net.Inner.Objects
return _allPlayers.TryGetValue(id, out var player) ? player : null;
}
public override ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
internal bool AddPlayer(InnerPlayerInfo playerInfo)
{
throw new NotImplementedException();
return _allPlayers.TryAdd(playerInfo.PlayerId, playerInfo) &&
_allPlayersByClientId.TryAdd(playerInfo.ClientId, playerInfo);
}
public override async ValueTask DeserializeAsync(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
internal void RemovePlayer(byte playerId)
{
if (!await ValidateHost(CheatContext.Deserialize, sender))
_allPlayers.TryRemove(playerId, out var player);
if (player != null)
{
return;
_allPlayersByClientId.TryRemove(player.ClientId, out _);
}
}
while (reader.Position < reader.Length)
internal byte GetNextAvailablePlayerId()
{
for (byte i = 0; i < byte.MaxValue; i++)
{
var inner = reader.ReadMessage();
var playerInfo = this.GetPlayerById(inner.Tag);
if (playerInfo != null)
if (!Players.ContainsKey(i))
{
playerInfo.Deserialize(inner);
}
else
{
playerInfo = new InnerPlayerInfo(inner.Tag);
playerInfo.Deserialize(inner);
if (!_allPlayers.TryAdd(playerInfo.PlayerId, playerInfo))
{
throw new ImpostorException("Failed to add player to InnerGameData.");
}
return i;
}
}
}
public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
{
if (!await ValidateHost(call, sender))
{
return false;
}
switch (call)
{
case RpcCalls.SetTasks:
{
Rpc29SetTasks.Deserialize(reader, out var playerId, out var taskTypeIds);
SetTasks(playerId, taskTypeIds);
break;
}
default:
return await base.HandleRpcAsync(sender, target, call, reader);
}
return true;
}
internal InnerPlayerInfo? AddPlayer(InnerPlayerControl control)
{
var playerId = control.PlayerId;
var playerInfo = new InnerPlayerInfo(control.PlayerId);
if (_allPlayers.TryAdd(playerId, playerInfo))
{
return playerInfo;
}
return null;
}
internal void RemovePlayer(InnerPlayerControl control)
{
_allPlayers.TryRemove(control.PlayerInfo.PlayerId, out _);
}
private void SetTasks(byte playerId, ReadOnlyMemory<byte> taskTypeIds)
{
var player = GetPlayerById(playerId);
if (player == null)
{
_logger.LogTrace("Could not set tasks for playerId {0}.", playerId);
return;
}
if (player.Disconnected)
{
return;
}
player.Tasks = new List<TaskInfo>(taskTypeIds.Length);
var taskId = 0u;
foreach (var taskTypeId in taskTypeIds.Span)
{
var mapTasks = Game.GameNet!.ShipStatus?.Data.Tasks;
var taskType = (mapTasks != null && mapTasks.ContainsKey(taskTypeId)) ? mapTasks[taskTypeId] : null;
player.Tasks.Add(new TaskInfo(player, _eventManager, taskId++, taskType));
}
return byte.MaxValue;
}
}
}
@@ -20,8 +20,9 @@ namespace Impostor.Server.Net.Inner.Objects
public async ValueTask SetNameAsync(string name)
{
PlayerInfo.PlayerName = name;
PlayerInfo.CurrentOutfit.PlayerName = name;
// TODO check playerinfo ownership
using var writer = Game.StartRpc(NetId, RpcCalls.SetName);
writer.Write(name);
await Game.FinishRpcAsync(writer);
@@ -32,7 +33,7 @@ namespace Impostor.Server.Net.Inner.Objects
PlayerInfo.CurrentOutfit.Color = color;
using var writer = Game.StartRpc(NetId, RpcCalls.SetColor);
Rpc08SetColor.Serialize(writer, color);
Rpc08SetColor.Serialize(writer, PlayerInfo.NetId, color);
await Game.FinishRpcAsync(writer);
}
@@ -167,7 +167,7 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
Rpc06SetName.Deserialize(reader, out var name);
Rpc06SetName.Deserialize(reader, out var _, out var name);
return await HandleSetName(sender, name);
}
@@ -191,7 +191,7 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
Rpc08SetColor.Deserialize(reader, out var color);
Rpc08SetColor.Deserialize(reader, out var _, out var color);
return await HandleSetColor(sender, color);
}
@@ -383,8 +383,16 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
Rpc44SetRole.Deserialize(reader, out var role);
Rpc44SetRole.Deserialize(reader, out var role, out var _);
if (role is RoleTypes.ImpostorGhost or RoleTypes.CrewmateGhost or RoleTypes.GuardianAngel)
{
PlayerInfo.RoleWhenAlive = PlayerInfo.RoleType;
PlayerInfo.IsDead = true;
}
PlayerInfo.RoleType = role;
PlayerInfo.IsDirty = true;
if (Game.GameState == GameStates.Starting && Game.Players.All(clientPlayer => clientPlayer.Character?.PlayerInfo.RoleType != null))
{
@@ -530,6 +538,7 @@ namespace Impostor.Server.Net.Inner.Objects
{
PlayerInfo.IsDead = true;
PlayerInfo.LastDeathReason = reason;
PlayerInfo.IsDirty = true;
}
internal void Protect(InnerPlayerControl guardianAngel)
@@ -679,7 +688,7 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
PlayerInfo.PlayerName = name;
PlayerInfo.CurrentOutfit.PlayerName = name;
return true;
}
@@ -3,7 +3,7 @@ using Impostor.Api.Net.Inner.Objects;
namespace Impostor.Server.Net.Inner.Objects
{
internal partial class InnerPlayerInfo : IInnerPlayerInfo
internal partial class InnerPlayerInfo : InnerNetObject, IInnerPlayerInfo
{
IEnumerable<ITaskInfo> IInnerPlayerInfo.Tasks => Tasks;
}
@@ -1,25 +1,35 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Impostor.Api.Games;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.Customization;
using Impostor.Api.Innersloth.GameOptions;
using Impostor.Api.Net;
using Impostor.Api.Net.Custom;
using Impostor.Api.Utils;
using Impostor.Server.Net.State;
using Microsoft.Extensions.Logging;
namespace Impostor.Server.Net.Inner.Objects
{
internal partial class InnerPlayerInfo
{
public InnerPlayerInfo(byte playerId)
private readonly ILogger<InnerPlayerInfo> _logger;
public InnerPlayerInfo(ICustomMessageManager<ICustomRpc> customMessageManager, Game game, ILogger<InnerPlayerInfo> logger) : base(customMessageManager, game)
{
PlayerId = playerId;
Components.Add(this);
_logger = logger;
}
public InnerPlayerControl? Controller { get; internal set; }
public byte PlayerId { get; }
public byte PlayerId { get; internal set; }
public string PlayerName { get; internal set; } = string.Empty;
public int ClientId { get; internal set; }
public string PlayerName => CurrentOutfit.PlayerName;
public Dictionary<PlayerOutfitType, PlayerOutfit> Outfits { get; } = new()
{
@@ -32,6 +42,8 @@ namespace Impostor.Server.Net.Inner.Objects
public RoleTypes? RoleType { get; internal set; }
public RoleTypes? RoleWhenAlive { get; internal set; }
public bool Disconnected { get; internal set; }
public bool IsImpostor => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.ImpostorGhost;
@@ -48,6 +60,12 @@ namespace Impostor.Server.Net.Inner.Objects
public uint PlayerLevel { get; internal set; }
public override bool IsDirty
{
get => CurrentOutfit.IsDirty;
internal set => CurrentOutfit.IsDirty = value;
}
public bool CanMurder(IGame game, IDateTimeProvider dateTimeProvider)
{
if (!IsImpostor)
@@ -71,13 +89,56 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
public void Serialize(IMessageWriter writer)
public override ValueTask<bool> SerializeAsync(IMessageWriter writer, bool initialState)
{
throw new NotImplementedException();
writer.Write(PlayerId);
writer.WritePacked(ClientId);
writer.Write((byte)Outfits.Count);
foreach (var outfit in Outfits)
{
writer.Write((byte)outfit.Key);
outfit.Value.Serialize(writer);
}
writer.WritePacked(PlayerLevel);
var flag = 0;
if (Disconnected)
{
flag = (byte)(flag | 1u);
}
if (IsDead)
{
flag = (byte)(flag | 4u);
}
writer.Write((byte)flag);
writer.Write((ushort)(RoleType ?? 0));
writer.Write(RoleWhenAlive.HasValue);
if (RoleWhenAlive.HasValue)
{
writer.Write((ushort)RoleWhenAlive.Value);
}
writer.Write((byte)Tasks.Count);
for (var i = 0; i < Tasks.Count; i++)
{
Tasks[i].Serialize(writer);
}
writer.Write(string.Empty); // FriendCode
writer.Write(string.Empty); // PUID
return new ValueTask<bool>(true);
}
public void Deserialize(IMessageReader reader)
public override ValueTask DeserializeAsync(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState)
{
PlayerId = reader.ReadByte();
ClientId = reader.ReadPackedInt32();
Outfits.Clear();
var b = reader.ReadByte();
for (var i = 0; i < b; i++)
@@ -93,7 +154,15 @@ namespace Impostor.Server.Net.Inner.Objects
Disconnected = (flag & 1) != 0;
IsDead = (flag & 4) != 0;
_ = (RoleTypes)reader.ReadUInt16(); // ignore the RoleType here and only trust the SetRole rpc
// Ignore the RoleType here and only trust the SetRole RPC, as
// RoleType is not nullable in vanilla, while Impostor checks game
// starts based on assigned roles.
_ = (RoleTypes)reader.ReadUInt16();
if (reader.ReadBoolean())
{
RoleWhenAlive = (RoleTypes)reader.ReadUInt16();
}
var taskCount = reader.ReadByte();
@@ -106,6 +175,12 @@ namespace Impostor.Server.Net.Inner.Objects
{
Tasks[i].Deserialize(reader);
}
// Impostor doesn't expose fields that aren't properly validated
reader.ReadString(); // FriendCode
reader.ReadString(); // PUID
return ValueTask.CompletedTask;
}
}
}
@@ -17,13 +17,8 @@ internal class CompatibilityManager : ICompatibilityManager
{
new[]
{
new GameVersion(2023, 10, 1), // 2023.10.24
new GameVersion(2023, 11, 15), // 2023.11.28
},
new[]
{
new GameVersion(2024, 2, 1), // 2024.3.5
new GameVersion(2024, 2, 3), // 2024.6.4
new GameVersion(2024, 3, 1), // 2024.6.18
new GameVersion(2024, 4, 1), // 2024.8.13
},
};
+95 -12
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Impostor.Api;
using Impostor.Api.Innersloth;
@@ -30,12 +31,21 @@ namespace Impostor.Server.Net.State
/// </summary>
private const int CurrentClient = -3;
/// <summary>
/// Used to list objects that are managed by the game server.
/// </summary>
private const int ServerOwned = -4;
/// <summary>
/// The first NetId that is considered as a server owned Network ID that the client will not allocate by default.
/// </summary>
private const int MinServerNetId = 100000;
private static readonly Dictionary<uint, Type> SpawnableObjects = new()
{
[0] = typeof(InnerSkeldShipStatus),
[1] = typeof(InnerMeetingHud),
[2] = typeof(InnerLobbyBehaviour),
[3] = typeof(InnerGameData),
[4] = typeof(InnerPlayerControl),
[5] = typeof(InnerMiraShipStatus),
[6] = typeof(InnerPolusShipStatus),
@@ -43,13 +53,19 @@ namespace Impostor.Server.Net.State
[8] = typeof(InnerAirshipStatus),
[9] = typeof(InnerHideAndSeekManager),
[10] = typeof(InnerNormalGameManager),
[11] = typeof(InnerPlayerInfo),
[12] = typeof(InnerVoteBanSystem),
[13] = typeof(InnerFungleShipStatus),
};
private static readonly Dictionary<Type, uint> SpawnableObjectIds = SpawnableObjects.ToDictionary((i) => i.Value, (i) => i.Key);
private readonly List<InnerNetObject> _allObjects = new List<InnerNetObject>();
private readonly Dictionary<uint, InnerNetObject> _allObjectsFast = new Dictionary<uint, InnerNetObject>();
private uint _nextNetId = MinServerNetId;
public T? FindObjectByNetId<T>(uint netId)
where T : IInnerNetObject
{
@@ -254,6 +270,10 @@ namespace Impostor.Server.Net.State
sender.Scene = scene;
_logger.LogTrace("> Scene {0} to {1}", clientId, sender.Scene);
await SyncServerObjectsAsync(sender);
await SpawnPlayerInfoAsync(sender);
break;
}
@@ -306,6 +326,17 @@ namespace Impostor.Server.Net.State
}
}
// Check for dirty netobjects
foreach (var netObject in _allObjectsFast.Values)
{
if (netObject.IsDirty && netObject.OwnerId == ServerOwned)
{
_logger.LogTrace("Sending over {Type} {NetId}", netObject.GetType().Name, netObject.NetId);
await SendObjectData(netObject);
netObject.IsDirty = false;
}
}
return true;
}
@@ -325,9 +356,17 @@ namespace Impostor.Server.Net.State
break;
}
case InnerGameData data:
case InnerPlayerInfo playerInfo:
{
GameNet.GameData = data;
if (!GameNet.GameData.AddPlayer(playerInfo))
{
_logger.LogWarning(
"Could not add PlayerInfo for playerId {PlayerId} with NetId {newId}, already have NetId {oldNetId}",
playerInfo.PlayerId,
playerInfo.NetId,
GameNet.GameData.GetPlayerById(playerInfo.PlayerId)?.NetId);
}
break;
}
@@ -357,7 +396,7 @@ namespace Impostor.Server.Net.State
}
// Hook up InnerPlayerControl <-> InnerPlayerControl.PlayerInfo.
var playerInfo = GameNet.GameData!.GetPlayerById(control.PlayerId) ?? GameNet.GameData.AddPlayer(control);
var playerInfo = GameNet.GameData.GetPlayerById(control.PlayerId);
if (playerInfo != null)
{
@@ -401,12 +440,6 @@ namespace Impostor.Server.Net.State
break;
}
case InnerGameData:
{
GameNet.GameData = null;
break;
}
case InnerVoteBanSystem:
{
GameNet.VoteBan = null;
@@ -419,13 +452,18 @@ namespace Impostor.Server.Net.State
break;
}
case InnerPlayerControl control:
case InnerPlayerInfo playerInfo:
{
if (GameState != GameStates.Started && GameState != GameStates.Starting)
{
GameNet.GameData?.RemovePlayer(control);
GameNet.GameData.RemovePlayer(playerInfo.PlayerId);
}
break;
}
case InnerPlayerControl control:
{
// Remove InnerPlayerControl <-> IClientPlayer.
if (TryGetPlayer(control.OwnerId, out var player))
{
@@ -438,6 +476,51 @@ namespace Impostor.Server.Net.State
}
}
private async ValueTask SyncServerObjectsAsync(ClientPlayer sender)
{
foreach (var obj in _allObjectsFast.Values)
{
if (obj.OwnerId == ServerOwned)
{
_logger.LogTrace("Syncing {Type} {NetId}", obj.GetType(), obj.NetId);
await SendObjectSpawnAsync(obj, sender.Client.Id);
}
}
}
private async ValueTask SpawnPlayerInfoAsync(ClientPlayer sender)
{
// Hosts spawn PlayerInfo objects if they requested authority
if (IsHostAuthoritive)
{
return;
}
// Only spawn a new PlayerInfo if one has not yet been spawned
if (GameNet.GameData.PlayersByClientId.ContainsKey(sender.Client.Id))
{
return;
}
var playerInfo = (InnerPlayerInfo)ActivatorUtilities.CreateInstance(_serviceProvider, typeof(InnerPlayerInfo), this);
playerInfo.SpawnFlags = SpawnFlags.None;
playerInfo.NetId = _nextNetId++;
playerInfo.OwnerId = ServerOwned;
playerInfo.ClientId = sender.Client.Id;
playerInfo.PlayerId = GameNet.GameData.GetNextAvailablePlayerId();
if (!AddNetObject(playerInfo))
{
_logger.LogError("Couldn't spawn PlayerInfo for {Name} ({ClientId})", sender.Client.Name, sender.Client.Id);
playerInfo.NetId = uint.MaxValue;
return;
}
_logger.LogTrace("Spawning PlayerInfo (netId {Netid})", playerInfo.NetId);
await OnSpawnAsync(sender, playerInfo);
await SendObjectSpawnAsync(playerInfo);
}
private bool AddNetObject(InnerNetObject obj)
{
if (_allObjectsFast.ContainsKey(obj.NetId))
@@ -112,5 +112,46 @@ namespace Impostor.Server.Net.State
{
Message12WaitForHostS2C.Serialize(message, clear, Code, player.Client.Id);
}
private async ValueTask SendObjectSpawnAsync(InnerNetObject obj, int? targetClientId = null)
{
using var writer = StartGameData(targetClientId);
writer.StartMessage(GameDataTag.SpawnFlag);
writer.WritePacked(SpawnableObjectIds[obj.GetType()]);
writer.WritePacked(obj.OwnerId);
writer.Write((byte)obj.SpawnFlags);
var components = obj.GetComponentsInChildren<InnerNetObject>();
writer.WritePacked(components.Count);
foreach (var component in components)
{
writer.WritePacked(obj.NetId);
writer.StartMessage(1);
await component.SerializeAsync(writer, true);
writer.EndMessage();
}
writer.EndMessage();
await FinishGameDataAsync(writer, targetClientId);
}
private ValueTask SendObjectDespawn(InnerNetObject obj, int? targetClientId = null)
{
using var writer = StartGameData(targetClientId);
writer.StartMessage(GameDataTag.DespawnFlag);
writer.WritePacked(obj.NetId);
writer.EndMessage();
return FinishGameDataAsync(writer, targetClientId);
}
private async ValueTask SendObjectData(InnerNetObject obj, int? targetClientId = null)
{
using var writer = StartGameData(targetClientId);
writer.StartMessage(GameDataTag.DataFlag);
writer.WritePacked(obj.NetId);
await obj.SerializeAsync(writer, false);
writer.EndMessage();
await FinishGameDataAsync(writer, targetClientId);
}
}
}
@@ -85,6 +85,24 @@ namespace Impostor.Server.Net.State
}
});
// Clean up the PlayerInfo if we own it
foreach (var playerInfo in GameNet.GameData.Players.Values)
{
if (playerInfo.ClientId == playerId)
{
if (playerInfo.OwnerId == ServerOwned)
{
_logger.LogInformation("Destroying PlayerInfo {nid}", playerInfo.NetId);
GameNet.GameData.RemovePlayer(playerInfo.PlayerId);
RemoveNetObject(playerInfo);
await SendObjectDespawn(playerInfo);
}
break;
}
}
return true;
}
+1 -1
View File
@@ -12,7 +12,7 @@ namespace Impostor.Server.Net.State
IInnerLobbyBehaviour? IGameNet.LobbyBehaviour => LobbyBehaviour;
IInnerGameData? IGameNet.GameData => GameData;
IInnerGameData IGameNet.GameData => GameData;
IInnerVoteBanSystem? IGameNet.VoteBan => VoteBan;
+1 -1
View File
@@ -11,7 +11,7 @@ namespace Impostor.Server.Net.State
public InnerLobbyBehaviour? LobbyBehaviour { get; internal set; }
public InnerGameData? GameData { get; internal set; }
public InnerGameData GameData { get; internal set; } = new();
public InnerVoteBanSystem? VoteBan { get; internal set; }