Files
EndlessHostRoles/Patches/PlayerControlPatch.cs
T
2026-09-24 20:26:16 +02:00

2685 lines
112 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using AmongUs.GameOptions;
using EHR.Gamemodes;
using EHR.Modules;
using EHR.Patches;
using EHR.Roles;
using HarmonyLib;
using Hazel;
using InnerNet;
using UnityEngine;
using static EHR.Translator;
using static EHR.Utils;
using Tree = EHR.Roles.Tree;
namespace EHR;
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckProtect))]
internal static class CheckProtectPatch
{
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target)
{
if (!AmongUsClient.Instance.AmHost || AntiBlackout.SkipTasks || target.Data.IsDead || !target.IsAlive()) return false;
Logger.Info($"CheckProtect: {__instance.GetNameWithRole()} => {target.GetNameWithRole()}", "CheckProtect");
if (__instance.HasAbilityCD())
{
if (!__instance.AmOwner) __instance.Notify(GetString("AbilityOnCooldown"));
else Main.Instance.StartCoroutine(ExternalRpcPetPatch.FlashCooldownTimer());
return true;
}
if (__instance.Is(CustomRoles.EvilSpirit))
{
if (target.Is(CustomRoles.Spiritcaller))
Spiritcaller.ProtectSpiritcaller();
else
Spiritcaller.HauntPlayer(target);
__instance.AddAbilityCD(Spiritcaller.SpiritAbilityCooldown.GetInt());
return true;
}
if (GhostRolesManager.AssignedGhostRoles.TryGetValue(__instance.PlayerId, out (CustomRoles Role, IGhostRole Instance) ghostRole))
{
__instance.AddAbilityCD(ghostRole.Instance.Cooldown);
ghostRole.Instance.OnProtect(__instance, target);
}
return true;
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcMurderPlayer))] // never used by the mod
internal static class RpcMurderPlayerPatch
{
public static bool Prefix()
{
return false;
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdCheckMurder))] // Modded
internal static class CmdCheckMurderPatch
{
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target)
{
if (AmongUsClient.Instance.AmHost)
__instance.CheckMurder(target);
else
{
MessageWriter messageWriter = AmongUsClient.Instance.StartRpcImmediately(__instance.NetId, (byte)RpcCalls.CheckMurder, SendOption.Reliable);
messageWriter.WriteNetObject(target);
AmongUsClient.Instance.FinishRpcImmediately(messageWriter);
}
return false;
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckMurder))] // Vanilla
internal static class CheckMurderPatch
{
public static readonly Dictionary<byte, float> TimeSinceLastKill = [];
public static void Update(byte id)
{
if (TimeSinceLastKill.ContainsKey(id))
{
TimeSinceLastKill[id] += Time.deltaTime;
if (15f < TimeSinceLastKill[id]) TimeSinceLastKill.Remove(id);
}
}
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target)
{
try
{
if (!AmongUsClient.Instance.AmHost) return false;
PlayerControl killer = __instance;
AFKDetector.SetNotAFK(killer.PlayerId);
Logger.Info($"{killer.GetNameWithRole()} => {target.GetNameWithRole()}", "CheckMurder");
if (killer.Data.IsDead)
{
Logger.Info($"Killer {killer.GetNameWithRole()} is dead, kill canceled", "CheckMurder");
return false;
}
if (AntiBlackout.SkipTasks)
{
Logger.Info("CheckMurder while AntiBlackOut protection is in progress, kill canceled", "CheckMurder");
return false;
}
if (!Main.IntroDestroyed)
{
Logger.Info("CheckMurder while intro is not destroyed, kill canceled", "CheckMurder");
return false;
}
if (killer.Is(CustomRoles.Dizzy))
{
Vector2 pos = killer.Pos();
float range = killer.GetKillDistance();
PlayerControl[] allInRange = FastVector2.GetPlayersInRange(pos, range, x => x.PlayerId != killer.PlayerId).ToArray();
if (allInRange.Length > 1)
{
PlayerControl tempTarget = target;
target = allInRange.RandomElement();
Logger.Info($"Target was {tempTarget.GetNameWithRole()}, new target is {target.GetNameWithRole()}", "Dizzy");
}
}
if (target.Is(CustomRoles.Detour) && target.GetAbilityUseLimit() >= 1f)
{
target.RpcRemoveAbilityUse();
PlayerControl tempTarget = target;
FastVector2.TryGetClosestPlayerTo(tempTarget, out target, x => x.PlayerId != killer.PlayerId);
Logger.Info($"Target was {tempTarget.GetNameWithRole()}, new target is {target.GetNameWithRole()}", "Detour");
if (tempTarget.AmOwner)
{
Detour.TotalRedirections++;
if (Detour.TotalRedirections >= 3) Achievements.Type.CantTouchThis.CompleteAfterGameEnd();
}
}
if (Spirit.TryGetSwapTarget(target, out PlayerControl newTarget))
{
Logger.Info($"Target was {target.GetNameWithRole()}, new target is {newTarget.GetNameWithRole()}", "Spirit");
target = newTarget;
}
if (!target.Data
|| target.inVent
|| target.inMovingPlat
|| target.MyPhysics.Animations.IsPlayingEnterVentAnimation()
|| target.MyPhysics.Animations.IsPlayingAnyLadderAnimation()
|| target.onLadder)
{
Logger.Info("The target is in a state where they cannot be killed, kill canceled.", "CheckMurder");
return false;
}
if (target.Data.IsDead)
{
Logger.Info("Target is already dead, kill canceled", "CheckMurder");
return false;
}
if (MeetingHud.Instance)
{
Logger.Info("Kill during meeting, canceled", "CheckMurder");
return false;
}
float minTime = CalculatePingDelay();
// No value is stored in TimeSinceLastKill || Stored time is greater than or equal to minTime => Allow kill
// ↓ If not allowed
if (TimeSinceLastKill.TryGetValue(killer.PlayerId, out float time) && time < minTime)
{
Logger.Info($"Last kill was too shortly before, canceled - time: {time}, minTime: {minTime}", "CheckMurder");
return false;
}
TimeSinceLastKill[killer.PlayerId] = 0f;
if (!killer.Is(CustomRoles.Focused))
{
if (target.Is(CustomRoles.Diseased))
{
if (!Main.KilledDiseased.TryAdd(killer.PlayerId, 1))
Main.KilledDiseased[killer.PlayerId] += 1;
}
if (target.Is(CustomRoles.Antidote))
{
if (!Main.KilledAntidote.TryAdd(killer.PlayerId, 1))
Main.KilledAntidote[killer.PlayerId] += 1;
}
}
killer.ResetKillCooldown(false);
if (killer.PlayerId != target.PlayerId && !killer.CanUseKillButton())
{
Logger.Info(killer.GetNameWithRole() + " cannot use their kill button, the kill was blocked", "CheckMurder");
return false;
}
switch (Options.CurrentGameMode)
{
case CustomGameMode.SoloPVP:
SoloPVP.OnPlayerAttack(killer, target);
return false;
case CustomGameMode.FFA:
FreeForAll.OnPlayerAttack(killer, target);
return false;
case CustomGameMode.HotPotato:
(byte holderID, byte lastHolderID) = HotPotato.GetState();
if (HotPotato.CanPassViaKillButton && holderID == killer.PlayerId && (lastHolderID != target.PlayerId || Main.AllAlivePlayerControlsCount <= 2))
HotPotato.FixedUpdatePatch.PassHotPotato(target, false);
return false;
case CustomGameMode.Mingle:
case CustomGameMode.TheMindGame:
case CustomGameMode.StopAndGo:
case CustomGameMode.RoomRush:
case CustomGameMode.NaturalDisasters:
case CustomGameMode.Speedrun when !Speedrun.OnCheckMurder(killer, target):
return false;
case CustomGameMode.Quiz:
if (Quiz.AllowKills) killer.Kill(target);
return false;
case CustomGameMode.Speedrun:
killer.Kill(target);
return false;
case CustomGameMode.HideAndSeek:
CustomHnS.OnCheckMurder(killer, target);
return false;
case CustomGameMode.CaptureTheFlag:
CaptureTheFlag.OnCheckMurder(killer, target);
return false;
case CustomGameMode.KingOfTheZones:
KingOfTheZones.OnCheckMurder(killer, target);
return false;
case CustomGameMode.BedWars:
BedWars.OnCheckMurder(killer, target);
return false;
case CustomGameMode.Deathrace:
Deathrace.OnCheckMurder(killer, target);
return false;
case CustomGameMode.Snowdown:
Snowdown.OnCheckMurder(killer, target);
return false;
case CustomGameMode.DoomTag:
DoomTag.OnCheckMurder(killer, target);
return false;
}
PlagueBearer.CheckAndSpreadInfection(killer, target);
PlagueBearer.CheckAndSpreadInfection(target, killer);
Deadlined.SetDone(killer);
if (ToiletMaster.OnAnyoneCheckMurderStart(killer, target)) return false;
if (Dad.OnAnyoneCheckMurderStart(target)) return false;
if (Quarry.OnAnyoneCheckMurderStart(killer, target)) return false;
Simon.RemoveTarget(killer, Simon.Instruction.Kill);
if (Mastermind.ManipulatedPlayers.ContainsKey(killer.PlayerId))
return Mastermind.ForceKillForManipulatedPlayer(killer, target);
if (target.Is(CustomRoles.Spy) && !Spy.OnKillAttempt(killer, target)) return false;
if (!Starspawn.CheckInteraction(killer, target)) return false;
if (Penguin.IsVictim(killer)) return false;
Sniper.TryGetSniper(target.PlayerId, ref killer);
if (killer != __instance) Logger.Info($"Real Killer: {killer.GetNameWithRole()}", "CheckMurder");
if (Pelican.IsEaten(target.PlayerId)) return false;
if (killer.IsRoleBlocked())
{
killer.Notify(BlockedAction.Kill.GetBlockNotify());
return false;
}
if (Pursuer.OnClientMurder(killer)) return false;
if (Main.PlayerStates[target.PlayerId].Role is Veteran veteran && Veteran.VeteranAlertActivatesOnNonKillingInteractions.GetBool() && !veteran.OnCheckMurderAsTarget(killer, target))
return false;
Seamstress.OnAnyoneCheckMurder(killer, target);
Empress.OnInteraction(target);
if (killer.PlayerId != target.PlayerId)
{
switch (killer.Is(CustomRoles.Bloodlust))
{
case false when !CheckMurder():
return false;
case true when killer.GetCustomRole().GetDYRole() is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Phantom:
if (killer.CheckDoubleTrigger(target, () =>
{
if (CheckMurder()) killer.RpcCheckAndMurder(target);
}))
killer.RpcCheckAndMurder(target);
return false;
}
bool CheckMurder() => target.Is(CustomRoles.Fragile) || Ambusher.FragilePlayers.ContainsKey(target.PlayerId) || Main.PlayerStates[killer.PlayerId].Role.OnCheckMurder(killer, target);
}
if (!killer.RpcCheckAndMurder(target, true)) return false;
if (killer.Is(CustomRoles.Unlucky))
{
if (IRandom.Instance.Next(0, 100) < Options.UnluckyKillSuicideChance.GetInt())
{
killer.Suicide();
return false;
}
}
if (killer.Is(CustomRoles.Mare)) killer.ResetKillCooldown();
if (!DoubleTrigger.FirstTriggerTimer.ContainsKey(killer.PlayerId) && killer.Is(CustomRoles.Swift) && !target.Is(CustomRoles.Pestilence))
{
target.Suicide(PlayerState.DeathReason.Kill, killer);
killer.SetKillCooldown();
Main.PlayerStates[killer.PlayerId].Role.OnMurder(killer, target);
RPC.PlaySoundRPC(killer.PlayerId, Sounds.KillSound);
return false;
}
if (killer.Is(CustomRoles.Magnet) && !target.Is(CustomRoles.Pestilence))
{
target.TP(killer);
LateTask.New(() => killer.RpcCheckAndMurder(target), 0.1f, log: false);
return false;
}
//==Kill processing==
__instance.Kill(target);
//===================
}
catch (Exception e) { ThrowException(e); }
return false;
}
public static bool RpcCheckAndMurder(PlayerControl killer, PlayerControl target, bool check = false)
{
if (!AmongUsClient.Instance.AmHost) return false;
if (!target) target = killer;
if (CustomTeamManager.AreInSameCustomTeam(killer.PlayerId, target.PlayerId) && !CustomTeamManager.IsSettingEnabledForPlayerTeam(killer.PlayerId, CTAOption.KillEachOther))
{
Notify("SameCTATeam");
return false;
}
if (AFKDetector.ShieldedPlayers.Contains(target.PlayerId))
{
Notify("AFKShielded");
return false;
}
if (killer.Is(CustomRoles.Jackal) && target.Is(CustomRoles.Sidekick) && !Options.JackalCanKillSidekick.GetBool() ||
killer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Jackal) && !Options.SidekickCanKillJackal.GetBool() ||
killer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Sidekick) && !Options.SidekickCanKillSidekick.GetBool())
{
Notify("JackalSidekick");
return false;
}
if (!Virus.ContagiousPlayersCanKillEachOther.GetBool() && target.Is(CustomRoles.Contagious) && killer.Is(CustomRoles.Contagious))
{
Notify("ContagiousPlayers");
return false;
}
if (killer.IsMadmate() && target.Is(CustomRoleTypes.Impostor) && !Options.MadmateCanKillImp.GetBool())
{
Notify("MadmateKillImpostor");
return false;
}
if (killer.Is(CustomRoleTypes.Impostor) && target.IsMadmate() && !Options.ImpCanKillMadmate.GetBool())
{
Notify("ImpostorKillMadmate");
return false;
}
if (killer.Is(CustomRoleTypes.Coven) && target.Is(CustomRoleTypes.Coven))
{
Notify("CovenKillEachOther");
return false;
}
if (killer.Is(CustomRoleTypes.Coven) && target.Is(CustomRoles.Entranced) && !Siren.CovenCanKillEntranced.GetBool())
{
Notify("CovenKillEntranced");
return false;
}
if (killer.Is(CustomRoles.Entranced) && target.Is(CustomRoleTypes.Coven) && !Siren.EntrancedCanKillCoven.GetBool())
{
Notify("EntrancedKillCoven");
return false;
}
if (!Tired.CheckMurderLimit(killer.PlayerId))
{
Notify("TiredReachedMaxKillsPerRound");
return false;
}
if (!Summoner.OnAnyoneCheckMurder(killer, target))
{
Notify("SummonerCantKillCoven");
return false;
}
if (Romantic.PartnerId == target.PlayerId && Romantic.IsPartnerProtected ||
Medic.OnAnyoneCheckMurder(killer, target) ||
Randomizer.IsShielded(target) ||
Aid.ShieldedPlayers.ContainsKey(target.PlayerId) ||
Blessed.ShieldActive != null && Blessed.ShieldActive.Contains(target.PlayerId) ||
Benefactor.ShieldedPlayers.Contains(target.PlayerId) ||
Gaslighter.IsShielded(target) ||
!Farmer.OnAnyoneCheckMurder(target) ||
!PotionMaster.OnAnyoneCheckMurder(target) ||
!Grappler.OnAnyoneCheckMurder(target) ||
!Adventurer.OnAnyoneCheckMurder(target) ||
!Sentinel.OnAnyoneCheckMurder(killer) ||
!ToiletMaster.OnAnyoneCheckMurder(killer, target))
{
Notify("SomeSortOfProtection");
return false;
}
if (!Gardener.OnAnyoneCheckMurder(killer, target))
{
Notify("GardenerPlantNearby");
return false;
}
if (!Socialite.OnAnyoneCheckMurder(killer, target))
{
Notify("SocialiteTarget");
return false;
}
if (Penguin.IsVictim(killer))
{
Notify("PenguinVictimKill");
return false;
}
if (Mathematician.State.ProtectedPlayerId == target.PlayerId)
{
Notify("MathematicianProtected");
return false;
}
if (killer.Is(CustomRoles.Renegade) && target.Is(CustomRoleTypes.Impostor))
{
Notify("RenegadeKillImpostor");
return false;
}
if (!Bodyguard.OnAnyoneCheckMurder(killer, target))
{
Notify("BodyguardProtected");
return false;
}
if (Echo.On)
{
foreach (Echo echo in Echo.Instances)
{
if (echo.EchoPC.shapeshiftTargetPlayerId == target.PlayerId)
{
echo.OnTargetCheckMurder(killer, target);
return false;
}
}
}
if (GhostRolesManager.AssignedGhostRoles.Values.Any(x => x.Instance is GA ga && ga.ProtectionList.Contains(target.PlayerId)))
{
Notify("GAGuarded");
if (killer.AmOwner)
Achievements.Type.IForgotThisRoleExists.CompleteAfterGameEnd();
return false;
}
if (SoulHunter.IsSoulHunterTarget(killer.PlayerId) && target.Is(CustomRoles.SoulHunter))
{
Notify("SoulHunterTargetNotifyNoKill");
return false;
}
if (killer.IsRoleBlocked())
{
killer.Notify(BlockedAction.Kill.GetBlockNotify());
return false;
}
if (killer.Is(CustomRoles.Traitor) && target.Is(CustomRoleTypes.Impostor))
{
Notify("TraitorKillImpostor");
return false;
}
if (Jackal.On && Jackal.ResetKillCooldownWhenSbGetKilled.GetBool() && !killer.Is(CustomRoles.Sidekick) && !target.Is(CustomRoles.Sidekick) && !killer.Is(CustomRoles.Jackal) && !target.Is(CustomRoles.Jackal) && !GameStates.IsMeeting)
Jackal.AfterPlayerDiedTask(killer);
if (target.Is(CustomRoles.Lucky))
{
if (IRandom.Instance.Next(0, 100) < Options.LuckyProbability.GetInt())
{
killer.SetKillCooldown(15f);
return false;
}
}
if (Crusader.ForCrusade.Contains(target.PlayerId))
{
foreach (PlayerControl player in Main.EnumeratePlayerControls())
{
if (player.Is(CustomRoles.Crusader) && player.IsAlive())
{
switch (killer.Is(CustomRoles.Pestilence))
{
case false when !killer.Is(CustomRoles.KillingMachine):
player.Kill(killer);
Crusader.ForCrusade.Remove(target.PlayerId);
killer.RpcGuardAndKill(target);
return false;
case true:
killer.Kill(player);
Crusader.ForCrusade.Remove(target.PlayerId);
target.RpcGuardAndKill(killer);
Main.PlayerStates[player.PlayerId].deathReason = PlayerState.DeathReason.Destroyed;
return false;
}
}
}
}
switch (target.GetCustomRole())
{
case CustomRoles.Medic when !check:
Medic.IsDead(target);
break;
case CustomRoles.Spiritcaller when Spiritcaller.Protected:
killer.RpcGuardAndKill(target);
Notify("SomeSortOfProtection");
return false;
}
if (MeetingStates.FirstMeeting && Main.ShieldPlayer == target.FriendCode && !string.IsNullOrWhiteSpace(target.FriendCode))
{
Main.ShieldPlayer = string.Empty;
killer.SetKillCooldown(15f);
killer.Notify(GetString("TriedToKillLastGameFirstKill"), 10f);
return false;
}
if (Options.MadmateSpawnMode.GetInt() == 1 && Main.MadmateNum < CustomRoles.Madmate.GetCount() && target.CanBeMadmate())
{
Main.MadmateNum++;
target.RpcSetCustomRole(CustomRoles.Madmate);
ExtendedPlayerControl.RpcSetCustomRole(target.PlayerId, CustomRoles.Madmate);
var sender = CustomRpcSender.Create("RpcCheckAndMurder - Madmate", SendOption.Reliable);
CustomRpcSenderExtensions.Notify(ref sender, target, CustomRoles.Madmate.ColoredTextByRole(GetString("BecomeMadmateCuzMadmateMode")));
sender.RpcGuardAndKill(target, killer);
sender.RpcGuardAndKill(target, target);
sender.RpcGuardAndKill(killer, target);
sender.SetKillCooldown(killer);
sender.SendMessage();
Logger.Info($"Add-on assigned: {target?.Data?.PlayerName} = {target.GetCustomRole()} + {CustomRoles.Madmate}", $"Assign {CustomRoles.Madmate}");
return false;
}
if (!Main.PlayerStates[target.PlayerId].Role.OnCheckMurderAsTarget(killer, target))
{
Notify("SomeSortOfProtection");
return false;
}
if (killer.Is(CustomRoles.Rookie) && MeetingStates.FirstMeeting)
{
Notify("RookieKillRoundOne");
return false;
}
if (!check) killer.Kill(target);
if (killer.Is(CustomRoles.Doppelganger)) Doppelganger.OnCheckMurderEnd(killer, target);
return true;
void Notify(string message) => killer.Notify(ColorString(Color.yellow, GetString("CheckMurderFail") + GetString(message)), 12f);
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.MurderPlayer))]
internal static class MurderPlayerPatch
{
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target, [HarmonyArgument(1)] MurderResultFlags resultFlags, ref bool __state)
{
if (GameStates.IsLobby || AntiBlackout.SkipTasks || !GameStates.InGame || GameStates.IsMeeting || ExileController.Instance) return false;
bool protectedByClient = resultFlags.HasFlag(MurderResultFlags.DecisionByHost) && target.IsProtected();
bool protectedByHost = resultFlags.HasFlag(MurderResultFlags.FailedProtected);
bool failed = resultFlags.HasFlag(MurderResultFlags.FailedError);
__state = !protectedByClient && !protectedByHost && !failed;
if (__state && (!Main.IntroDestroyed || IntroCutsceneDestroyPatch.PreventKill)) return false;
Logger.Info($"{__instance.GetNameWithRole()} => {target.GetNameWithRole()} - {nameof(protectedByClient)}: {protectedByClient}, {nameof(protectedByHost)}: {protectedByHost}, {nameof(failed)}: {failed}", "MurderPlayer");
RandomSpawn.CustomNetworkTransformHandleRpcPatch.HasSpawned.Add(__instance.PlayerId);
return true;
}
public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target, bool __state)
{
if (!__instance || !target || __instance.PlayerId >= 254 || target.PlayerId >= 254 || !__state) return;
if (target.AmOwner) RemoveDisableDevicesPatch.UpdateDisableDevices();
if (!target.Data.IsDead || !AmongUsClient.Instance.AmHost) return;
if (Butcher.ButcherDeadPlayerList.Contains(target.PlayerId) || __instance.AmOwner && __instance.Is(CustomRoles.SoulCollector)) return;
PlayerControl killer = __instance; // Alternative variable
try
{
Infection.OnAnyMurder();
Summoner.OnAnyoneMurder(killer, target);
// Replacement process when the actual killer and killer are different
if (Sniper.TryGetSniper(target.PlayerId, ref killer)) Main.PlayerStates[target.PlayerId].deathReason = PlayerState.DeathReason.Sniped;
if (killer.Is(CustomRoles.Sniper))
{
if (!Options.UsePets.GetBool() || Options.UsePhantomBasis.GetBool())
killer.RpcResetAbilityCooldown();
else
killer.AddAbilityCD(Options.DefaultShapeshiftCooldown.GetInt());
}
}
catch (Exception e) { ThrowException(e); }
if (killer != __instance) Logger.Info($"Real Killer = {killer.GetNameWithRole()}", "MurderPlayer");
if (Main.PlayerStates[target.PlayerId].deathReason == PlayerState.DeathReason.etc)
{
// If the cause of death is not specified, it is determined as a normal kill.
Main.PlayerStates[target.PlayerId].deathReason = PlayerState.DeathReason.Kill;
}
// Let’s see if Youtuber was stabbed first
if (Main.FirstDied == string.Empty && target.Is(CustomRoles.Youtuber))
{
CustomSoundsManager.RPCPlayCustomSoundAll("Congrats");
CustomWinnerHolder.SetWinnerOrAdditonalWinner(CustomWinner.Youtuber);
CustomWinnerHolder.WinnerIds.Add(target.PlayerId);
}
try
{
Postman.CheckAndResetTargets(target, true);
if (target.Is(CustomRoles.Beartrap) && killer != target) killer.BeartrapKilled(target);
if (target.Is(CustomRoles.Stained)) Stained.OnDeath(target, killer);
if (Witness.On)
{
Witness.AllKillers ??= [];
Witness.AllKillers.Add(killer.PlayerId);
PlayerControl killer1 = killer;
LateTask.New(() => Witness.AllKillers.Remove(killer1.PlayerId), Options.WitnessTime.GetInt(), log: false);
}
killer.AddKillTimerToDict();
switch (target.GetCustomRole())
{
case CustomRoles.Lightning when killer != target:
Roles.Lightning.MurderPlayer(killer, target);
break;
case CustomRoles.Bane when killer != target:
Bane.OnKilled(killer);
break;
case CustomRoles.Markseeker:
Markseeker.OnDeath(target);
break;
}
}
catch (Exception e) { ThrowException(e); }
try { Main.PlayerStates[killer.PlayerId].Role.OnMurder(killer, target); }
catch (Exception e) { ThrowException(e); }
try
{
Tired.OnMurder(killer.PlayerId);
Chef.SpitOutFood(killer);
EvilTracker.OnAnyoneMurder(killer, target);
Berserker.OnAnyoneMurder(killer);
Empress.OnAnyoneMurder(killer);
if (Options.CurrentGameMode == CustomGameMode.Speedrun)
Speedrun.ResetTimer(killer);
if (killer.Is(CustomRoles.Stealer) && killer.PlayerId != target.PlayerId)
killer.Notify(string.Format(GetString("StealerGetVote"), ((Main.EnumeratePlayerControls().Count(x => x.GetRealKiller()?.PlayerId == killer.PlayerId) + 1) * Options.VotesPerKill.GetFloat()).ToString("0.0#####")));
if (killer.Is(CustomRoles.Pickpocket) && killer.PlayerId != target.PlayerId)
killer.Notify(string.Format(GetString("PickpocketGetVote"), ((Main.EnumeratePlayerControls().Count(x => x.GetRealKiller()?.PlayerId == killer.PlayerId) + 1) * Pickpocket.VotesPerKill.GetFloat()).ToString("0.0#####")));
if (killer.Is(CustomRoles.Deadlined)) Deadlined.SetDone(killer);
if (target.Is(CustomRoles.Avenger))
{
PlayerControl[] pcList = Main.EnumerateAlivePlayerControls().Where(x => x.PlayerId != target.PlayerId).ToArray();
PlayerControl rp = pcList.RandomElement();
if (!rp.Is(CustomRoles.Pestilence)) rp.Suicide(PlayerState.DeathReason.Revenge, target);
}
if (target.Is(CustomRoles.Bait))
{
var realKiller = target.GetRealKiller();
if (realKiller) killer = realKiller;
if (killer.AmOwner && Main.PlayerStates.TryGetValue(killer.PlayerId, out var ks) && ks.GetKillCount() <= 1)
Achievements.Type.OhNo.CompleteAfterGameEnd();
if (target != killer && !killer.Is(CustomRoles.KillingMachine) && !killer.Is(CustomRoles.Focused) && (killer.PlayerId != target.PlayerId || target.GetRealKiller()?.GetCustomRole() is CustomRoles.Swooper or CustomRoles.Wraith || !killer.Is(CustomRoles.Oblivious) || !Options.ObliviousBaitImmune.GetBool()))
{
killer.RPCPlayCustomSound("Congrats");
target.RPCPlayCustomSound("Congrats");
float delay;
if (Options.BaitDelayMax.GetFloat() < Options.BaitDelayMin.GetFloat())
delay = 0f;
else
delay = IRandom.Instance.Next((int)Options.BaitDelayMin.GetFloat(), (int)Options.BaitDelayMax.GetFloat() + 1);
delay = Math.Max(delay, 0.15f);
if (delay > 0.15f && Options.BaitDelayNotify.GetBool()) killer.Notify(CustomRoles.Bait.ColoredTextByRole(string.Format(GetString("KillBaitNotify"), (int)delay)), delay);
Logger.Info($"{killer.GetNameWithRole()} killed Bait => {target.GetNameWithRole()}", "MurderPlayer");
LateTask.New(() =>
{
if (GameStates.IsInTask)
{
if (!Options.ReportBaitAtAllCost.GetBool())
killer.CmdReportDeadBody(target.Data);
else
killer.NoCheckStartMeeting(target.Data, true);
}
}, delay, "Bait Self Report");
}
}
}
catch (Exception e) { ThrowException(e); }
try { AfterPlayerDeathTasks(target); }
catch (Exception e) { ThrowException(e); }
Main.PlayerStates[target.PlayerId].SetDead();
target.SetRealKiller(killer, true);
CountAlivePlayers(true);
if (Options.CurrentGameMode == CustomGameMode.Standard)
{
__instance.MarkDirtySettings();
target.MarkDirtySettings();
Main.Instance.StartCoroutine(NotifyEveryoneAsync(false));
}
Statistics.OnMurder(killer, target);
}
}
// Triggered when the shapeshifter selects a target
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckShapeshift))]
internal static class CheckShapeshiftPatch
{
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target, [HarmonyArgument(1)] bool shouldAnimate)
{
if (!AmongUsClient.Instance.AmHost) return true;
if (ShapeshiftPatch.ProcessShapeshift(__instance, target))
{
bool animated = shouldAnimate
&& !Options.DisableShapeshiftAnimations.GetBool()
&& !Options.DisableAllShapeshiftAnimations.GetBool()
&& !__instance.GetCustomRole().IsNoAnimationShifter();
__instance.RpcShapeshift(target, animated);
}
return false;
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdCheckShapeshift))]
internal static class CmdCheckShapeshiftPatch
{
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target, [HarmonyArgument(1)] bool shouldAnimate)
{
return CheckShapeshiftPatch.Prefix(__instance, target, shouldAnimate);
}
}
// Triggered when the egg animation starts playing
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Shapeshift))]
internal static class ShapeshiftPatch
{
public static bool ProcessShapeshift(PlayerControl shapeshifter, PlayerControl target)
{
if (MeetingHud.Instance && MeetingHud.Instance.state == MeetingHud.MeetingStates.Results) return true;
bool meetingSS = Options.UseMeetingShapeshift.GetBool() && GameStates.IsMeeting;
if (!Main.ProcessShapeshifts && !meetingSS || shapeshifter.PlayerId >= 254) return true;
if (AntiBlackout.SkipTasks)
{
Logger.Info("Shapeshift while AntiBlackOut protection is in progress, shapeshift canceled", "Shapeshift");
return false;
}
if (!shapeshifter || !target) return true;
Logger.Info($"{shapeshifter.GetNameWithRole()} => {target.GetNameWithRole()}", "Shapeshift");
if (AmongUsClient.Instance.AmHost && meetingSS)
{
if (MeetingHud.Instance.state is MeetingHud.MeetingStates.Discussion or MeetingHud.MeetingStates.Voted or MeetingHud.MeetingStates.NotVoted)
Main.PlayerStates[shapeshifter.PlayerId].Role.OnMeetingShapeshift(shapeshifter, target);
shapeshifter.RpcRejectShapeshift();
return false;
}
bool shapeshifting = shapeshifter.PlayerId != target.PlayerId;
CustomRoles role = shapeshifter.GetCustomRole();
if (AmongUsClient.Instance.AmHost && shapeshifting)
{
if (!Rhapsode.CheckAbilityUse(shapeshifter) || Stasis.IsTimeFrozen || TimeMaster.Rewinding || IntroCutsceneDestroyPatch.PreventKill) return false;
if (shapeshifter.Is(CustomRoles.Trainee) && MeetingStates.FirstMeeting)
{
shapeshifter.Notify(GetString("TraineeNotify"));
shapeshifter.RpcRejectShapeshift();
return false;
}
}
Main.CheckShapeshift[shapeshifter.PlayerId] = shapeshifting;
Main.ShapeshiftTarget[shapeshifter.PlayerId] = target.PlayerId;
if (!AmongUsClient.Instance.AmHost) return true;
if (!shapeshifting)
{
Camouflage.RpcSetSkin(shapeshifter);
}
else
{
PlagueBearer.CheckAndSpreadInfection(shapeshifter, target);
PlagueBearer.CheckAndSpreadInfection(target, shapeshifter);
}
if (Main.PlayerStates[target.PlayerId].Role is Veteran veteran && Veteran.VeteranAlertActivatesOnNonKillingInteractions.GetBool())
veteran.OnCheckMurderAsTarget(shapeshifter, target);
var isSSneeded = shapeshifter.IsAlive();
if (!Pelican.IsEaten(shapeshifter.PlayerId) && !GameStates.IsVoting && isSSneeded)
isSSneeded = Main.PlayerStates[shapeshifter.PlayerId].Role.OnShapeshift(shapeshifter, target, shapeshifting);
bool forceCancel = role.ForceCancelShapeshift() || !shapeshifter.IsAlive();
if (Changeling.ChangedRole.TryGetValue(shapeshifter.PlayerId, out bool changed) && changed && shapeshifter.GetRoleTypes() != RoleTypes.Shapeshifter)
{
forceCancel = true;
isSSneeded = false;
}
bool shouldCancel = Options.DisableShapeshiftAnimations.GetBool();
bool shouldAlwaysCancel = shouldCancel && Options.DisableAllShapeshiftAnimations.GetBool();
bool doSSwithoutAnim = isSSneeded && shouldAlwaysCancel;
doSSwithoutAnim |= isSSneeded && role.IsNoAnimationShifter();
isSSneeded &= !shouldAlwaysCancel;
forceCancel |= shouldAlwaysCancel;
isSSneeded &= !doSSwithoutAnim;
// Forced rewriting in case the name cannot be corrected due to the timing of unshifting being off.
if (!shapeshifting && !shapeshifter.Is(CustomRoles.Glitch) && isSSneeded)
LateTask.New(() => Main.Instance.StartCoroutine(NotifyEveryoneAsync()), 1.2f, "ShapeShiftNotify");
if (!(shapeshifting && doSSwithoutAnim) && !isSSneeded && !Swapster.FirstSwapTarget.ContainsKey(shapeshifter.PlayerId) && !Transporter.FirstSwapTarget.ContainsKey(shapeshifter.PlayerId) && !Transposer.FirstSwapTarget.ContainsKey(shapeshifter.PlayerId))
LateTask.New(shapeshifter.RpcResetAbilityCooldown, 0.01f, log: false);
if (!isSSneeded)
{
if (!doSSwithoutAnim) Main.CheckShapeshift[shapeshifter.PlayerId] = false;
shapeshifter.RpcRejectShapeshift();
NotifyRoles(SpecifySeer: shapeshifter, SpecifyTarget: shapeshifter);
}
if (doSSwithoutAnim)
{
shapeshifter.RpcShapeshift(target, false);
return false;
}
bool animated = isSSneeded || !shouldCancel && !forceCancel || !shapeshifting && !shouldAlwaysCancel;
Statistics.OnShapeshift(shapeshifter, shapeshifting, animated);
return animated;
}
// Tasks that should run when someone performs a shapeshift (with the egg animation) should be here.
public static void Prefix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target, [HarmonyArgument(1)] bool shouldAnimate)
{
if (!Main.ProcessShapeshifts || !GameStates.IsInTask || !__instance || !target) return;
if (AmongUsClient.Instance.AmHost && shouldAnimate)
{
LateTask.New(() =>
{
if (!__instance || !target) return;
if (ReportDeadBodyPatch.MeetingStarted || GameStates.IsMeeting) return;
__instance.RpcSetName(Main.AllPlayerNames[target.PlayerId]);
NotifyRoles(SpecifyTarget: __instance, NoCache: true);
}, 1.2f, "ShapeshiftPatch - Force Name");
}
bool shapeshifting = __instance.PlayerId != target.PlayerId;
foreach (PlayerControl pc in Main.CachedAlivePlayerControls())
{
PlainShipRoom plainShipRoom = pc.GetPlainShipRoom();
string room = GetString(plainShipRoom ? plainShipRoom.RoomId.ToString() : "Outside");
if (pc.Is(CustomRoles.Shiftguard)) pc.Notify($"[<#00ffa5>{room}</color>] {GetString(shapeshifting ? "ShiftguardNotifySS" : "ShiftguardNotifyUnshift")}");
switch (Main.PlayerStates[pc.PlayerId].Role)
{
case Adventurer av when shapeshifting:
Adventurer.OnAnyoneShapeshiftLoop(av, __instance);
break;
case Roles.Sentry st:
st.OnAnyoneShapeshiftLoop(__instance, target);
break;
}
}
}
public static void Postfix(PlayerControl __instance)
{
// Set CNO name visible for modded clients after shapeshift
if (__instance.PlayerId >= 254)
__instance.transform.Find("Names").Find("NameText_TMP").gameObject.SetActive(true);
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcShapeshift))]
internal static class RpcShapeshiftPatch
{
public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target, [HarmonyArgument(1)] bool shouldAnimate)
{
Main.CheckShapeshift[__instance.PlayerId] = __instance.PlayerId != target.PlayerId;
Main.ShapeshiftIsAnimated[__instance.PlayerId] = shouldAnimate;
if (__instance.Is(CustomRoles.Morphling) && __instance.IsNonHostModdedClient())
SendRPC(CustomRPC.SyncRoleData, __instance.PlayerId, __instance.PlayerId, Main.CheckShapeshift[__instance.PlayerId]);
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.ReportDeadBody))]
internal static class ReportDeadBodyPatch
{
public static HashSet<byte> AlreadyReportedBodies = [];
public static readonly Dictionary<byte, bool> CanReport = [];
public static readonly Dictionary<byte, List<NetworkedPlayerInfo>> WaitReport = [];
public static bool MeetingStarted;
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] NetworkedPlayerInfo target)
{
if (GameStates.IsMeeting || MeetingStarted) return false;
if (Options.DisableMeeting.GetBool()) return false;
if (Options.CurrentGameMode != CustomGameMode.Standard) return false;
if (Options.DisableReportWhenCC.GetBool() && Camouflage.IsCamouflage) return false;
if (target && AlreadyReportedBodies.Contains(target.PlayerId)) return false;
if (AmongUsClient.Instance.AmHost && (!CanReport[__instance.PlayerId] || __instance.IsRoleBlocked()))
{
WaitReport[__instance.PlayerId].Add(target);
Logger.Warn($"{__instance.GetNameWithRole()}: Reporting is currently prohibited, so we will wait until it becomes possible.", "ReportDeadBody");
return false;
}
Logger.Info($"{__instance.GetNameWithRole()} => {target?.Object?.GetNameWithRole() ?? "null"}", "ReportDeadBody");
try
{
foreach (KeyValuePair<byte, PlayerState> kvp in Main.PlayerStates)
kvp.Value.LastRoom = GetPlayerById(kvp.Key)?.GetPlainShipRoom();
}
catch (Exception e) { ThrowException(e); }
if (!AmongUsClient.Instance.AmHost) return true;
try
{
// If the caller is dead, this process will cancel the meeting, so stop here.
if (__instance.Data.IsDead || !__instance.IsAlive()) return false;
//=============================================
// Next, check whether this meeting is allowed
//=============================================
if (!target && Main.NumEmergencyMeetingsUsed.TryGetValue(__instance.PlayerId, out int used))
{
if (used >= Main.RealOptionsData.GetInt(Int32OptionNames.NumEmergencyMeetings))
{
Notify("NoMoreEmergencyMeetingsLeft");
return false;
}
Main.NumEmergencyMeetingsUsed[__instance.PlayerId]++;
}
PlayerControl killer = target?.Object?.GetRealKiller();
CustomRoles? killerRole = killer?.GetCustomRole();
if (Main.PlayerStates.Values.Any(x => x.Role is Tremor { IsDoom: true })) return false;
if (!target)
{
if (IsAnySabotageActive())
{
Notify("CannotCallEmergencyMeetingWhileSabotage");
return false;
}
if (__instance.Is(CustomRoles.Jester) && !Jester.JesterCanUseButton.GetBool())
{
Notify("JesterCannotCallEmergencyMeeting");
return false;
}
if (__instance.Is(CustomRoles.Swapper) && !Swapper.CanStartMeeting.GetBool())
{
Notify("SwapperCannotCallEmergencyMeeting");
return false;
}
if (__instance.Is(CustomRoles.Adrenaline) && !Adrenaline.CanCallMeeting(__instance))
{
Notify("AdrenalineCannotCallEmergencyMeeting");
return false;
}
if (SoulHunter.IsSoulHunterTarget(__instance.PlayerId))
{
Notify("SoulHunterTargetNotifyNoMeeting");
return false;
}
if (!Wyrd.CheckPlayerAction(__instance, Wyrd.Action.Button)) return false; // Player dies, no notify needed
}
if (!Frightener.OnAnyoneReportDeadBody(__instance, !target)) return false; // Player dies, no notify needed
if (target)
{
if (Coroner.UnreportablePlayers != null && Coroner.UnreportablePlayers.Contains(target.PlayerId)
|| Vulture.UnreportablePlayers.Contains(target.PlayerId)
|| killer && killer.Is(CustomRoles.Goddess)
|| Main.PlayerStates[target.PlayerId].deathReason == PlayerState.DeathReason.Gambled
|| killerRole == CustomRoles.Scavenger
|| Cleaner.CleanerBodies.Contains(target.PlayerId))
{
Notify("UnreportableBody");
return false;
}
if (!Occultist.OnAnyoneReportDeadBody(target) ||
!Altruist.OnAnyoneCheckReportDeadBody(__instance, target) ||
!TimeMaster.OnAnyoneCheckReportDeadBody(__instance, target))
{
Notify("PlayerWasRevived");
return false;
}
if (!Quarry.OnAnyoneCheckReportDeadBody(__instance))
{
Notify("QuarryTarget");
return false;
}
if (!Librarian.OnAnyoneReport(__instance)) return false; // Player dies, no notify needed
if (!Trapster.OnAnyoneCheckReportDeadBody(__instance, target)) return false; // Player dies, no notify needed
if (!Hypnotist.OnAnyoneReport())
{
if (__instance.AmOwner)
Achievements.Type.Hypnosis.CompleteAfterGameEnd();
Notify("HypnosisNoMeeting");
return false;
}
if (!Summoner.OnAnyoneReport())
{
Notify("SummonedPlayerAlive");
return false;
}
if (!Wyrd.CheckPlayerAction(__instance, Wyrd.Action.Report)) return false; // Player dies, no notify needed
PlayerControl tpc = target.Object;
PlagueBearer.CheckAndSpreadInfection(__instance, tpc);
PlagueBearer.CheckAndSpreadInfection(tpc, __instance);
if (!Main.PlayerStates[__instance.PlayerId].Role.CheckReportDeadBody(__instance, target, killer)) return false;
if (__instance.Is(CustomRoles.Unlucky) && (!tpc || !tpc.Is(CustomRoles.Bait)))
{
if (IRandom.Instance.Next(0, 100) < Options.UnluckyReportSuicideChance.GetInt())
{
__instance.Suicide();
return false;
}
}
if (tpc.Is(CustomRoles.Disregarded))
{
Notify("TargetDisregarded");
return false;
}
if (__instance.Is(CustomRoles.Oblivious))
{
Notify("AmOblivious");
return false;
}
}
if (Options.SyncButtonMode.GetBool() && !target)
{
Logger.Info($"Max buttons: {Options.SyncedButtonCount.GetInt()}, used: {Options.UsedButtonCount}", "ReportDeadBody");
if (Options.SyncedButtonCount.GetFloat() <= Options.UsedButtonCount)
{
Logger.Info("The ship has no more emergency meetings left", "ReportDeadBody");
Notify("ShipHasNoMoreMeetingsLeft");
return false;
}
Options.UsedButtonCount++;
if (Math.Abs(Options.SyncedButtonCount.GetFloat() - Options.UsedButtonCount) < 0.5f)
Logger.Info("This was the last allowed emergency meeting", "ReportDeadBody");
}
}
catch (Exception e) { ThrowException(e); }
AfterReportTasks(__instance, target);
return true;
void Notify(string str) => __instance.Notify(ColorString(Color.yellow, GetString("CheckReportFail") + GetString(str)), 15f);
}
public static void AfterReportTasks(PlayerControl player, NetworkedPlayerInfo target)
{
//=============================================================================================
// Hereinafter, it is confirmed that the meeting is allowed, and the meeting will start.
//=============================================================================================
if (MeetingStarted) return;
MeetingStarted = true;
LateTask.New(() => MeetingStarted = false, 1f, "ResetMeetingStarted");
if (ClientControlGUI.HudHidden)
{
ClientControlGUI.HudHidden = false;
if (HudManager.InstanceExists)
HudManager.Instance.gameObject.SetActive(true);
}
if (!SubmergedCompatibility.IsSubmerged())
{
try
{
Main.PlayerStates.Values.DoIf(x => !x.IsDead, x =>
{
x.IsBlackOut = true;
x.Player.MarkDirtySettings();
});
PlayerGameOptionsSender.SendAllImmediately();
}
catch (Exception e) { ThrowException(e); }
}
LateTask.New(() =>
{
Main.PlayerStates.Values.DoIf(x => x.IsBlackOut, x =>
{
x.IsBlackOut = false;
x.Player.MarkDirtySettings();
});
}, 15f, "RemoveBlackout");
if (Main.GameTimer.IsRunning && !Options.GameTimeLimitRunsDuringMeetings.GetBool())
Main.GameTimer.Stop();
foreach (CustomNetObject cno in CustomNetObject.AllObjects)
{
try
{
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(cno.playerControl.NetId, (byte)RpcCalls.Shapeshift, SendOption.Reliable);
writer.WriteNetObject(cno.playerControl);
writer.Write(false);
AmongUsClient.Instance.FinishRpcImmediately(writer);
}
catch (Exception e) { ThrowException(e); }
}
LateTask.New(() =>
{
for (var index = CustomNetObject.AllObjects.Count - 1; index >= 0; index--)
{
try { CustomNetObject.AllObjects[index]?.OnMeeting(); }
catch (Exception e) { ThrowException(e); }
}
}, 5f, "CNO OnMeeting");
if (HudManager.InstanceExists) HudManager.Instance.SetRolePanelOpen(false);
Asthmatic.RunChecks = false;
Damocles.CountRepairSabotage = false;
Stressed.CountRepairSabotage = false;
Listener.LocalPlayerHeardMessagesThisMeeting = 0;
GameEndChecker.ShouldNotCheck = false;
try
{
if (Options.CurrentGameMode == CustomGameMode.Standard)
{
foreach (byte id in Main.DiedThisRound)
{
PlayerControl receiver = id.GetPlayer();
if (!receiver) continue;
PlayerControl killer = receiver.GetRealKiller();
if (!killer) continue;
LateTask.New(() => SendMessage("\n", receiver.PlayerId, string.Format(GetString("DeathCommand"), killer.PlayerId.ColoredPlayerName(), (killer.Is(CustomRoles.Bloodlust) ? $"{CustomRoles.Bloodlust.ToColoredString()} " : string.Empty) + killer.GetCustomRole().ToColoredString()), importance: MessageImportance.Low), 13f);
}
}
}
catch (Exception e) { ThrowException(e); }
Main.DiedThisRound = [];
try { Main.EnumerateAlivePlayerControls().DoIf(x => x.Is(CustomRoles.Lazy), x =>
{
Lazy.BeforeMeetingPositions ??= [];
Lazy.BeforeMeetingPositions[x.PlayerId] = x.Pos();
}); }
catch (Exception e) { ThrowException(e); }
try
{
if (Lovers.PrivateChat.GetBool() && Main.LoversPlayers.Count > 0)
{
if (GameStates.CurrentServerType != GameStates.ServerType.Vanilla)
ChatManager.ClearChat(Main.CachedAlivePlayerControls().FindAll(x => !Main.LoversPlayers.Exists(l => l.PlayerId == x.PlayerId)));
else
ChatManager.ClearChat();
}
}
catch (Exception e) { ThrowException(e); }
CustomSabotage.Reset();
try
{
if (!target)
{
switch (Main.PlayerStates[player.PlayerId].Role)
{
case Mayor:
Mayor.MayorUsedButtonCount[player.PlayerId]++;
break;
case Rogue rg:
rg.OnButtonPressed();
break;
}
if (QuizMaster.On)
{
QuizMaster.Data.LastPlayerPressedButtonName = player.GetRealName();
QuizMaster.Data.NumEmergencyMeetings++;
}
}
else
{
PlayerControl tpc = target.Object;
if (tpc && !tpc.IsAlive())
{
if (player.Is(CustomRoles.Forensic) && player.PlayerId != target.PlayerId)
Forensic.OnReportDeadBody(player, tpc);
else if (player.Is(CustomRoles.Sleuth) && player.PlayerId != target.PlayerId)
{
string msg = string.Format(GetString("SleuthMsg"), tpc.GetRealName(), tpc.GetDisplayRoleName());
Main.SleuthMsgs[player.PlayerId] = msg;
}
}
if (Virus.InfectedBodies.Contains(target.PlayerId))
Virus.OnKilledBodyReport(player);
if (QuizMaster.On)
{
QuizMaster.Data.LastReporterName = player.GetRealName();
QuizMaster.Data.LastReportedPlayer = (Palette.GetColorName(target.DefaultOutfit.ColorId), tpc);
if (MeetingStates.FirstMeeting) QuizMaster.Data.FirstReportedBodyPlayerName = tpc.GetRealName();
}
if (player.Is(CustomRoles.Looter))
tpc.GetCustomSubRoles().FindAll(x => !player.Is(x) && !x.IsGhostRole() && !x.IsNotAssignableMidGame() && CustomRolesHelper.CheckAddonConflict(x, player)).ForEach(x => player.RpcSetCustomRole(x));
if (player.Is(CustomRoles.Absorber))
{
float uses = tpc.GetAbilityUseLimit();
if (!float.IsNaN(uses) && uses > 0f)
{
if (uses < 1f)
{
player.RpcIncreaseAbilityUseLimitBy(uses);
tpc.SetAbilityUseLimit(0f);
}
else
{
player.RpcIncreaseAbilityUseLimitBy(1f);
tpc.RpcRemoveAbilityUse();
}
}
}
}
if (QuizMaster.On)
{
if (MeetingStates.FirstMeeting) QuizMaster.Data.NumPlayersDeadFirstRound = Main.EnumeratePlayerControls().Count(x => !x.IsAlive() && !x.Is(CustomRoles.GM));
QuizMaster.Data.NumMeetings++;
}
if (Main.IsLoversDead && Lovers.LoverDieConsequence.GetValue() == 1)
{
PlayerControl aliveLover = Main.LoversPlayers.Find(x => x && x.IsAlive());
if (aliveLover)
{
switch (Lovers.LoverSuicideTime.GetValue())
{
case 1:
aliveLover.Suicide(PlayerState.DeathReason.FollowingSuicide);
break;
case 2:
CheckForEndVotingPatch.TryAddAfterMeetingDeathPlayers(PlayerState.DeathReason.FollowingSuicide, aliveLover.PlayerId);
break;
}
}
}
}
catch (Exception e) { ThrowException(e); }
foreach (PlayerControl pc in Main.CachedAllPlayerControls())
{
try
{
if (Main.CheckShapeshift.ContainsKey(pc.PlayerId) && !Doppelganger.DoppelVictim.ContainsKey(pc.PlayerId))
Camouflage.RpcSetSkin(pc, revertToDefault: true);
if (Main.CurrentMap == MapNames.Fungle && (pc.IsMushroomMixupActive() || IsActive(SystemTypes.MushroomMixupSabotage)))
pc.FixMixedUpOutfit();
if (Main.Invisible.Contains(pc.PlayerId))
LateTask.New(() => pc.RpcMakeVisible(), 1f, log: false);
// Fix name when LateTask "Shapeshift Patch - Force Name" was not revert name back before meeting started
if (pc.IsShifted() && Main.ShapeshiftIsAnimated.GetValueOrDefault(pc.PlayerId))
{
Logger.Info($"Revert name back for shifted player: {player.GetNameWithRole()}", "ReportDeadBody");
pc.RpcSetName(Main.AllPlayerNames[pc.PlayerId]);
}
// PhantomRolePatch.OnReportDeadBody(pc);
}
catch (Exception e) { ThrowException(e); }
}
EAC.TimeSinceLastTaskCompletion.Clear();
try
{
Enigma.OnReportDeadBody(player, target);
Medium.OnReportDeadBody(target);
Mortician.OnReportDeadBody(player, target);
Spiritualist.OnReportDeadBody(target);
Bloodmoon.OnMeetingStart();
Deadlined.OnMeetingStart();
Committed.OnMeetingStart();
Reroll.OnMeetingStart();
}
catch (Exception e) { ThrowException(e); }
Main.LastVotedPlayerInfo = null;
Arsonist.ArsonistTimer.Clear();
Investigator.InvestigatorTimer = null;
Puppeteer.PuppeteerList.Clear();
Puppeteer.PuppeteerDelayList.Clear();
Veteran.VeteranInProtect.Clear();
Grenadier.GrenadierBlinding.Clear();
SecurityGuard.BlockSabo.Clear();
Grenadier.MadGrenadierBlinding.Clear();
FortuneTeller.DidVote = null;
Oracle.DidVote = null;
if (Imitator.ImitatingRole != null)
{
Imitator.ImitatingRole.ToArray().DoIf(x => !Main.PlayerStates.TryGetValue(x.Key, out var s) || s.MainRole != x.Value, x => Imitator.ImitatingRole.Remove(x.Key));
Imitator.ImitatingRole.SetAllValues(CustomRoles.Imitator);
}
foreach (PlayerState state in Main.PlayerStates.Values)
{
try
{
if (state.Role.IsEnable)
state.Role.OnReportDeadBody();
}
catch (Exception e) { ThrowException(e); }
}
Main.AbilityCD.Clear();
SendRPC(CustomRPC.SyncAbilityCD, 2);
try
{
if (player.Is(CustomRoles.Damocles)) Damocles.OnReport(player.PlayerId);
Damocles.OnMeetingStart();
if (player.Is(CustomRoles.Stressed)) Stressed.OnReport(player);
Stressed.OnMeetingStart();
}
catch (Exception e) { ThrowException(e); }
MeetingTimeManager.OnReportDeadBody();
NameNotifyManager.Reset();
NotifyRoles(ForMeeting: true, ForceLoop: true, CamouflageIsForMeeting: true, GuesserIsForMeeting: true);
try
{
Main.ProcessShapeshifts = false;
foreach (PlayerControl pc in Main.EnumeratePlayerControls())
{
try
{
if (pc.IsAlive())
{
if (Camouflage.IsCamouflage && !Magistrate.CallCourtNextMeeting && !Doppelganger.DoppelVictim.ContainsKey(pc.PlayerId))
Camouflage.RpcSetSkin(pc, revertToDefault: true, forceRevert: true);
if (Magistrate.CallCourtNextMeeting)
{
string name = pc.GetRealName();
RpcChangeSkin(pc, new NetworkedPlayerInfo.PlayerOutfit().Set(name, 15, "", "", "", "", ""));
}
if (Main.CheckShapeshift.ContainsKey(pc.PlayerId))
pc.RpcShapeshift(pc, false);
if (pc.Is(CustomRoles.Truant))
Main.AllPlayerSpeed[pc.PlayerId] = Main.MinSpeed;
}
else if (!pc.Data.IsDead && (!pc.AmOwner || !TempReviveHostRunning))
pc.RpcExileV2();
}
catch (Exception e) { ThrowException(e); }
}
RPCHandlerPatch.RemoveExpiredWhiteList();
Camouflage.CamoTimesThisRound = 0;
NumSnapToCallsThisRound = 0;
if (HudManagerPatch.AchievementUnlockedText == string.Empty)
HudManagerPatch.ClearLowerInfoText();
Logger.Info("AfterReportTasks finished", "ReportDeadBody");
}
catch (Exception e) { ThrowException(e); }
}
}
//[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))]
internal static class FixedUpdatePatch
{
private static readonly StringBuilder Mark = new(20);
private static readonly StringBuilder Suffix = new();
private static int LevelKickBufferTime = 10;
private static readonly Dictionary<byte, int> DeadBufferTime = [];
private static readonly Dictionary<byte, long> LastUpdate = [];
private static readonly Dictionary<byte, long> LastAddAbilityTime = [];
private static readonly List<string> AdditionalSuffixes = [];
private static long LastErrorTS;
public static void Postfix(PlayerControl __instance, bool lowLoad)
{
try
{
if (__instance.PlayerId >= 254) return;
CheckMurderPatch.Update(__instance.PlayerId);
if (AmongUsClient.Instance.AmHost && __instance.AmOwner)
CustomNetObject.FixedUpdate();
byte id = __instance.PlayerId;
if (AmongUsClient.Instance.AmHost && GameStates.IsInTask && ReportDeadBodyPatch.CanReport[id] && !id.IsPlayerRoleBlocked() && ReportDeadBodyPatch.WaitReport[id].Count > 0)
{
NetworkedPlayerInfo info = ReportDeadBodyPatch.WaitReport[id][0];
ReportDeadBodyPatch.WaitReport[id].Clear();
Logger.Info($"{__instance.GetNameWithRole()}: Now that it is possible to report, we will process the report.", "ReportDeadBody");
__instance.ReportDeadBody(info);
}
if (AmongUsClient.Instance.AmHost)
{
if (GhostRolesManager.AssignedGhostRoles.TryGetValue(id, out (CustomRoles Role, IGhostRole Instance) ghostRole))
{
switch (ghostRole.Instance)
{
case Warden warden:
warden.Update(__instance);
break;
case Haunter haunter:
haunter.Update(__instance);
break;
case Bloodmoon:
Bloodmoon.Update(__instance);
break;
}
}
else if (!Main.HasJustStarted && GameStates.IsInTask && !ExileController.Instance && GhostRolesManager.ShouldHaveGhostRole(__instance))
GhostRolesManager.AssignGhostRole(__instance);
}
if (GameStates.InGame && Options.DontUpdateDeadPlayers.GetBool() && !(__instance.IsHost() && __instance.AmOwner) && !__instance.IsAlive() && !__instance.GetCustomRole().NeedsUpdateAfterDeath() && !__instance.HasAbilityCD() && Options.CurrentGameMode is not CustomGameMode.RoomRush and not CustomGameMode.Quiz)
{
int buffer = Options.DeepLowLoad.GetBool() ? 150 : 60;
DeadBufferTime.TryAdd(id, buffer);
DeadBufferTime[id]--;
if (DeadBufferTime[id] > 0) return;
DeadBufferTime[id] = buffer;
}
}
catch (Exception e) { ThrowException(e); }
try { DoPostfix(__instance, lowLoad); }
catch (Exception ex)
{
if (OnGameJoinedPatch.JoiningGame && ex is NullReferenceException) return;
long now = TimeStamp;
string nameWithRole = __instance.GetNameWithRole();
if (string.IsNullOrWhiteSpace(nameWithRole)) return;
if (LastErrorTS != now)
{
Logger.Error($"Error for {nameWithRole}:", "FixedUpdatePatch");
ThrowException(ex);
LastErrorTS = now;
return;
}
Logger.Error($"Error for {nameWithRole}: {ex}", "FixedUpdatePatch");
}
}
private static void DoPostfix(PlayerControl player, bool lowLoad)
{
byte playerId = player.PlayerId;
byte lpId = PlayerControl.LocalPlayer.PlayerId;
bool self = playerId == lpId; // Updates that are independent of the player are only executed for the local player.
bool inTask = GameStates.IsInTask;
bool isInGame = GameStates.IsInGame;
bool isLobby = GameStates.IsLobby;
bool alive = player.IsAlive();
var playerState = Main.PlayerStates.GetValueOrDefault(playerId);
var localPlayerState = Main.PlayerStates.GetValueOrDefault(lpId);
if (self)
{
if (NumSnapToCallsThisRound > 80)
NumSnapToCallsThisRound = 80;
Zoom.OnFixedUpdate();
TextBoxPatch.CheckChatOpen();
if (!isLobby)
{
CustomSabotage.UpdateAll();
NameNotifyManager.OnFixedUpdate();
LastImpostor.SetSubRole();
}
}
if (!lowLoad)
{
TargetArrow.OnFixedUpdate(player);
LocateArrow.OnFixedUpdate(player);
if (AmongUsClient.Instance.AmHost)
{
Camouflage.OnFixedUpdate(player);
if (self && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.FFA or CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.Snowdown && IntroCutsceneDestroyPatch.IntroDestroyTS + 20 == TimeStamp)
NotifyRoles();
}
}
long now = TimeStamp;
if (AmongUsClient.Instance.AmHost)
{
AFKDetector.OnFixedUpdate(player);
if (isLobby && (ModUpdater.HasUpdate && ModUpdater.ForceUpdate || ModUpdater.IsBroken || !Main.AllowPublicRoom) && AmongUsClient.Instance.IsGamePublic)
AmongUsClient.Instance.ChangeGamePublic(false);
// Kick low-level people
if (!lowLoad && isLobby && GameSettingMenuPatch.LastPresetChange + 5 < TimeStamp && !player.AmOwner && Options.KickLowLevelPlayer.GetInt() != 0 && (
player.Data.PlayerLevel != 0 && player.Data.PlayerLevel < Options.KickLowLevelPlayer.GetInt() ||
player.Data.FriendCode == string.Empty
))
{
LevelKickBufferTime--;
if (LevelKickBufferTime <= 0)
{
LevelKickBufferTime = 20;
if (player.GetClient().ProductUserId != "")
{
string hashedPuid = player.GetClient().GetHashedPuid();
if (!BanManager.TempBanWhiteList.Contains(hashedPuid)) BanManager.TempBanWhiteList.Add(hashedPuid);
}
if (!Main.EnumeratePlayerControls().All(x => x.Data.PlayerLevel <= 1))
{
string msg = string.Format(GetString("KickBecauseLowLevel"), player.GetRealName().RemoveHtmlTags());
Logger.SendInGame(msg, Color.yellow);
AmongUsClient.Instance.KickPlayer(player.OwnerId, true);
Logger.Info(msg, "Low Level Temp Ban");
}
}
}
if (!isLobby)
{
if (player.Is(CustomRoles.Spurt) && !Mathf.Approximately(Main.AllPlayerSpeed[playerId], Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod)) && !inTask && !GameStates.IsMeeting) // fix ludicrous bug
{
Main.AllPlayerSpeed[playerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod);
player.MarkDirtySettings();
}
if (!Main.KillTimers.TryAdd(playerId, 10f) && (!player.inVent || player.Is(CustomRoles.Haste)) && Main.KillTimers[playerId] > 0)
Main.KillTimers[playerId] -= Time.fixedDeltaTime;
if (self)
{
if (QuizMaster.On && inTask && !lowLoad && QuizMaster.AllSabotages.FindFirst(IsActive, out SystemTypes active))
QuizMaster.Data.LastSabotage = active;
}
if (!lowLoad && player.IsModdedClient() && player.Is(CustomRoles.Haste))
player.ForceKillTimerContinue = true;
if (DoubleTrigger.FirstTriggerTimer.Count > 0)
DoubleTrigger.OnFixedUpdate(player);
SabotageSystemTypeUpdateSystemPatch.SabotageDoubleTrigger.Update(Time.deltaTime);
if (playerState.Role.IsEnable)
playerState.Role.OnFixedUpdate(player);
if (inTask && player.Is(CustomRoles.PlagueBearer) && PlagueBearer.IsPlaguedAll(player))
{
player.RpcSetCustomRole(CustomRoles.Pestilence);
player.Notify(GetString("PlagueBearerToPestilence"));
player.RpcGuardAndKill(player);
playerState.SubRoles
.FindAll(x => x is CustomRoles.Fragile or CustomRoles.Unbound or CustomRoles.Diseased or CustomRoles.Antidote or CustomRoles.Beartrap or CustomRoles.Youtuber or CustomRoles.Lucky or CustomRoles.Onbound or CustomRoles.Allergic or CustomRoles.Asthmatic or CustomRoles.Bewilder or CustomRoles.Compelled or CustomRoles.Unlucky or CustomRoles.Bait)
.ForEach(playerState.RemoveSubRole);
if (!PlagueBearer.PestilenceList.Contains(playerId))
PlagueBearer.PestilenceList.Add(playerId);
player.SetKillCooldown(5f);
PlagueBearer.PlayerIdList.Remove(playerId);
}
bool checkPos = inTask && !ExileController.Instance && !AntiBlackout.SkipTasks && alive && !Pelican.IsEaten(playerId) && Main.IntroDestroyed;
if (checkPos)
{
Asthmatic.OnCheckPlayerPosition(player);
Transfer.OnCheckPlayerPosition(player);
}
foreach (PlayerState state in Main.PlayerStates.Values)
{
if (!state.Role.IsEnable) continue;
if (checkPos) state.Role.OnCheckPlayerPosition(player);
state.Role.OnGlobalFixedUpdate(player, lowLoad);
}
if (!lowLoad && inTask && alive && playerState != null)
{
List<CustomRoles> subRoles = playerState.SubRoles;
if (subRoles.Contains(CustomRoles.Dynamo)) Dynamo.OnFixedUpdate(player);
if (subRoles.Contains(CustomRoles.Spurt)) Spurt.OnFixedUpdate(player);
if (subRoles.Contains(CustomRoles.Damocles)) Damocles.Update(player);
if (subRoles.Contains(CustomRoles.Stressed)) Stressed.Update(player);
if (subRoles.Contains(CustomRoles.Disco)) Disco.OnFixedUpdate(player);
if (subRoles.Contains(CustomRoles.Clumsy)) Clumsy.OnFixedUpdate(player);
if (subRoles.Contains(CustomRoles.Sonar)) Sonar.OnFixedUpdate(player);
if (subRoles.Contains(CustomRoles.Sleep)) Sleep.CheckGlowNearby(player);
if (subRoles.Contains(CustomRoles.Introvert)) Introvert.OnFixedUpdate(player);
if (subRoles.Contains(CustomRoles.Allergic)) Allergic.OnFixedUpdate(player);
if (subRoles.Contains(CustomRoles.Entombed)) Entombed.OnFixedUpdate(player);
}
if (inTask && alive && !lowLoad)
Asthmatic.OnFixedUpdate();
if (!lowLoad && inTask && (!LastUpdate.TryGetValue(playerId, out long lastPetNotify) || lastPetNotify < now))
{
if (Main.AbilityCD.TryGetValue(playerId, out (long StartTimeStamp, int TotalCooldown) timer))
{
SendOption sendOption = SendOption.None;
if (timer.StartTimeStamp + timer.TotalCooldown < now || !alive && !player.HasGhostRole())
{
player.RemoveAbilityCD();
sendOption = SendOption.Reliable;
}
long remaining = timer.TotalCooldown - (now - timer.StartTimeStamp);
if (!player.IsModdedClient() && remaining <= 30)
{
if (remaining % 10 == 0 || remaining == 5) sendOption = SendOption.Reliable;
NotifyRoles(SpecifySeer: player, SpecifyTarget: player, SendOption: sendOption);
}
LastUpdate[playerId] = now;
}
}
if (!lowLoad) Randomizer.OnFixedUpdateForPlayers(player);
RoleBlockManager.OnFixedUpdate(player);
}
}
if (!lowLoad)
{
// Ability Use Gain every 5 seconds
if (inTask && alive && playerState.TaskState.IsTaskFinished && (!LastAddAbilityTime.TryGetValue(playerId, out long ts) || ts + 5 < now))
{
LastAddAbilityTime[playerId] = now;
AddExtraAbilityUsesOnFinishedTasks(player);
}
if (AmongUsClient.Instance.AmHost && inTask && alive && Options.LadderDeath.GetBool())
FallFromLadder.FixedUpdate(player);
if (inTask && Options.DisableDevices.GetBool())
DisableDevice.FixedUpdate(player);
if (!Main.DoBlockNameChange && ApplySuffix(player, out var name))
player.RpcSetName(name);
}
if (GameStates.IsEnded || !Main.IntroDestroyed || GameStates.IsMeeting || ExileController.Instance || AntiBlackout.SkipTasks) return;
bool shouldUpdateRegardlessOfLowLoad = self && GameStates.InGame && PlayerControl.LocalPlayer.IsAlive() && (PlayerControl.AllPlayerControls.Count > 30 && PerSecondUpdateScheduler.ShouldRunUpdate() && Options.CurrentGameMode is CustomGameMode.StopAndGo or CustomGameMode.HotPotato or CustomGameMode.Speedrun or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.Mingle || DirtyName.Remove(lpId));
if (lowLoad && !shouldUpdateRegardlessOfLowLoad) return;
if (isLobby && !player.AmOwner)
{
if (Main.PlayerVersion.TryGetValue(playerId, out PlayerVersion ver))
{
if (Main.ForkId != ver.forkId)
player.cosmetics.nameText.text = $"<color=#ff0000><size=1.4>{ver.forkId}</size>\n{player.name}</color>";
else if (Main.Version.CompareTo(ver.version) == 0)
player.cosmetics.nameText.text = ver.tag == $"{ThisAssembly.Git.Commit}({ThisAssembly.Git.Branch})" ? Main.ShowModdedClientText.Value ? $"<color=#00a5ff><size=1.4>{GetString("ModdedClient")}</size>\n{player.name}</color>" : $"<color=#00a5ff>{player.name}</color>" : $"<color=#ffff00><size=1.4>{ver.tag}</size>\n{player.name}</color>";
else
player.cosmetics.nameText.text = $"<color=#ff0000><size=1.4>v{ver.version}</size>\n{player.name}</color>";
}
else
player.cosmetics.nameText.text = Main.ShowPlayerInfoInLobby.Value && !player.AmOwner ? $"<#888888><size=1.2>{player.GetClient().PlatformData.Platform} | {player.FriendCode} | {player.GetClient().GetHashedPuid()}</size></color>\n{player.Data?.PlayerName}" : player.Data?.PlayerName;
}
if (isInGame)
{
if (!AmongUsClient.Instance.AmHost && Options.CurrentGameMode != CustomGameMode.Standard) return;
bool shouldSeeTargetAddons = self || PlayerControl.LocalPlayer.Is(Team.Impostor) && player.Is(Team.Impostor);
string roleText;
bool hideRoleText = false;
if (Options.CurrentGameMode is not CustomGameMode.Standard and not CustomGameMode.HideAndSeek || !IsRoleTextEnabled(player))
{
roleText = string.Empty;
hideRoleText = true;
}
else
{
(string, Color) roleTextData = GetRoleText(lpId, playerId, seeTargetBetrayalAddons: shouldSeeTargetAddons);
roleText = ColorString(roleTextData.Item2, roleTextData.Item1);
}
if (!hideRoleText && PlayerControl.LocalPlayer.IsAlive() && Investigator.RandomRole != null && PlayerControl.LocalPlayer.IsRevealedPlayer(player) && player.Is(CustomRoles.Trickster))
{
roleText = Investigator.RandomRole[lpId];
roleText += Investigator.GetTaskState();
}
string progressText = GetProgressText(player);
if (progressText.RemoveHtmlTags().Length > 25 && Main.VisibleTasksCount)
progressText = $"\n{progressText}";
bool stopandgo = Options.CurrentGameMode == CustomGameMode.StopAndGo;
if (!hideRoleText && Main.VisibleTasksCount)
{
if (stopandgo) roleText = roleText.Insert(0, progressText);
else roleText += progressText;
}
PlayerControl seer = PlayerControl.LocalPlayer;
PlayerControl target = player;
if (target.Is(CustomRoles.Car))
{
target.cosmetics.nameText.text = Car.Name;
return;
}
if (playerState.Role is Tree { TreeSpriteActive: true })
{
target.cosmetics.nameText.text = Tree.Sprite;
return;
}
Mark.Clear();
Suffix.Clear();
string realName = target.GetRealName();
if (ApplySuffix(player, out var formattedName))
realName = formattedName;
if (target.Is(CustomRoles.BananaMan))
realName = realName.Insert(0, $"{GetString("Prefix.BananaMan")} ");
if (!self && playerState.Role is Venerer { ChangedSkin: true })
realName = string.Empty;
if (target.AmOwner && inTask)
{
if (target.Is(CustomRoles.Arsonist) && target.IsDouseDone())
realName = CustomRoles.Arsonist.ColoredTextByRole(GetString(Options.UsePets.GetBool() ? "PetToWin" : "EnterVentToWin"));
else if (target.Is(CustomRoles.Revolutionist) && target.IsDrawDone()) realName = CustomRoles.Revolutionist.ColoredTextByRole(string.Format(GetString("EnterVentWinCountDown"), Revolutionist.RevolutionistCountdown.GetValueOrDefault(lpId, 10)));
if (Pelican.IsEaten(lpId)) realName = CustomRoles.Pelican.ColoredTextByRole(GetString("EatenByPelican"));
switch (Options.CurrentGameMode)
{
case CustomGameMode.SoloPVP:
SoloPVP.GetNameNotify(target, ref realName);
break;
case CustomGameMode.BedWars when self:
case CustomGameMode.Quiz when self:
realName = string.Empty;
break;
}
if (Deathpact.IsInActiveDeathpact(seer)) realName = Deathpact.GetDeathpactString(seer);
if (Options.CurrentGameMode != CustomGameMode.BedWars && NameNotifyManager.GetNameNotify(target, out string name) && name.Length > 0)
realName = name;
}
// Name Color Manager
realName = realName.ApplyNameColorData(seer, target, false);
Suffix.Append(BuildSuffix(seer, target, meeting: GameStates.IsMeeting));
AdditionalSuffixes.Clear();
if (self) AdditionalSuffixes.Add(CustomTeamManager.GetSuffix(seer));
AdditionalSuffixes.Add(AFKDetector.GetSuffix(seer, target));
if (!GameStates.IsMeeting && Options.CurrentGameMode == CustomGameMode.Standard && Main.Invisible.Contains(target.PlayerId) && (self && seer.IsAlive() && target.GetCustomRole() is not (CustomRoles.Swooper or CustomRoles.Wraith or CustomRoles.Chameleon) || seer.IsImpostor() && target.IsImpostor()))
AdditionalSuffixes.Add(ColorString(Palette.White_75Alpha, "\n" + GetString("Invisible")));
switch (target.GetCustomRole())
{
case CustomRoles.Snitch when seer.IsImpostor() && target.Is(CustomRoles.Madmate) && target.GetTaskState().IsTaskFinished:
Mark.Append(CustomRoles.Impostor.ColoredTextByRole("★"));
break;
case CustomRoles.Marshall when Marshall.CanSeeMarshall(seer) && target.GetTaskState().IsTaskFinished:
Mark.Append(CustomRoles.Marshall.ColoredTextByRole("★"));
break;
case CustomRoles.SuperStar when Options.EveryOneKnowSuperStar.GetBool():
Mark.Append(CustomRoles.SuperStar.ColoredTextByRole("★"));
break;
case CustomRoles.Infection:
Mark.Append(Infection.GetMarkOthers(seer, target));
break;
}
switch (seer.GetCustomRole())
{
case CustomRoles.Lookout when seer.IsAlive() && target.IsAlive():
Mark.Append(CustomRoles.Lookout.ColoredTextByRole(" " + target.PlayerId) + " ");
break;
case CustomRoles.PlagueBearer when PlagueBearer.IsPlagued(lpId, target.PlayerId):
Mark.Append(CustomRoles.PlagueBearer.ColoredTextByRole("●"));
break;
case CustomRoles.Arsonist:
if (seer.IsDousedPlayer(target))
Mark.Append(CustomRoles.Arsonist.ColoredTextByRole("▲"));
else if (
Arsonist.CurrentDousingTarget != byte.MaxValue &&
Arsonist.CurrentDousingTarget == target.PlayerId
)
Mark.Append(CustomRoles.Arsonist.ColoredTextByRole("△"));
break;
case CustomRoles.Revolutionist:
if (seer.IsDrawPlayer(target))
Mark.Append(CustomRoles.Revolutionist.ColoredTextByRole("●"));
else if (
Revolutionist.CurrentDrawTarget != byte.MaxValue &&
Revolutionist.CurrentDrawTarget == target.PlayerId
)
Mark.Append(CustomRoles.Revolutionist.ColoredTextByRole("○"));
break;
case CustomRoles.Investigator:
if (Investigator.CurrentRevealTarget != byte.MaxValue &&
Investigator.CurrentRevealTarget == target.PlayerId)
Mark.Append(CustomRoles.Investigator.ColoredTextByRole("○"));
break;
case CustomRoles.Analyst when (localPlayerState.Role as Analyst).CurrentTarget.ID == target.PlayerId:
Mark.Append(CustomRoles.Analyst.ColoredTextByRole("○"));
break;
case CustomRoles.Samurai when (localPlayerState.Role as Samurai).Target.Id == target.PlayerId:
Mark.Append(CustomRoles.Samurai.ColoredTextByRole("○"));
break;
case CustomRoles.Puppeteer when Puppeteer.PuppeteerList.ContainsValue(lpId) && Puppeteer.PuppeteerList.ContainsKey(target.PlayerId):
Mark.Append(CustomRoles.Impostor.ColoredTextByRole("◆"));
break;
case CustomRoles.EvilTracker:
Mark.Append(EvilTracker.GetTargetMark(seer, target));
break;
case CustomRoles.Scout:
Mark.Append(Scout.GetTargetMark(seer, target));
break;
case CustomRoles.Telecommunication when inTask && self:
if (AntiAdminer.IsAdminWatch) AdditionalSuffixes.Add($"{GetString("AntiAdminerAD")} <size=70%>({AntiAdminer.PlayersNearDevices.Where(x => x.Value.Contains(AntiAdminer.Device.Admin)).Select(x => x.Key.ColoredPlayerName()).Join()})</size>");
if (AntiAdminer.IsVitalWatch) AdditionalSuffixes.Add($"{GetString("AntiAdminerVI")} <size=70%>({AntiAdminer.PlayersNearDevices.Where(x => x.Value.Contains(AntiAdminer.Device.Vitals)).Select(x => x.Key.ColoredPlayerName()).Join()})</size>");
if (AntiAdminer.IsDoorLogWatch) AdditionalSuffixes.Add($"{GetString("AntiAdminerDL")} <size=70%>({AntiAdminer.PlayersNearDevices.Where(x => x.Value.Contains(AntiAdminer.Device.DoorLog)).Select(x => x.Key.ColoredPlayerName()).Join()})</size>");
if (AntiAdminer.IsCameraWatch) AdditionalSuffixes.Add($"{GetString("AntiAdminerCA")} <size=70%>({AntiAdminer.PlayersNearDevices.Where(x => x.Value.Contains(AntiAdminer.Device.Camera)).Select(x => x.Key.ColoredPlayerName()).Join()})</size>");
break;
case CustomRoles.AntiAdminer when inTask && self:
if (AntiAdminer.IsAdminWatch) AdditionalSuffixes.Add(GetString("AntiAdminerAD"));
if (AntiAdminer.IsVitalWatch) AdditionalSuffixes.Add(GetString("AntiAdminerVI"));
if (AntiAdminer.IsDoorLogWatch) AdditionalSuffixes.Add(GetString("AntiAdminerDL"));
if (AntiAdminer.IsCameraWatch) AdditionalSuffixes.Add(GetString("AntiAdminerCA"));
break;
case CustomRoles.Demon:
Mark.Append(Demon.TargetMark(seer, target));
break;
}
Mark.Append(Executioner.TargetMark(seer, target));
Mark.Append(Follower.TargetMark(seer, target));
Mark.Append(Romantic.TargetMark(seer, target));
Mark.Append(Lawyer.LawyerMark(seer, target));
Mark.Append(Marshall.GetWarningMark(seer, target));
if (Randomizer.IsShielded(target)) Mark.Append(CustomRoles.Randomizer.ColoredTextByRole("✚"));
if (target.AmOwner) Mark.Append(Sniper.GetShotNotify(target.PlayerId));
if (Roles.Lightning.IsGhost(target)) Mark.Append(CustomRoles.Lightning.ColoredTextByRole("■"));
Mark.Append(Medic.GetMark(seer, target));
Mark.Append(Gaslighter.GetMark(seer, target));
Mark.Append(Snitch.GetWarningArrow(seer, target));
Mark.Append(Snitch.GetWarningMark(seer, target));
Mark.Append(Deathpact.GetDeathpactMark(seer, target));
if (!lowLoad)
{
for (int i = Main.LoversPlayers.Count - 1; i >= 0; i--)
{
PlayerControl lover = Main.LoversPlayers[i];
if (!lover)
{
Main.LoversPlayers.RemoveAt(i);
continue;
}
if (!Main.HasJustStarted && !lover.Is(CustomRoles.Lovers))
lover.RpcSetCustomRole(CustomRoles.Lovers);
}
}
if (Main.LoversPlayers.Exists(x => x.PlayerId == target.PlayerId) && (Main.LoversPlayers.Exists(x => x.PlayerId == lpId) || !seer.IsAlive()))
Mark.Append(CustomRoles.Lovers.ColoredTextByRole("♥"));
if (self)
{
AdditionalSuffixes.Add(Bloodmoon.GetSuffix(seer));
AdditionalSuffixes.Add(Haunter.GetSuffix(seer));
if (seer.Is(CustomRoles.Asthmatic)) AdditionalSuffixes.Add(Asthmatic.GetSuffixText(lpId));
if (seer.Is(CustomRoles.Sonar)) AdditionalSuffixes.Add(Sonar.GetSuffix(seer, GameStates.IsMeeting));
if (seer.Is(CustomRoles.Deadlined)) AdditionalSuffixes.Add(Deadlined.GetSuffix(seer));
if (seer.Is(CustomRoles.Allergic)) AdditionalSuffixes.Add(Allergic.GetSelfSuffix(seer));
if (localPlayerState.Role is CovenBase { HasNecronomicon: true })
Mark.Append(" <").Append(Main.CovenColor).Append(">♤</color>");
}
switch (Options.CurrentGameMode)
{
case CustomGameMode.SoloPVP:
AdditionalSuffixes.Add(SoloPVP.GetDisplayHealth(target, self));
break;
case CustomGameMode.FFA:
AdditionalSuffixes.Add(FreeForAll.GetPlayerArrow(seer, target));
break;
case CustomGameMode.StopAndGo when self:
AdditionalSuffixes.Add(StopAndGo.GetSuffixText(seer));
break;
case CustomGameMode.Speedrun when self:
AdditionalSuffixes.Add(Speedrun.GetSuffixText(seer));
break;
case CustomGameMode.HideAndSeek:
AdditionalSuffixes.Add(CustomHnS.GetSuffixText(seer, target));
break;
case CustomGameMode.CaptureTheFlag:
AdditionalSuffixes.Add(CaptureTheFlag.GetSuffixText(seer, target));
break;
case CustomGameMode.RoomRush when self:
AdditionalSuffixes.Add(RoomRush.GetSuffix(seer));
break;
case CustomGameMode.KingOfTheZones when self:
AdditionalSuffixes.Add(KingOfTheZones.GetSuffix(seer));
break;
case CustomGameMode.Quiz when self:
AdditionalSuffixes.Add(Quiz.GetSuffix(seer));
break;
case CustomGameMode.TheMindGame:
AdditionalSuffixes.Add(TheMindGame.GetSuffix(seer, target));
break;
case CustomGameMode.BedWars:
AdditionalSuffixes.Add(BedWars.GetSuffix(seer, target));
break;
case CustomGameMode.Deathrace:
AdditionalSuffixes.Add(Deathrace.GetSuffix(seer, target, false));
break;
case CustomGameMode.Mingle when self:
AdditionalSuffixes.Add(Mingle.GetSuffix(seer));
break;
case CustomGameMode.Snowdown:
AdditionalSuffixes.Add(Snowdown.GetSuffix(seer, target));
break;
}
if (MeetingStates.FirstMeeting && Main.ShieldPlayer == target.FriendCode && !string.IsNullOrWhiteSpace(target.FriendCode) && !self && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.SoloPVP or CustomGameMode.FFA)
AdditionalSuffixes.Add(GetString("DiedR1Warning"));
for (int i = AdditionalSuffixes.Count - 1; i >= 0; i--)
{
if (string.IsNullOrWhiteSpace(AdditionalSuffixes[i]))
{
AdditionalSuffixes.RemoveAt(i);
}
}
if (AdditionalSuffixes.Count > 0)
{
if (Suffix.ToString().RemoveHtmlTags().Length > 0 && Suffix[^1] != '\n')
Suffix.Append('\n');
Suffix.Append(string.Join('\n', AdditionalSuffixes));
}
if (self && IntroCutsceneDestroyPatch.IntroDestroyTS + 20 > TimeStamp && Main.HasPlayedGM.TryGetValue(Options.CurrentGameMode, out HashSet<string> playedFCs) && !playedFCs.Contains(seer.FriendCode))
Suffix.Append($"\n\n<#ffffff>{GetString($"GameModeTutorial.{Options.CurrentGameMode}")}</color>\n");
// Devourer
if (Devourer.HideNameOfConsumedPlayer.GetBool())
{
foreach (byte pId in Devourer.PlayerIdList)
if (Main.PlayerStates[pId].Role is Devourer { IsEnable: true } dv && dv.PlayerSkinsCosumed.Contains(target.PlayerId))
{
realName = GetString("DevouredName");
break;
}
}
if (!self && Options.CurrentGameMode == CustomGameMode.KingOfTheZones && Main.IntroDestroyed && !KingOfTheZones.GameGoing)
realName = EmptyMessage;
if (!hideRoleText)
{
if (!Options.LargerRoleTextSize.GetBool())
roleText = $"<size=1.7>{roleText}</size>\n";
else
roleText += "\n";
}
else
roleText = string.Empty;
string suffix = Suffix.ToString().Trim();
string newLineBeforeSuffix = !(Options.CurrentGameMode == CustomGameMode.BedWars && !self && GameStates.InGame) ? "\n" : " - ";
string deathReason = !seer.IsAlive() && seer.KnowDeathReason(target) ? $"{newLineBeforeSuffix}<size=1.5>『{CustomRoles.Doctor.ColoredTextByRole(GetVitalText(target.PlayerId))}』</size>" : string.Empty;
target.cosmetics.nameText.text = $"{roleText}{realName}{deathReason}{Mark}{newLineBeforeSuffix}{suffix}";
// Camouflage
if (Camouflage.IsCamouflage) target.cosmetics.nameText.text = $"<size=0>{target.cosmetics.nameText.text}</size>";
}
}
public static void AddExtraAbilityUsesOnFinishedTasks(PlayerControl player)
{
if (Main.HasJustStarted || !player.IsAlive()) return;
float add = GetSettingNameAndValueForRole(player.GetCustomRole(), "AbilityChargesWhenFinishedTasks");
if (Math.Abs(add - float.MaxValue) > 0.5f && add > 0)
{
if (player.Is(CustomRoles.Bloodlust)) add *= 5;
if (player.Is(CustomRoles.Composter)) add *= Composter.AbilityUseGainMultiplier.GetFloat();
player.RpcIncreaseAbilityUseLimitBy(add);
}
}
public static void LoversSuicide(byte deathId = 0x7f, bool exile = false, bool force = false, bool guess = false)
{
if (Main.LoversPlayers.Count == 0 || Options.CurrentGameMode != CustomGameMode.Standard) return;
if (Lovers.LoverDieConsequence.GetValue() == 0 || Main.IsLoversDead || Main.LoversPlayers.FindAll(x => x.IsAlive()).Count != 1 && !force) return;
PlayerControl partnerPlayer = Main.LoversPlayers.FirstOrDefault(player => player.PlayerId != deathId && player.IsAlive());
if (!partnerPlayer) return;
Main.IsLoversDead = true;
if (Main.PlayerStates.TryGetValue(deathId, out var deadState) && deadState.deathReason == PlayerState.DeathReason.Disconnected) return;
if (Lovers.LoverDieConsequence.GetValue() == 2)
{
partnerPlayer.MarkDirtySettings();
PlayerGameOptionsSender.SetDirty(deathId);
return;
}
if (Lovers.LoverSuicideTime.GetValue() != 0 && !exile && !guess) return;
if (exile)
CheckForEndVotingPatch.TryAddAfterMeetingDeathPlayers(PlayerState.DeathReason.FollowingSuicide, partnerPlayer.PlayerId);
else
partnerPlayer.Suicide(PlayerState.DeathReason.FollowingSuicide);
}
}
[HarmonyPatch(typeof(Vent), nameof(Vent.ExitVent))]
internal static class ExitVentPatch
{
public static void Postfix(Vent __instance, [HarmonyArgument(0)] PlayerControl pc)
{
Logger.Info($" {pc.GetNameWithRole()}, Vent ID: {__instance.Id} ({__instance.name})", "ExitVent");
if (pc.AmOwner) LateTask.New(() => HudManager.Instance.SetHudActive(pc, pc.Data.Role, true), 0.1f, log: false);
if (!AmongUsClient.Instance.AmHost) return;
if (Main.KillTimers.ContainsKey(pc.PlayerId))
Main.KillTimers[pc.PlayerId] += 0.5f;
Drainer.OnAnyoneExitVent(pc);
Main.PlayerStates[pc.PlayerId].Role.OnExitVent(pc, __instance);
LateTask.New(() =>
{
if (Options.OverrideVisionInVents.GetBool() && Options.InVentVision.ContainsKey(pc.GetTeam()))
pc.MarkDirtySettings();
}, 0.6f);
if (Options.WhackAMole.GetBool()) LateTask.New(() => pc.TPToRandomVent(), 0.6f, "Whack-A-Mole TP");
if (!pc.IsModdedClient() && pc.Is(CustomRoles.Haste)) pc.SetKillCooldown(Math.Max(Main.KillTimers[pc.PlayerId], 0.1f));
}
}
[HarmonyPatch(typeof(Vent), nameof(Vent.EnterVent))]
internal static class EnterVentPatch
{
public static void Postfix(Vent __instance, [HarmonyArgument(0)] PlayerControl pc)
{
Logger.Info($" {pc.GetNameWithRole()}, Vent ID: {__instance.Id} ({__instance.name})", "EnterVent");
if (pc.AmOwner) LateTask.New(() => HudManager.Instance.SetHudActive(pc, pc.Data.Role, true), 0.1f, log: false);
if (AmongUsClient.Instance.AmHost && !pc.CanUseVent(__instance.Id) && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.HideAndSeek && !pc.Is(CustomRoles.Nimble) && !pc.Is(CustomRoles.Bloodlust))
{
pc.MyPhysics?.RpcBootFromVent(__instance.Id);
return;
}
Drainer.OnAnyoneEnterVent(pc, __instance);
Analyst.OnAnyoneEnterVent(pc);
Roles.Sentry.OnAnyoneEnterVent(pc);
switch (pc.GetCustomRole())
{
case CustomRoles.Mayor when !Options.UsePets.GetBool() && Mayor.MayorUsedButtonCount.TryGetValue(pc.PlayerId, out int count2) && count2 < Mayor.MayorNumOfUseButton.GetInt():
if (AmongUsClient.Instance.AmHost) pc.MyPhysics?.RpcBootFromVent(__instance.Id);
pc.ReportDeadBody(null);
break;
case CustomRoles.Vector when !AmongUsClient.Instance.AmHost:
Main.PlayerStates[pc.PlayerId].Role.OnEnterVent(pc, __instance);
break;
}
if (!AmongUsClient.Instance.AmHost) return;
if (Main.KillTimers.TryGetValue(pc.PlayerId, out var killTimer) && killTimer > 0f)
Main.KillTimers[pc.PlayerId] += 0.5f;
CheckInvalidMovementPatch.ExemptedPlayers.Add(pc.PlayerId);
Main.LastEnteredVent[pc.PlayerId] = __instance;
Main.LastEnteredVentLocation[pc.PlayerId] = pc.Pos();
switch (Options.CurrentGameMode)
{
case CustomGameMode.HotPotato:
case CustomGameMode.Speedrun:
case CustomGameMode.CaptureTheFlag:
case CustomGameMode.NaturalDisasters:
case CustomGameMode.KingOfTheZones:
case CustomGameMode.TheMindGame:
case CustomGameMode.Quiz:
case CustomGameMode.SoloPVP when !SoloPVP.CanVent:
case CustomGameMode.DoomTag:
pc.MyPhysics?.RpcBootFromVent(__instance.Id);
break;
}
if (pc.IsRoleBlocked())
{
pc.Notify(BlockedAction.Vent.GetBlockNotify());
pc.MyPhysics?.RpcExitVent(__instance.Id);
return;
}
if (!Rhapsode.CheckAbilityUse(pc) || Stasis.IsTimeFrozen || TimeMaster.Rewinding)
{
pc.MyPhysics?.RpcExitVent(__instance.Id);
return;
}
if (Penguin.IsVictim(pc))
{
pc.MyPhysics?.RpcBootFromVent(__instance.Id);
return;
}
if (SoulHunter.IsSoulHunterTarget(pc.PlayerId))
{
pc.Notify(GetString("SoulHunterTargetNotifyNoVent"));
pc.MyPhysics?.RpcBootFromVent(__instance.Id);
return;
}
if (Ventguard.BlockedVents.Contains(__instance.Id) && (!Ventguard.VentguardBlockDoesNotAffectCrew.GetBool() || !pc.IsCrewmate()))
{
pc.Notify(GetString("EnteredBlockedVent"));
pc.MyPhysics?.RpcExitVent(__instance.Id);
if (Ventguard.VentguardNotifyOnBlockedVentUse.GetBool())
{
foreach (PlayerControl ventguard in Main.CachedAlivePlayerControls())
{
if (ventguard.Is(CustomRoles.Ventguard))
ventguard.Notify(GetString("VentguardNotify"));
}
}
return;
}
if (pc.Is(CustomRoles.Circumvent))
Circumvent.OnEnterVent(pc, __instance);
if (!pc.CanUseVent(__instance.Id) && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.HideAndSeek && !pc.Is(CustomRoles.Nimble) && !pc.Is(CustomRoles.Bloodlust))
pc.MyPhysics?.RpcExitVent(__instance.Id);
if (pc.Is(CustomRoles.Unlucky))
{
if (IRandom.Instance.Next(0, 100) < Options.UnluckyVentSuicideChance.GetInt())
pc.Suicide();
}
if (pc.Is(CustomRoles.Damocles)) Damocles.OnEnterVent(pc.PlayerId, __instance.Id);
Chef.SpitOutFood(pc);
Main.PlayerStates[pc.PlayerId].Role.OnEnterVent(pc, __instance);
if (Options.OverrideVisionInVents.GetBool() && Options.InVentVision.ContainsKey(pc.GetTeam()))
pc.MarkDirtySettings();
if (pc.AmOwner)
Statistics.VentTimes++;
}
}
[HarmonyPatch(typeof(GameData), nameof(GameData.CompleteTask))]
internal static class GameDataCompleteTaskPatch
{
public static void Prefix(PlayerControl pc)
{
Workhorse.OnCompleteTask(pc);
Capitalist.AddTaskForPlayer(pc);
}
public static void Postfix(PlayerControl pc, uint taskId)
{
try
{
if (MeetingHud.Instance && MeetingHud.Instance.state != MeetingHud.MeetingStates.Animating) return;
if (Options.CurrentGameMode == CustomGameMode.HideAndSeek && CustomHnS.PlayerRoles[pc.PlayerId].Interface.Team == Team.Crewmate && pc.IsAlive())
{
#if IL2CPP
var task = pc.myTasks.Find((Il2CppSystem.Predicate<PlayerTask>)(x => taskId == x.Id));
#else
var task = pc.myTasks.Find(x => taskId == x.Id);
#endif
Hider.OnSpecificTaskComplete(pc, task);
}
if (pc.IsAlive())
{
#if IL2CPP
var task = pc.myTasks.Find((Il2CppSystem.Predicate<PlayerTask>)(x => taskId == x.Id));
#else
var task = pc.myTasks.Find(x => taskId == x.Id);
#endif
Benefactor.OnTaskComplete(pc, task);
}
Logger.Info($"TaskComplete: {pc.GetNameWithRole()}", "CompleteTask");
}
catch (Exception e) { ThrowException(e); }
Main.PlayerStates[pc.PlayerId].UpdateTask(pc);
NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc);
GameEndChecker.SetDirtyCheckEnd();
}
}
[HarmonyPatch(typeof(GameManager), nameof(GameManager.OnPlayerDeath))]
static class GameManagerOnPlayerDeathPatch
{
public static void Prefix([HarmonyArgument(0)] PlayerControl player, [HarmonyArgument(1)] ref bool assignGhostRole)
{
if (!ReportDeadBodyPatch.MeetingStarted && !GameStates.IsMeeting && Main.PlayerStates.Values.Any(x => x.Role is SoulCollector sc && sc.ToExile.Contains(player.PlayerId)))
assignGhostRole = false;
}
}
[HarmonyPatch(typeof(LogicRoleSelectionNormal), nameof(LogicRoleSelectionNormal.OnPlayerDeath))]
static class LogicRoleSelectionNormalOnPlayerDeathPatch
{
public static void Prefix([HarmonyArgument(0)] PlayerControl player, [HarmonyArgument(1)] ref bool assignGhostRole)
{
if (!ReportDeadBodyPatch.MeetingStarted && !GameStates.IsMeeting && Main.PlayerStates.Values.Any(x => x.Role is SoulCollector sc && sc.ToExile.Contains(player.PlayerId)))
assignGhostRole = false;
}
}
[HarmonyPatch(typeof(RoleManager), nameof(RoleManager.AssignRoleOnDeath))]
static class RoleManagerAssignRoleOnDeathPatch
{
public static bool Prefix([HarmonyArgument(0)] PlayerControl player)
{
return ReportDeadBodyPatch.MeetingStarted || GameStates.IsMeeting || !Main.PlayerStates.Values.Any(x => x.Role is SoulCollector sc && sc.ToExile.Contains(player.PlayerId));
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Die))]
public static class PlayerControlDiePatch
{
public static void Prefix(PlayerControl __instance, [HarmonyArgument(1)] ref bool assignGhostRole)
{
if (!ReportDeadBodyPatch.MeetingStarted && !GameStates.IsMeeting && Main.PlayerStates.Values.Any(x => x.Role is SoulCollector sc && sc.ToExile.Contains(__instance.PlayerId)))
assignGhostRole = false;
}
public static void Postfix(PlayerControl __instance)
{
if (!AmongUsClient.Instance.AmHost) return;
PetsHelper.RpcRemovePet(__instance);
if (!Main.LIMap) return;
LateTask.New(() =>
{
if (Main.PlayerStates.TryGetValue(__instance.PlayerId, out var state) && !state.IsDead)
{
state.IsDead = true;
var sender = CustomRpcSender.Create($"LIDeathSync:{__instance.GetRealName()}", SendOption.Reliable);
var hasValue = sender.SyncGeneralOptions(__instance);
sender.SendMessage(dispose: !hasValue);
}
}, 0.2f);
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Revive))]
static class PlayerControlRevivePatch
{
public static void Postfix(PlayerControl __instance)
{
if (!AmongUsClient.Instance.AmHost || !Main.LIMap) return;
LateTask.New(() =>
{
if (Main.PlayerStates.TryGetValue(__instance.PlayerId, out var state) && state.IsDead)
{
state.SetAlive();
var sender = CustomRpcSender.Create($"LIReviveSync:{__instance.GetRealName()}", SendOption.Reliable);
var hasValue = sender.SyncGeneralOptions(__instance);
sender.SendMessage(dispose: !hasValue);
__instance.TP(FastVector2.TryGetClosest(__instance.Pos(), Main.EnumerateAlivePlayerControls().Without(__instance).Select(x => x.Pos()).Concat(ShipStatus.Instance.AllVents.Select(x => new Vector2(x.transform.position.x, x.transform.position.y + 0.3636f))), out Vector2 closest) ? closest : Vector2.zero);
}
}, 0.2f);
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckSporeTrigger))]
public static class PlayerControlCheckSporeTriggerPatch
{
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] Mushroom mushroom)
{
Logger.Info($"{__instance.GetNameWithRole()}, mushroom: {mushroom.name} / {mushroom.Id}, at {mushroom.origPosition}", "Spore Trigger");
return !AmongUsClient.Instance.AmHost || !Options.DisableSporeTriggerOnFungle.GetBool();
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckUseZipline))]
public static class PlayerControlCheckUseZiplinePatch
{
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target, [HarmonyArgument(1)] ZiplineBehaviour ziplineBehaviour, [HarmonyArgument(2)] bool fromTop)
{
Logger.Info($"{__instance.GetNameWithRole()}, target: {target.GetNameWithRole()}, {(fromTop ? $"from Top, travel time: {ziplineBehaviour.downTravelTime}s" : $"from Bottom, travel time: {ziplineBehaviour.upTravelTime}s")}", "Zipline Use");
if (Options.CurrentGameMode == CustomGameMode.Deathrace) return true;
if (AmongUsClient.Instance.AmHost)
{
if (Options.DisableZiplineFromTop.GetBool() && fromTop) return false;
if (Options.DisableZiplineFromUnder.GetBool() && !fromTop) return false;
if (__instance.Is(CustomRoleTypes.Coven) && Options.DisableZiplineForCoven.GetBool()) return false;
if (__instance.IsImpostor() && Options.DisableZiplineForImps.GetBool()) return false;
if (__instance.GetCustomRole().IsNeutral() && Options.DisableZiplineForNeutrals.GetBool()) return false;
if (__instance.IsCrewmate() && Options.DisableZiplineForCrew.GetBool()) return false;
}
return true;
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.ProtectPlayer))]
internal static class PlayerControlProtectPlayerPatch
{
public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)] PlayerControl target)
{
Logger.Info($"{__instance.GetNameWithRole()} => {target.GetNameWithRole()}", "ProtectPlayer");
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RemoveProtection))]
internal static class PlayerControlRemoveProtectionPatch
{
public static void Postfix(PlayerControl __instance)
{
Logger.Info($"{__instance.GetNameWithRole()}", "RemoveProtection");
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcSetRole))]
internal static class PlayerControlSetRolePatch
{
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] ref RoleTypes roleType, [HarmonyArgument(1)] ref bool canOverrideRole)
{
canOverrideRole = true;
// Skip after first assign
if (StartGameHostPatch.RpcSetRoleReplacer.BlockSetRole) return true;
string targetName = __instance.GetNameWithRole();
Logger.Info($"{targetName} => {roleType}", "PlayerControl.RpcSetRole");
if (!ShipStatus.Instance.enabled) return true;
if (roleType is RoleTypes.CrewmateGhost or RoleTypes.ImpostorGhost)
roleType = __instance.GetGhostRoleBasis();
return true;
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CoSetRole))]
internal static class PlayerControlLocalSetRolePatch
{
public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)] RoleTypes role)
{
if (!AmongUsClient.Instance.AmHost && !GameStates.IsModHost)
{
CustomRoles moddedRole = role switch
{
RoleTypes.Impostor => CustomRoles.ImpostorEHR,
RoleTypes.Phantom => CustomRoles.PhantomEHR,
RoleTypes.Shapeshifter => CustomRoles.ShapeshifterEHR,
RoleTypes.Crewmate => CustomRoles.CrewmateEHR,
RoleTypes.Engineer => CustomRoles.EngineerEHR,
RoleTypes.Noisemaker => CustomRoles.NoisemakerEHR,
RoleTypes.Scientist => CustomRoles.ScientistEHR,
RoleTypes.Tracker => CustomRoles.TrackerEHR,
RoleTypes.Detective => CustomRoles.DetectiveEHR,
RoleTypes.Viper => CustomRoles.ViperEHR,
RoleTypes.Judge => CustomRoles.JudgeEHR,
_ => CustomRoles.NotAssigned
};
if (moddedRole != CustomRoles.NotAssigned) Main.PlayerStates[__instance.PlayerId].SetMainRole(moddedRole);
}
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.AssertWithTimeout))]
internal static class AssertWithTimeoutPatch
{
public static bool Prefix()
{
return false;
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.MixUpOutfit))]
public static class PlayerControlMixupOutfitPatch
{
public static void Postfix(PlayerControl __instance)
{
if (!__instance.IsAlive()) return;
if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && // Has Impostor role behavior
!PlayerControl.LocalPlayer.Is(Team.Impostor) && // Not an actual Impostor
PlayerControl.LocalPlayer.GetCustomRole().GetDYRole() is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Phantom) // Has Desynced Impostor role
__instance.cosmetics.ToggleNameVisible(false);
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixMixedUpOutfit))]
public static class PlayerControlFixMixedUpOutfitPatch
{
public static void Postfix(PlayerControl __instance)
{
if (!__instance.IsAlive()) return;
__instance.cosmetics.ToggleNameVisible(true);
}
}
[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.RpcBootFromVent))]
internal static class BootFromVentPatch
{
public static bool Prefix(PlayerPhysics __instance)
{
return !GameStates.IsInTask || ExileController.Instance || !__instance || !__instance.myPlayer || !__instance.myPlayer.IsAlive() || !__instance.myPlayer.Is(CustomRoles.Nimble);
}
}