using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using AmongUs.GameOptions; using AmongUs.QuickChat; using EHR.Gamemodes; using EHR.Patches; using EHR.Roles; using HarmonyLib; using Hazel; using InnerNet; using UnityEngine; using static EHR.Translator; namespace EHR.Modules; public enum CustomRPC { // Vanilla RpcCalls currently (2025.11.18) uses 65 // It is better to leave the remaining RPC numbers as a reserve in case AU adds new RpcCalls VersionCheck = 102, RequestRetryVersionCheck, SyncCustomSettings, SetDeathReason, EndGame, PlaySound, SetCustomRole, SetNameColorData, SetRealKiller, SyncLobbyTimer, AntiBlackout, PlayCustomSound, SetKillTimer = 114, /* RED SUS Ranzion 64 RPC = 115 */ SyncAllPlayerNames = 116, SyncAllClientRealNames, SyncNameNotify, KnCheat = 119, ShowPopUp, KillFlash, SyncAbilityUseLimit, RemoveAbilityUseLimit, RemoveSubRole, Arrow, FixModdedClientCNO, SyncAbilityCD, SyncGeneralOptions, SyncRoleData, NotificationPopper, RequestCommandProcessing, // Roles DoSpell, SniperSync, SetLoversPlayers, SetExecutionerTarget, RemoveExecutionerTarget, SetLawyerTarget, RemoveLawyerTarget, SendFireworkerState, SetCurrentDousingTarget, SetEvilTrackerTarget, SetBountyTarget, SetKillOrSpell, SetDousedPlayer, SetPlaguedPlayer, SetDrawPlayer, SyncHeadHunter, SyncRabbit, SyncSoulHunter, BAU = 150, SyncMycologist, SyncBubble, AddTornado, SyncHookshot, SyncStressedTimer, SetLibrarianMode, SyncYinYanger, DruidAddTrigger, SyncMafiosoData, SyncMafiosoPistolCD, SyncDamoclesTimer, SyncChronomancer, PenguinSync, Sicko = 164, SyncInfection, SetAlchemistPotion, SetRicochetTarget, SetTetherTarget, SetHitmanTarget, SetWeaponMasterMode, SyncGlitchTimers, SyncSpy, SetHackerLimit, SetCurrentDrawTarget, SetCpTasksDone, SetDemonHealth, SetPelicanEtenNum, VigilanteKill, SetGhostPlayer, SetStalkerKillCount, SetConsigliere, SetGreedyOe, SetCollectorVotes, SetQuickShooterShotLimit, GuessKill, SetMarkedPlayer, SetMedicalerProtectList, SyncPsychicRedList, SetCleanserCleanLimit, SetJailorTarget, SetDoppelgangerStealLimit, SetSwapperVotes, Judge, Guess, MeetingKill, NemesisRevenge, SetBanditStealLimit, SyncFollowerTargetAndTimes, SyncRomanticTarget, SyncVengefulRomanticTarget, SetRevealedPlayer, SetCurrentRevealTarget, RpcPassBomb, SyncPostman, SyncChangeling, SyncSentry, SyncBargainer, SyncOverheat, SetDoomsayerProgress = 209, /* * SUBMERGED RPCs * 210 - SetCustomData * 211 - RequestChangeFloor * 212 - AcknowledgeChangeFloor * 213 - EngineVent * 214 - OxygenDeath */ SetTrackerTarget = 215, SyncIntrovert, SyncAllergic, SyncAsthmatic, InspectorCommand, Invisibility, ResetAbilityCooldown, SyncCamouflage, SetChatVisible, Exclusionary, Deadlined, Blessed, Necronomicon, Stained, Entombed, Committed, Haunter, CTA, // Game Modes RoomRushDataSync, FFASync, QuizSync, HNSSync, HotPotatoSync, SoloPVPSync, CTFSync, KOTZSync, SpeedrunSync, NaturalDisastersSync, TMGSync, BedWarsSync, DeathraceSync, DoomTagSync // The total number of RPCs must not exceed 255 // Because HandleRpc accepts Rpc in byte (max 255) system, and it will be impossible to use int } public enum Sounds { KillSound, TaskComplete, TaskUpdateSound, SabotageSound, ImpDiscovered, LobbyTimerPopUp, SpawnSound, ImpTransform, YeehawTransform } [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] internal static class RPCHandlerPatch { private static readonly Dictionary> NumRPCsThisSecond = []; private static readonly Dictionary RateLimitWhiteList = []; // By Rabek009 private static readonly Dictionary RpcRateLimit = new() { [RpcCalls.PlayAnimation] = 3, [RpcCalls.CompleteTask] = 2, [RpcCalls.CheckColor] = 10, [RpcCalls.SendChat] = 2, [RpcCalls.SetScanner] = 10, [RpcCalls.SetStartCounter] = 15, [RpcCalls.EnterVent] = 3, [RpcCalls.ExitVent] = 3, [RpcCalls.SnapTo] = 8, [RpcCalls.ClimbLadder] = 1, [RpcCalls.UsePlatform] = 20, [RpcCalls.SendQuickChat] = 1, [RpcCalls.SetHatStr] = 10, [RpcCalls.SetSkinStr] = 10, [RpcCalls.SetPetStr] = 10, [RpcCalls.SetVisorStr] = 10, [RpcCalls.SetNamePlateStr] = 10, [RpcCalls.CheckMurder] = 25, [RpcCalls.CheckProtect] = 25, [RpcCalls.Pet] = 40, [RpcCalls.CancelPet] = 40, [RpcCalls.CheckZipline] = 1, [RpcCalls.CheckSpore] = 5, [RpcCalls.CheckShapeshift] = 25, [RpcCalls.CheckVanish] = 25, [RpcCalls.CheckAppear] = 25, [RpcCalls.QueueOverruleVotes] = 3 }; public static void WhiteListFromRateLimitUntil(byte id, long timestamp) { if (RateLimitWhiteList.TryGetValue(id, out var ts) && ts > timestamp) return; RateLimitWhiteList[id] = timestamp; } public static void RemoveExpiredWhiteList() { long ts = Utils.TimeStamp; foreach (var key in RateLimitWhiteList.Keys.ToArray()) if (RateLimitWhiteList[key] < ts) RateLimitWhiteList.Remove(key); } private static bool TrustedRpc(byte id) { if (id == 115) return true; if (SubmergedCompatibility.IsSubmerged() && id is >= 120 and <= 124) return true; return (CustomRPC)id is CustomRPC.VersionCheck or CustomRPC.RequestRetryVersionCheck or CustomRPC.AntiBlackout or CustomRPC.SyncNameNotify or CustomRPC.RequestCommandProcessing or CustomRPC.Judge or CustomRPC.SetSwapperVotes or CustomRPC.MeetingKill or CustomRPC.Guess or CustomRPC.NemesisRevenge or CustomRPC.BAU or CustomRPC.TMGSync or CustomRPC.InspectorCommand; } private static bool CheckRateLimit(PlayerControl __instance, RpcCalls rpcType) { if (!AmongUsClient.Instance.AmHost || __instance.AmOwner || (__instance.IsModdedClient() && rpcType == RpcCalls.SendChat)) return true; if (NumRPCsThisSecond.TryAdd(__instance.PlayerId, [])) LateTask.New(() => NumRPCsThisSecond.Remove(__instance.PlayerId), 1f, log: false); Dictionary calls = NumRPCsThisSecond[__instance.PlayerId]; if (!calls.TryAdd(rpcType, 1)) calls[rpcType]++; if ((!RateLimitWhiteList.TryGetValue(__instance.PlayerId, out long expireTS) || expireTS < Utils.TimeStamp) && RpcRateLimit.TryGetValue(rpcType, out int limit) && calls[rpcType] > limit) { bool kick = Options.EnableEHRRateLimit.GetBool(); if (kick) AmongUsClient.Instance.KickPlayer(__instance.OwnerId, false); Logger.SendInGame(string.Format(GetString("Warning.TooManyRPCs"), kick ? __instance.Data?.PlayerName : "Someone"), Color.yellow); Logger.Warn($"Sent {calls[rpcType]} RPCs of type {rpcType} ({(byte)rpcType}), which exceeds the limit of {limit}. Kicking player.", "Kick"); return !kick; } return true; } public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) { var rpcType = (RpcCalls)callId; MessageReader subReader = MessageReader.Get(reader); try { if (EAC.ReceiveRpc(__instance, callId, reader)) { subReader.Recycle(); return false; } Logger.Info($"From ID: {__instance?.Data?.PlayerId} ({(__instance?.Data?.PlayerId == 0 ? "Host" : __instance?.Data?.PlayerName)}) : {callId} ({RPC.GetRpcName(callId)})", "ReceiveRPC"); if (__instance != null) { if (!__instance.IsTrusted() && !CheckRateLimit(__instance, rpcType)) { subReader.Recycle(); return false; } switch (rpcType) { case RpcCalls.SetName: subReader.ReadUInt32(); string name = subReader.ReadString(); if (subReader.BytesRemaining > 0 && subReader.ReadBoolean()) { subReader.Recycle(); return false; } Logger.Info($"RPC Set Name For Player: {__instance.GetNameWithRole()} => {name}", "SetName"); break; case RpcCalls.SetRole: var role = (RoleTypes)subReader.ReadUInt16(); bool canOverriddenRole = subReader.ReadBoolean(); Logger.Info($"RPC Set Role For Player: {__instance.GetRealName()} => {role} CanOverrideRole: {canOverriddenRole}", "SetRole"); break; case RpcCalls.SendChat: string text = subReader.ReadString(); Logger.Info($"({__instance.FriendCode}|{__instance.GetClient()?.GetHashedPuid()}) {__instance.GetNameWithRole()}: {text}", "ReceiveChat"); ChatCommands.OnReceiveChat(__instance, text, out bool canceled); if (canceled) { subReader.Recycle(); return false; } break; case RpcCalls.SendQuickChat: string quickText = QuickChatNetData.Deserialize(subReader).ToChatText(); Logger.Info($"({__instance.FriendCode}|{__instance.GetClient()?.GetHashedPuid()}) {__instance.GetNameWithRole()}: {quickText}", "ReceiveQuickChat"); ChatCommands.OnReceiveChat(__instance, quickText, out bool quickCanceled); if (quickCanceled) { subReader.Recycle(); return false; } break; case RpcCalls.StartMeeting: PlayerControl p = Utils.GetPlayerById(subReader.ReadByte()); Logger.Info($"{__instance.GetNameWithRole()} => {p?.GetNameWithRole() ?? "null"}", "StartMeeting"); break; case RpcCalls.Pet: Logger.Info($"{__instance.GetNameWithRole()} petted their pet", "RpcHandlerPatch"); break; } if (!__instance.IsHost() && Enum.IsDefined(typeof(CustomRPC), (int)callId) && !TrustedRpc(callId)) { Logger.Warn($"{__instance.Data?.PlayerName}:{callId}({RPC.GetRpcName(callId)}) canceled because it was sent by someone other than the host.", "CustomRPC"); if (!Options.KickOnInvalidRPC.GetBool() || !AmongUsClient.Instance.AmHost || !EAC.ReceiveInvalidRpc(__instance, callId)) { subReader.Recycle(); return false; } AmongUsClient.Instance.KickPlayer(__instance.OwnerId, false); Logger.Warn($"The RPC received from {__instance.Data?.PlayerName} is not trusted, so they were kicked.", "Kick"); Logger.SendInGame(string.Format(GetString("Warning.InvalidRpc"), __instance.Data?.PlayerName), Color.yellow); subReader.Recycle(); return false; } } } finally { subReader.Recycle(); } return true; } public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) { try { Logger.Info($"__instance: {__instance.GetNameWithRole()}, callId: {callId} ({RPC.GetRpcName(callId)})", "RPCHandlerPatch.Postfix"); var rpcType = (CustomRPC)callId; if (AmongUsClient.Instance.AmHost) { switch (callId) { case 70: { Logger.SendInGame(string.Format(GetString("ModMismatch"), __instance.Data?.PlayerName), Color.yellow); break; } case 80: { Logger.SendInGame(string.Format(GetString("ModMismatch"), __instance.Data?.PlayerName), Color.red); break; } case 62 when GameStates.IsInTask && Main.IntroDestroyed && !ExileController.Instance && !AntiBlackout.SkipTasks: { PhantomRolePatch.CheckTrigger(__instance); break; } } } if (AmongUsClient.Instance.AmHost && !TrustedRpc(callId)) return; switch (rpcType) { case CustomRPC.AntiBlackout: { if (Options.EndWhenPlayerBug.GetBool()) { Logger.Fatal($"{__instance?.Data?.PlayerName}({__instance?.PlayerId}): {reader.ReadString()} - Error, terminate the game according to settings", "Anti-blackout"); Main.OverrideWelcomeMsg = string.Format(GetString("RpcAntiBlackOutNotifyInLobby"), __instance?.Data?.PlayerName, GetString("EndWhenPlayerBug")); LateTask.New(() => { Logger.SendInGame(string.Format(GetString("RpcAntiBlackOutEndGame"), __instance?.Data?.PlayerName) /*, true*/, Color.red); }, 3f, "Anti-Black Msg SendInGame"); LateTask.New(() => { CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Error); GameManager.Instance.LogicFlow.CheckEndCriteria(); RPC.ForceEndGame(CustomWinner.Error); }, 5.5f, "Anti-Black End Game"); } else if (GameStates.IsOnlineGame) { Logger.Fatal($"{__instance?.Data?.PlayerName}({__instance?.PlayerId}): Change Role Setting Postfix - Error, continue the game according to settings", "Anti-blackout"); LateTask.New(() => { Logger.SendInGame(string.Format(GetString("RpcAntiBlackOutIgnored"), __instance?.Data?.PlayerName) /*, true*/, Color.red); }, 3f, "Anti-Black Msg SendInGame"); } break; } case CustomRPC.VersionCheck: { try { Version version = Version.Parse(reader.ReadString()); string tag = reader.ReadString(); string forkId = reader.ReadString(); try { if (!Main.PlayerVersion.ContainsKey(__instance.PlayerId)) RPC.RpcVersionCheck(); } catch { } Main.PlayerVersion[__instance.PlayerId] = new(version, tag, forkId); } catch (Exception e) { Logger.Warn($"{__instance?.Data?.PlayerName}({__instance?.PlayerId}): error during version check", "RpcVersionCheck"); Utils.ThrowException(e); if (GameStates.InGame || AmongUsClient.Instance.IsGameStarted) break; LateTask.New(() => { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.RequestRetryVersionCheck, SendOption.Reliable, __instance?.OwnerId ?? -1); AmongUsClient.Instance.FinishRpcImmediately(writer); }, 1f, "Retry Version Check Task"); } break; } case CustomRPC.RequestRetryVersionCheck: { RPC.RpcVersionCheck(); break; } case CustomRPC.SyncCustomSettings: { if (AmongUsClient.Instance.AmHost) break; while (reader.ReadPackedInt32() is var id and >= 0) { OptionItem option = OptionItem.FastOptions[id]; var data = reader.ReadPackedInt32(); option.SetValue(data, false, false); try { if (option.Id == OptionItem.PresetId && option.CurrentValue != 19) option.SetValue(19, false, false); } catch (Exception e) { Utils.ThrowException(e); } } OptionSaver.Save(); //Main.Instance.StartCoroutine(OptionShower.GetText()); break; } case CustomRPC.SetDeathReason: { RPC.GetDeathReason(reader); break; } case CustomRPC.EndGame: { RPC.EndGame(reader); break; } case CustomRPC.PlaySound: { byte playerID = reader.ReadByte(); var sound = (Sounds)reader.ReadByte(); RPC.PlaySound(playerID, sound); break; } case CustomRPC.ShowPopUp: { string msg = reader.ReadString(); HudManager.Instance.ShowPopUp(msg); break; } case CustomRPC.SyncAllClientRealNames: { Main.AllClientRealNames.Clear(); int num2 = reader.ReadPackedInt32(); for (var i = 0; i < num2; i++) Main.AllClientRealNames.TryAdd(reader.ReadInt32(), reader.ReadString()); break; } case CustomRPC.SetCustomRole: { byte customRoleTargetId = reader.ReadByte(); var role = (CustomRoles)reader.ReadPackedInt32(); bool replaceAllAddons = reader.ReadBoolean(); RPC.SetCustomRole(customRoleTargetId, role, replaceAllAddons); break; } case CustomRPC.SyncAbilityUseLimit: { PlayerControl pc = Utils.GetPlayerById(reader.ReadByte()); pc.SetAbilityUseLimit(reader.ReadSingle(), false); break; } case CustomRPC.RemoveAbilityUseLimit: { Main.AbilityUseLimit.Remove(reader.ReadByte()); break; } case CustomRPC.RemoveSubRole: { byte id = reader.ReadByte(); if (reader.ReadPackedInt32() == 2) Main.PlayerStates[id].SubRoles.Clear(); else Main.PlayerStates[id].RemoveSubRole((CustomRoles)reader.ReadPackedInt32()); break; } case CustomRPC.Arrow: { if (reader.ReadBoolean()) TargetArrow.ReceiveRPC(reader); else LocateArrow.ReceiveRPC(reader); break; } case CustomRPC.SyncAbilityCD: { switch (reader.ReadPackedInt32()) { case 1: { byte id = reader.ReadByte(); int cd = reader.ReadPackedInt32(); long ts = Utils.TimeStamp; Main.AbilityCD[id] = (ts, cd); break; } case 2: { Main.AbilityCD.Clear(); break; } case 3: { byte id = reader.ReadByte(); Main.AbilityCD.Remove(id); break; } } break; } case CustomRPC.SyncRoleData: { byte id = reader.ReadByte(); RoleBase r = Main.PlayerStates[id].Role; r.GetType().GetMethod("ReceiveRPC")?.Invoke(r, [reader]); break; } case CustomRPC.FixModdedClientCNO: { var cno = reader.ReadNetObject(); bool active = reader.ReadBoolean(); if (cno) { cno.transform.FindChild("Names").FindChild("NameText_TMP").gameObject.SetActive(active); cno.Collider.enabled = false; } break; } case CustomRPC.SyncGeneralOptions: { byte id = reader.ReadByte(); var role = (CustomRoles)reader.ReadPackedInt32(); bool dead = reader.ReadBoolean(); var dr = (PlayerState.DeathReason)reader.ReadPackedInt32(); if (Main.PlayerStates.TryGetValue(id, out PlayerState state)) { state.MainRole = role; state.IsDead = dead; state.deathReason = dr; } float kcd = reader.ReadSingle(); float speed = reader.ReadSingle(); Main.AllPlayerKillCooldown[id] = kcd; Main.AllPlayerSpeed[id] = speed; break; } case CustomRPC.NotificationPopper: { Loop.Times(reader.ReadPackedInt32(), _ => NotificationPopperPatch.AddSettingsChangeMessage(OptionItem.FastOptions[reader.ReadPackedInt32()])); break; } case CustomRPC.RequestCommandProcessing: { if (!AmongUsClient.Instance.AmHost) break; string commandKey = reader.ReadString(); string text = reader.ReadString(); if (!__instance || !__instance.IsModdedClient()) { Logger.Error("Player is null or not a modded client", "RequestCommandProcessingFromHost"); break; } Command command = Command.AllCommands.Find(x => x.Key == commandKey); if (command == null) { Logger.Error($"Invalid Command {commandKey}.", "RequestCommandProcessingFromHost"); break; } if (!command.CanUseCommand(__instance)) break; command.Action(__instance, text, text.Split(' ')); Logger.Info($"Invoke Command: {command.Action.Method.Name} ({__instance.Data?.PlayerName}, {text})", "RequestCommandProcessing"); break; } case CustomRPC.SyncPostman: { byte id = reader.ReadByte(); byte target = reader.ReadByte(); bool isFinished = reader.ReadBoolean(); if (Main.PlayerStates[id].Role is not Postman pm) break; pm.Target = target; pm.IsFinished = isFinished; break; } case CustomRPC.SyncChangeling: { if (Main.PlayerStates[reader.ReadByte()].Role is not Changeling changeling) break; changeling.CurrentRole = (CustomRoles)reader.ReadPackedInt32(); break; } case CustomRPC.SyncSentry: { byte id = reader.ReadByte(); if (Main.PlayerStates[id].Role is not Roles.Sentry sentry) break; sentry.MonitoredRoom = Utils.GetPlayerById(id).GetPlainShipRoom(); break; } case CustomRPC.SyncOverheat: { ((Overheat)Main.PlayerStates[reader.ReadByte()].Role).Temperature = reader.ReadPackedInt32(); break; } case CustomRPC.SyncIntrovert: { Introvert.ReceiveRPC(reader); break; } case CustomRPC.SyncAllergic: { Allergic.ReceiveRPC(reader); break; } case CustomRPC.SyncAsthmatic: { Asthmatic.ReceiveRPC(reader); break; } case CustomRPC.SetBountyTarget: { byte bountyId = reader.ReadByte(); byte targetId = reader.ReadByte(); (Main.PlayerStates[bountyId].Role as BountyHunter)?.ReceiveRPCTarget(targetId); break; } case CustomRPC.SyncBargainer: { Bargainer.ReceiveRPC(reader); break; } case CustomRPC.SetKillOrSpell: { Witch.ReceiveRPC(reader, false); break; } case CustomRPC.SetCpTasksDone: { Crewpostor.RecieveRPC(reader); break; } case CustomRPC.SetLibrarianMode: { Librarian.ReceiveRPC(reader); break; } case CustomRPC.SyncSoulHunter: { SoulHunter.ReceiveRPC(reader); break; } case CustomRPC.SetDousedPlayer: { byte arsonistId = reader.ReadByte(); byte dousedId = reader.ReadByte(); bool doused = reader.ReadBoolean(); Arsonist.IsDoused[(arsonistId, dousedId)] = doused; break; } case CustomRPC.SetPlaguedPlayer: { PlagueBearer.ReceiveRPC(reader); break; } case CustomRPC.SyncHeadHunter: { HeadHunter.ReceiveRPC(reader); break; } case CustomRPC.SyncDamoclesTimer: { Damocles.ReceiveRPC(reader); break; } case CustomRPC.SetDrawPlayer: { byte revolutionistId = reader.ReadByte(); byte drawId = reader.ReadByte(); bool drawed = reader.ReadBoolean(); Revolutionist.IsDraw[(revolutionistId, drawId)] = drawed; break; } case CustomRPC.SetRevealedPlayer: { byte investigatorId = reader.ReadByte(); byte revealId = reader.ReadByte(); bool revealed = reader.ReadBoolean(); Investigator.IsRevealed ??= []; Investigator.IsRevealed[(investigatorId, revealId)] = revealed; break; } case CustomRPC.SetNameColorData: { NameColorManager.ReceiveRPC(reader); break; } case CustomRPC.RpcPassBomb: { Agitator.ReceiveRPC(reader); break; } case CustomRPC.DoSpell: { Witch.ReceiveRPC(reader, true); break; } case CustomRPC.SetBanditStealLimit: { Bandit.ReceiveRPC(reader); break; } case CustomRPC.SniperSync: { byte id = reader.ReadByte(); (Main.PlayerStates[id].Role as Sniper)?.ReceiveRPC(reader); } break; case CustomRPC.SyncSpy: { Spy.ReceiveRPC(reader); break; } case CustomRPC.SetAlchemistPotion: { Alchemist.ReceiveRPCData(reader); break; } case CustomRPC.SetRicochetTarget: { Ricochet.ReceiveRPCSyncTarget(reader); break; } case CustomRPC.SyncRabbit: { Rabbit.ReceiveRPCStatic(reader); break; } case CustomRPC.SyncYinYanger: { YinYanger.ReceiveRPC(reader); break; } case CustomRPC.SetTetherTarget: { Tether.ReceiveRPCSyncTarget(reader); break; } case CustomRPC.SetHitmanTarget: { byte hitmanId = reader.ReadByte(); byte targetId = reader.ReadByte(); (Main.PlayerStates[hitmanId].Role as Hitman)?.ReceiveRPC(targetId); } break; case CustomRPC.SetWeaponMasterMode: { WeaponMaster.ReceiveRPC(reader); break; } case CustomRPC.SyncGlitchTimers: { Glitch.ReceiveRPCSyncTimers(reader); break; } case CustomRPC.DruidAddTrigger: { Druid.ReceiveRPCAddTrigger(reader); break; } case CustomRPC.SetHackerLimit: { Hacker.ReceiveRPC(reader); break; } case CustomRPC.SetLoversPlayers: { Main.LoversPlayers.Clear(); int count = reader.ReadInt32(); for (var i = 0; i < count; i++) Main.LoversPlayers.Add(Utils.GetPlayerById(reader.ReadByte())); } break; case CustomRPC.SetExecutionerTarget: { Executioner.ReceiveRPC(reader, true); break; } case CustomRPC.RemoveExecutionerTarget: { Executioner.ReceiveRPC(reader, false); break; } case CustomRPC.SetLawyerTarget: { Lawyer.ReceiveRPC(reader, true); break; } case CustomRPC.RemoveLawyerTarget: { Lawyer.ReceiveRPC(reader, false); break; } case CustomRPC.SendFireworkerState: { byte id = reader.ReadByte(); int count = reader.ReadInt32(); int state = reader.ReadInt32(); var newState = (Fireworker.FireworkerState)state; (Main.PlayerStates[id].Role as Fireworker)?.ReceiveRPC(count, newState); } break; case CustomRPC.SyncMycologist: { Mycologist.ReceiveRPC(reader); break; } case CustomRPC.SyncBubble: { Bubble.ReceiveRPC(reader); break; } case CustomRPC.SetCurrentDousingTarget: { byte arsonistId = reader.ReadByte(); byte dousingTargetId = reader.ReadByte(); if (PlayerControl.LocalPlayer.PlayerId == arsonistId) Arsonist.CurrentDousingTarget = dousingTargetId; break; } case CustomRPC.SetCurrentDrawTarget: { byte revolutionistId = reader.ReadByte(); byte doTargetId = reader.ReadByte(); if (PlayerControl.LocalPlayer.PlayerId == revolutionistId) Revolutionist.CurrentDrawTarget = doTargetId; break; } case CustomRPC.SetCurrentRevealTarget: { byte investigatorId = reader.ReadByte(); byte doTargetId = reader.ReadByte(); if (PlayerControl.LocalPlayer.PlayerId == investigatorId) Investigator.CurrentRevealTarget = doTargetId; break; } case CustomRPC.SetEvilTrackerTarget: { EvilTracker.ReceiveRPC(reader); break; } case CustomRPC.SyncInfection: { Infection.ReceiveRPC(reader); break; } case CustomRPC.PenguinSync: { byte id = reader.ReadByte(); int operate = reader.ReadInt32(); if (operate == 1) { byte victim = reader.ReadByte(); (Main.PlayerStates[id].Role as Penguin)?.ReceiveRPCVictim(victim); } else { bool reset = reader.ReadBoolean(); (Main.PlayerStates[id].Role as Penguin)?.ReceiveRPCTimer(reset); } break; } case CustomRPC.SetRealKiller: { byte targetId = reader.ReadByte(); byte killerId = reader.ReadByte(); RPC.SetRealKiller(targetId, killerId); break; } case CustomRPC.SyncLobbyTimer: { GameStartManagerPatch.TimerStartTS = long.Parse(reader.ReadString()); break; } case CustomRPC.SetDemonHealth: { Demon.ReceiveRPC(reader); break; } case CustomRPC.SetPelicanEtenNum: { Pelican.ReceiveRPC(reader); break; } case CustomRPC.SetDoomsayerProgress: { Doomsayer.ReceiveRPC(reader); break; } case CustomRPC.VigilanteKill: { Vigilante.ReceiveRPC(reader); break; } case CustomRPC.PlayCustomSound: { CustomSoundsManager.ReceiveRPC(reader); break; } case CustomRPC.SetGhostPlayer: { Roles.Lightning.ReceiveRPC(reader); break; } case CustomRPC.SetStalkerKillCount: { Stalker.ReceiveRPC(reader); break; } case CustomRPC.SetGreedyOe: { byte id = reader.ReadByte(); bool isOdd = reader.ReadBoolean(); (Main.PlayerStates[id].Role as Greedy)?.ReceiveRPC(isOdd); } break; case CustomRPC.SetCollectorVotes: { Collector.ReceiveRPC(reader); break; } case CustomRPC.SetQuickShooterShotLimit: { QuickShooter.ReceiveRPC(reader); break; } case CustomRPC.GuessKill: { GuessManager.RpcClientGuess(Utils.GetPlayerById(reader.ReadByte())); break; } case CustomRPC.SetMarkedPlayer: { Ninja.ReceiveRPC(reader); break; } case CustomRPC.SyncChronomancer: { byte id = reader.ReadByte(); bool isRampaging = reader.ReadBoolean(); int chargePercent = reader.ReadInt32(); (Main.PlayerStates[id].Role as Chronomancer)?.ReceiveRPC(isRampaging, chargePercent); break; } case CustomRPC.SetMedicalerProtectList: { Medic.ReceiveRPCForProtectList(reader); break; } case CustomRPC.SyncPsychicRedList: { Psychic.ReceiveRPC(reader); break; } case CustomRPC.SetKillTimer: { float time = reader.ReadSingle(); PlayerControl.LocalPlayer.SetKillTimer(time); break; } case CustomRPC.SyncAllPlayerNames: { Main.AllPlayerNames = []; int num = reader.ReadInt32(); for (var i = 0; i < num; i++) Main.AllPlayerNames.TryAdd(reader.ReadByte(), reader.ReadString()); break; } case CustomRPC.SyncMafiosoData: { byte id = reader.ReadByte(); (Main.PlayerStates[id].Role as Mafioso)?.ReceiveRPC(reader); } break; case CustomRPC.SyncMafiosoPistolCD: { byte id = reader.ReadByte(); (Main.PlayerStates[id].Role as Mafioso)?.ReceiveRPCSyncPistolCD(reader); } break; case CustomRPC.SyncNameNotify: { NameNotifyManager.ReceiveRPC(reader); break; } case CustomRPC.Judge: { Prosecutor.ReceiveRPC(reader, __instance); break; } case CustomRPC.MeetingKill: { Councillor.ReceiveRPC(reader, __instance); break; } case CustomRPC.Guess: { GuessManager.ReceiveRPC(reader, __instance); break; } case CustomRPC.NemesisRevenge: { Nemesis.ReceiveRPC(reader, __instance); break; } case CustomRPC.SyncFollowerTargetAndTimes: { Follower.ReceiveRPC(reader); break; } case CustomRPC.SyncRomanticTarget: { Romantic.ReceiveRPC(reader); break; } case CustomRPC.SyncVengefulRomanticTarget: { VengefulRomantic.ReceiveRPC(reader); break; } case CustomRPC.SetConsigliere: { byte id = reader.ReadByte(); byte targetId = reader.ReadByte(); (Main.PlayerStates[id].Role as Consigliere)?.ReceiveRPC(targetId); } break; case CustomRPC.SyncHookshot: { Hookshot.ReceiveRPC(reader); break; } case CustomRPC.AddTornado: { Tornado.ReceiveRPCAddTornado(reader); break; } case CustomRPC.SetDoppelgangerStealLimit: { Doppelganger.ReceiveRPC(reader); break; } case CustomRPC.SyncStressedTimer: { Stressed.ReceiveRPC(reader); break; } case CustomRPC.KillFlash: { Utils.FlashColor(new(1f, 0f, 0f, 0.3f)); if (Constants.ShouldPlaySfx()) RPC.PlaySound(PlayerControl.LocalPlayer.PlayerId, Sounds.KillSound); break; } case CustomRPC.SetCleanserCleanLimit: { Cleanser.ReceiveRPC(reader); break; } case CustomRPC.SetJailorTarget: { Jailor.ReceiveRPC(reader); break; } case CustomRPC.SetSwapperVotes: { Swapper.ReceiveRPC(reader, __instance); break; } case CustomRPC.SetTrackerTarget: { Scout.ReceiveRPC(reader); break; } case CustomRPC.RoomRushDataSync: { RoomRush.ReceiveRPC(reader); break; } case CustomRPC.FFASync: { switch (reader.ReadPackedInt32()) { case 1: int roundTime = reader.ReadPackedInt32(); FreeForAll.RoundTime = roundTime; break; case 2: FreeForAll.KillCount[reader.ReadByte()] = reader.ReadPackedInt32(); break; } break; } case CustomRPC.QuizSync: { Quiz.AllowKills = reader.ReadBoolean(); break; } case CustomRPC.HNSSync: { CustomHnS.ReceiveRPC(reader); break; } case CustomRPC.HotPotatoSync: { HotPotato.ReceiveRPC(reader); break; } case CustomRPC.SoloPVPSync: { switch (reader.ReadPackedInt32()) { case 1: SoloPVP.RoundTimer = Stopwatch.StartNew(); break; case 2: SoloPVP.PlayerScore[reader.ReadByte()] = reader.ReadPackedInt32(); break; } break; } case CustomRPC.CTFSync: { CaptureTheFlag.ReceiveRPC(reader); break; } case CustomRPC.KOTZSync: { KingOfTheZones.ReceiveRPC(reader); break; } case CustomRPC.SpeedrunSync: { if (reader.ReadPackedInt32() == 1) Speedrun.CanKill = []; else Speedrun.CanKill.Add(reader.ReadByte()); break; } case CustomRPC.NaturalDisastersSync: { NaturalDisasters.SurvivalTimes[reader.ReadByte()] = reader.ReadPackedInt32(); break; } case CustomRPC.TMGSync: { TheMindGame.ReceiveRPC(reader); break; } case CustomRPC.BedWarsSync: { BedWars.ReceiveRPC(reader); break; } case CustomRPC.DeathraceSync: { if (Deathrace.Data.TryGetValue(reader.ReadByte(), out Deathrace.PlayerData data)) data.Lap = reader.ReadPackedInt32(); break; } case CustomRPC.DoomTagSync: { DoomTag.ReceiveRPC(reader); break; } case CustomRPC.InspectorCommand: { Inspector.ReceiveRPC(reader); break; } case CustomRPC.Invisibility: { int num = reader.ReadPackedInt32(); switch (num) { case 1: __instance.MakeInvisible(); Main.Invisible.Add(__instance.PlayerId); break; case 0: __instance.MakeVisible(); Main.Invisible.Remove(__instance.PlayerId); break; case 11: Main.Invisible.Add(__instance.PlayerId); break; case 10: Main.Invisible.Remove(__instance.PlayerId); break; } break; } case CustomRPC.ResetAbilityCooldown: { PlayerControl.LocalPlayer.Data.Role.SetCooldown(); break; } case CustomRPC.SyncCamouflage: { Camouflage.IsCamouflage = reader.ReadBoolean(); break; } case CustomRPC.SetChatVisible: { HudManager.Instance.Chat.SetVisible(reader.ReadBoolean()); HudManager.Instance.Chat.HideBanButton(); break; } case CustomRPC.Exclusionary: { if (reader.ReadBoolean()) { foreach (PlayerControl player in Main.CachedAlivePlayerControls()) { if (player.AmOwner) continue; player.SetPet(""); player.invisibilityAlpha = 0f; player.cosmetics.SetPhantomRoleAlpha(player.invisibilityAlpha); player.shouldAppearInvisible = true; player.Visible = false; } } else { foreach (PlayerControl player in Main.CachedAlivePlayerControls()) { if (player.AmOwner) continue; if (Options.UsePets.GetBool()) PetsHelper.SetPet(player, PetsHelper.GetPetId()); player.shouldAppearInvisible = false; player.Visible = true; player.invisibilityAlpha = 1f; player.cosmetics.SetPhantomRoleAlpha(player.invisibilityAlpha); player.shouldAppearInvisible = false; player.Visible = !player.inVent; } } break; } case CustomRPC.Deadlined: { Deadlined.ReceiveRPC(reader); break; } case CustomRPC.Blessed: { Blessed.ReceiveRPC(reader); break; } case CustomRPC.Necronomicon: { if (!Main.PlayerStates.TryGetValue(reader.ReadByte(), out PlayerState state) || state.Role is not CovenBase covenRole) break; covenRole.HasNecronomicon = true; break; } case CustomRPC.Stained: { byte id = reader.ReadByte(); Stained.VioletNameList ??= []; Stained.VioletNameList.Add(id); LateTask.New(() => Stained.VioletNameList.Remove(id), 3f); break; } case CustomRPC.Entombed: { Entombed.ReceiveRPC(reader); break; } case CustomRPC.Committed: { Committed.ReceiveRPC(reader); break; } case CustomRPC.Haunter: { Haunter.ReceiveRPC(reader); break; } case CustomRPC.CTA: { CustomTeamManager.ReceiveRPC(reader); break; } } } catch (Exception e) { Utils.ThrowException(e); } } } internal static class RPC { const int MaxBytesPerRPC = 1000; // Credit: https://github.com/music-discussion/TownOfHost-TheOtherRoles/blob/main/Modules/RPC.cs public static void SyncCustomSettingsRPC(int targetId = -1) { if (targetId != -1) { ClientData client = Utils.GetClientById(targetId); if (client == null || !client.Character || !Main.PlayerVersion.ContainsKey(client.Character.PlayerId)) return; } if (!AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls.Count <= 1) return; MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncCustomSettings, SendOption.Reliable, targetId); var idx = 0; while (idx < OptionItem.AllOptions.Count) { var option = OptionItem.AllOptions[idx++]; writer.WritePacked(option.Id); writer.WritePacked(option.GetValue()); if (writer.Position >= MaxBytesPerRPC && idx < OptionItem.AllOptions.Count) { writer.WritePacked(-1); // Stop indicator AmongUsClient.Instance.FinishRpcImmediately(writer); writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncCustomSettings, SendOption.Reliable, targetId); } } writer.WritePacked(-1); // Stop indicator AmongUsClient.Instance.FinishRpcImmediately(writer); } public static void PlaySoundRPC(byte playerID, Sounds sound) { if (AmongUsClient.Instance.AmHost) PlaySound(playerID, sound); long now = Utils.TimeStamp; if (now == CustomSoundsManager.LastSoundRPCTS) return; CustomSoundsManager.LastSoundRPCTS = now; SendOption sendOption = SendOption.Reliable; if (GameStates.CurrentServerType == GameStates.ServerType.Vanilla) sendOption = SendOption.None; MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.PlaySound, sendOption); writer.Write(playerID); writer.Write((byte)sound); AmongUsClient.Instance.FinishRpcImmediately(writer); } public static void SyncAllPlayerNames() { if (!AmongUsClient.Instance.AmHost) return; MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncAllPlayerNames, SendOption.Reliable); writer.Write(Main.AllPlayerNames.Count); foreach (KeyValuePair name in Main.AllPlayerNames) { writer.Write(name.Key); writer.Write(name.Value); } AmongUsClient.Instance.FinishRpcImmediately(writer); } public static void ShowPopUp(this PlayerControl pc, string msg) { if (!AmongUsClient.Instance.AmHost) return; MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ShowPopUp, SendOption.Reliable, pc.OwnerId); writer.Write(msg); AmongUsClient.Instance.FinishRpcImmediately(writer); } public static void SyncAllClientRealNames() { if (!AmongUsClient.Instance.AmHost) return; MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncAllClientRealNames, SendOption.Reliable); writer.WritePacked(Main.AllClientRealNames.Count); foreach (KeyValuePair name in Main.AllClientRealNames) { writer.Write(name.Key); writer.Write(name.Value); } AmongUsClient.Instance.FinishRpcImmediately(writer); } public static void RpcVersionCheck() { Main.Instance.StartCoroutine(VersionCheck()); return; static IEnumerator VersionCheck() { yield return null; while (PlayerControl.LocalPlayer == null) yield return null; if (AmongUsClient.Instance.AmHost) Utils.SendRPC(CustomRPC.VersionCheck, Main.PluginVersion, $"{ThisAssembly.Git.Commit}({ThisAssembly.Git.Branch})", Main.ForkId); else { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VersionCheck, SendOption.Reliable, AmongUsClient.Instance.HostId); writer.Write(Main.PluginVersion); writer.Write($"{ThisAssembly.Git.Commit}({ThisAssembly.Git.Branch})"); writer.Write(Main.ForkId); AmongUsClient.Instance.FinishRpcImmediately(writer); } Main.PlayerVersion[PlayerControl.LocalPlayer.PlayerId] = new(Main.PluginVersion, $"{ThisAssembly.Git.Commit}({ThisAssembly.Git.Branch})", Main.ForkId); } } public static void SendDeathReason(byte playerId, PlayerState.DeathReason deathReason, bool isDead) { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetDeathReason, SendOption.Reliable); writer.Write(playerId); writer.Write((int)deathReason); writer.Write(isDead); AmongUsClient.Instance.FinishRpcImmediately(writer); } public static void GetDeathReason(MessageReader reader) { byte playerId = reader.ReadByte(); var deathReason = (PlayerState.DeathReason)reader.ReadInt32(); PlayerState state = Main.PlayerStates[playerId]; state.deathReason = deathReason; state.IsDead = reader.ReadBoolean(); Main.ForceRebuildCachesPlayerControls(); } public static void ForceEndGame(CustomWinner win) { if (!ShipStatus.Instance) return; try { CustomWinnerHolder.ResetAndSetWinner(win); } catch { } if (AmongUsClient.Instance.AmHost) { ShipStatus.Instance.enabled = false; try { GameManager.Instance.LogicFlow.CheckEndCriteria(); } catch { } try { GameManager.Instance.ShouldCheckForGameEnd = false; MessageWriter msg = AmongUsClient.Instance.StartEndGame(); msg.Write((byte)5); msg.Write(false); AmongUsClient.Instance.FinishEndGame(msg); } catch { } } } public static void EndGame(MessageReader reader) { try { CustomWinnerHolder.ReadFrom(reader); } catch (Exception ex) { Utils.ThrowException(ex); } } public static void PlaySound(byte playerID, Sounds sound) { if (PlayerControl.LocalPlayer.PlayerId == playerID) { switch (sound) { case Sounds.KillSound: SoundManager.Instance.PlaySound(PlayerControl.LocalPlayer.KillSfx, false); break; case Sounds.TaskComplete when HudManager.InstanceExists: SoundManager.Instance.PlaySound(HudManager.Instance.TaskCompleteSound, false); break; case Sounds.TaskUpdateSound when HudManager.InstanceExists: SoundManager.Instance.PlaySound(HudManager.Instance.TaskUpdateSound, false); break; case Sounds.SabotageSound: SoundManager.Instance.PlaySound(ShipStatus.Instance.SabotageSound, false); break; case Sounds.ImpDiscovered: SoundManager.Instance.PlaySound(PlayerControl.LocalPlayer.MyPhysics.ImpostorDiscoveredSound, false); break; case Sounds.LobbyTimerPopUp when HudManager.InstanceExists: SoundManager.Instance.PlaySound(HudManager.Instance.LobbyTimerExtensionUI.lobbyTimerPopUpSound, false); break; case Sounds.SpawnSound: SoundManager.Instance.PlaySound(LobbyBehaviour.Instance.SpawnSound, false); break; case Sounds.ImpTransform: SoundManager.Instance.PlaySound(DestroyableSingleton.Instance.HnSOtherImpostorTransformSfx, false, 0.8f); break; case Sounds.YeehawTransform: SoundManager.Instance.PlaySound(DestroyableSingleton.Instance.HnSOtherYeehawSfx, false, 0.8f); break; } } } public static void SetCustomRole(byte targetId, CustomRoles role, bool replaceAllAddons = false) { Main.PlayerStates.TryAdd(targetId, new(targetId)); if (role < CustomRoles.NotAssigned) Main.PlayerStates[targetId].SetMainRole(role); else Main.PlayerStates[targetId].SetSubRole(role, replaceAllAddons); HudManager.Instance.SetHudActive(true); if (PlayerControl.LocalPlayer.PlayerId == targetId) RemoveDisableDevicesPatch.UpdateDisableDevices(); } public static void SyncLoversPlayers() { if (!AmongUsClient.Instance.AmHost) return; MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetLoversPlayers, SendOption.Reliable); writer.Write(Main.LoversPlayers.Count); foreach (PlayerControl lp in Main.LoversPlayers) writer.Write(lp.PlayerId); AmongUsClient.Instance.FinishRpcImmediately(writer); } public static string GetRpcName(byte callId) { string rpcName; if ((rpcName = Enum.GetName(typeof(RpcCalls), callId)) != null) { } else if ((rpcName = Enum.GetName(typeof(CustomRPC), callId)) != null) { } else rpcName = callId.ToString(); return rpcName; } public static void SetCurrentDousingTarget(byte arsonistId, byte targetId) { if (PlayerControl.LocalPlayer.PlayerId == arsonistId) Arsonist.CurrentDousingTarget = targetId; else { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetCurrentDousingTarget, SendOption.Reliable); writer.Write(arsonistId); writer.Write(targetId); AmongUsClient.Instance.FinishRpcImmediately(writer); } } public static void SetCurrentDrawTarget(byte revolutionistId, byte targetId) { if (PlayerControl.LocalPlayer.PlayerId == revolutionistId) Revolutionist.CurrentDrawTarget = targetId; else { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetCurrentDrawTarget, SendOption.Reliable); writer.Write(revolutionistId); writer.Write(targetId); AmongUsClient.Instance.FinishRpcImmediately(writer); } } public static void SetCurrentRevealTarget(byte investigatorId, byte targetId) { if (PlayerControl.LocalPlayer.PlayerId == investigatorId) Investigator.CurrentRevealTarget = targetId; else { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetCurrentRevealTarget, SendOption.Reliable); writer.Write(investigatorId); writer.Write(targetId); AmongUsClient.Instance.FinishRpcImmediately(writer); } } public static void ResetCurrentDousingTarget(byte arsonistId) { SetCurrentDousingTarget(arsonistId, 255); } public static void ResetCurrentDrawTarget(byte revolutionistId) { SetCurrentDrawTarget(revolutionistId, 255); } public static void ResetCurrentRevealTarget(byte investigatorId) { SetCurrentRevealTarget(investigatorId, 255); } public static void SetRealKiller(byte targetId, byte killerId) { PlayerState state = Main.PlayerStates[targetId]; state.RealKiller.TimeStamp = DateTime.Now; state.RealKiller.ID = killerId; if (!AmongUsClient.Instance.AmHost) return; MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetRealKiller, SendOption.Reliable); writer.Write(targetId); writer.Write(killerId); AmongUsClient.Instance.FinishRpcImmediately(writer); } } [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] internal static class PlayerPhysicsRPCHandlerPatch { public static bool Prefix(PlayerPhysics __instance, byte callId, MessageReader reader) { bool host = __instance.IsHost(); if (!host && EAC.PlayerPhysicsRpcCheck(__instance, callId, reader)) return false; PlayerControl player = __instance.myPlayer; if (!player) { Logger.Warn("Received Physics RPC without a player", "PlayerPhysics_ReceiveRPC"); return false; } return true; } }