diff --git a/EHR.sln.DotSettings b/EHR.sln.DotSettings
index e7ba64a8a..8784b5d37 100644
--- a/EHR.sln.DotSettings
+++ b/EHR.sln.DotSettings
@@ -17,6 +17,7 @@
BG
CD
CK
+ CN
CNO
CRLF
CTA
diff --git a/GameMode/FreeForAll.cs b/GameMode/FreeForAll.cs
index a627dbaca..6499965cd 100644
--- a/GameMode/FreeForAll.cs
+++ b/GameMode/FreeForAll.cs
@@ -227,6 +227,12 @@ internal static class FreeForAll
}
Logger.Info($"The last 2 players ({killer.GetRealName().RemoveHtmlTags()} & {otherPC?.GetRealName().RemoveHtmlTags()}) now have an arrow toward each other", "FFA");
+
+ if (FFADisableVentingWhenTwoPlayersAlive.GetBool())
+ {
+ if (killer.inVent) killer.MyPhysics?.RpcExitVent(killer.GetClosestVent().Id);
+ if (otherPC != null && otherPC.inVent) otherPC.MyPhysics?.RpcExitVent(otherPC.GetClosestVent().Id);
+ }
}
if (FFAEnableRandomAbilities.GetBool())
diff --git a/GameMode/KingOfTheZones.cs b/GameMode/KingOfTheZones.cs
index 7dae7dd22..572f619d4 100644
--- a/GameMode/KingOfTheZones.cs
+++ b/GameMode/KingOfTheZones.cs
@@ -286,7 +286,6 @@ public static class KingOfTheZones
.SelectMany(x => x)
.ToDictionary(x => x.Key, x => x.Value);
- Main.AllPlayerSpeed.SetAllValues(Main.MinSpeed);
float normalSpeed = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod);
if (Main.GM.Value) Main.AllPlayerSpeed[0] = normalSpeed;
ChatCommands.Spectators.Do(x => Main.AllPlayerSpeed[x] = normalSpeed);
@@ -336,14 +335,19 @@ public static class KingOfTheZones
hasData |= writer.Notify(player, $"<#ffffff>{notify}", 100f);
Logger.Info($"{name} assigned to {team} team", "KOTZ");
- try
+ if (!player.AmOwner)
{
- int targetClientId = player.OwnerId;
- PlayerTeams.DoIf(
- x => x.Key != id && x.Value == team,
- x => writer.RpcSetRole(x.Key.GetPlayer(), RoleTypes.Impostor, targetClientId, changeRoleMap: true));
+ try
+ {
+ int targetClientId = player.OwnerId;
+ PlayerTeams.DoIf(
+ x => x.Key != id && x.Value == team,
+ x => writer.RpcSetRole(x.Key.GetPlayer(), RoleTypes.Impostor, targetClientId, changeRoleMap: true));
+
+ hasData = true;
+ }
+ catch (Exception e) { Utils.ThrowException(e); }
}
- catch (Exception e) { Utils.ThrowException(e); }
writer.SendMessage(!hasData);
}
diff --git a/Modules/AntiBlackout.cs b/Modules/AntiBlackout.cs
index 69d5c745c..15168c4a3 100644
--- a/Modules/AntiBlackout.cs
+++ b/Modules/AntiBlackout.cs
@@ -14,7 +14,7 @@ public static class AntiBlackout
// Optimally, there's 1 living impostor and at least 2 living crewmates in everyone's POV.
// We force this to prevent black screens after meetings.
- public static void SetOptimalRoleTypesToPreventBlackScreen()
+ public static void SetOptimalRoleTypes()
{
// If there are only 2 or fewer players in the game in total, there's nothing we can do.
if (CustomWinnerHolder.WinnerTeam != CustomWinner.Default || PlayerControl.AllPlayerControls.Count <= 2) return;
@@ -126,7 +126,6 @@ public static class AntiBlackout
pc.Exiled();
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(pc.NetId, 4, SendOption.Reliable);
AmongUsClient.Instance.FinishRpcImmediately(writer);
- if (pc.HasDesyncRole()) pc.FixBlackScreen();
if (pc.HasGhostRole()) pc.RpcResetAbilityCooldown();
}
}
diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs
index 0afee65bf..97680981f 100644
--- a/Modules/CustomNetObject.cs
+++ b/Modules/CustomNetObject.cs
@@ -24,7 +24,6 @@ namespace EHR
public static readonly List AllObjects = [];
private static int MaxId = -1;
- private static List TempDespawnedObjects = [];
protected int Id;
public PlayerControl playerControl;
public Vector2 Position;
diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs
index 3cedbd29f..dbe7fa31f 100644
--- a/Modules/ExtendedPlayerControl.cs
+++ b/Modules/ExtendedPlayerControl.cs
@@ -438,12 +438,15 @@ internal static class ExtendedPlayerControl
Logger.Warn($"Invalid Revive for {player.GetRealName()} / Player was already alive? {!player.Data.IsDead}", "RpcRevive");
return;
}
+
+ if (!Main.PlayerStates.TryGetValue(player.PlayerId, out var state)) return;
GhostRolesManager.RemoveGhostRole(player.PlayerId);
- Main.PlayerStates[player.PlayerId].IsDead = false;
- Main.PlayerStates[player.PlayerId].deathReason = PlayerState.DeathReason.etc;
+ state.RealKiller = (DateTime.MinValue, byte.MaxValue);
+ state.IsDead = false;
+ state.deathReason = PlayerState.DeathReason.etc;
TempExiled.Remove(player.PlayerId);
- if (Options.CurrentGameMode == CustomGameMode.Standard) Main.PlayerStates[player.PlayerId].Role.OnRevived(player);
+ if (Options.CurrentGameMode == CustomGameMode.Standard) state.Role.OnRevived(player);
var sender = CustomRpcSender.Create("RpcRevive", SendOption.Reliable);
player.RpcChangeRoleBasis(player.GetCustomRole());
player.ResetKillCooldown();
diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs
index 72c1cf22b..13d120cd2 100644
--- a/Modules/GuessManager.cs
+++ b/Modules/GuessManager.cs
@@ -1188,9 +1188,8 @@ public static class GuessManager
}
private State CurrentState = State.WaitingForTargetSelection;
- private readonly byte GuesserId = guesserId;
private PlayerControl Target;
- private CustomRoleTypes CurrentTeamType;
+ private CustomRoleTypes CurrentTeam;
private CustomRoles[] ShownRoles;
private List CurrentRoles;
private CustomRoles SelectedRole;
@@ -1199,78 +1198,38 @@ public static class GuessManager
public void Reset()
{
- if (CurrentState == State.WaitingForTargetSelection) return;
- CurrentState = State.WaitingForTargetSelection;
- Target = null;
- CurrentTeamType = default(CustomRoleTypes);
- ExistingCNOs.Do(x => x.Despawn());
- ExistingCNOs.Clear();
- NetIdToRawDisplay.Clear();
- Utils.SendGameDataTo(GuesserId.GetPlayer().OwnerId);
- Logger.Msg($"Reset Meeting Shapeshift Menu For Guessing ({Main.AllPlayerNames.GetValueOrDefault(GuesserId, "Someone")})", "Meeting Shapeshift For Guessing");
+ try
+ {
+ if (CurrentState == State.WaitingForTargetSelection) return;
+ CurrentState = State.WaitingForTargetSelection;
+ Target = null;
+ CurrentTeam = default(CustomRoleTypes);
+ ExistingCNOs.Do(x => x.Despawn());
+ ExistingCNOs.Clear();
+ NetIdToRawDisplay.Clear();
+ PlayerControl pc = guesserId.GetPlayer();
+ if (pc != null) Utils.SendGameDataTo(pc.OwnerId);
+ Logger.Msg($"Reset Meeting Shapeshift Menu For Guessing ({Main.AllPlayerNames.GetValueOrDefault(guesserId, "Someone")})", "Meeting Shapeshift For Guessing");
+ }
+ catch (Exception e) { Utils.ThrowException(e); }
}
public void AdvanceStep(PlayerControl target)
{
- Logger.Info($"Advancing Step ({Main.AllPlayerNames.GetValueOrDefault(GuesserId, "Someone")}, from {CurrentState})", "Meeting Shapeshift For Guessing");
-
- switch (CurrentState)
+ try
{
- case State.WaitingForTargetSelection:
+ Logger.Info($"Advancing Step ({Main.AllPlayerNames.GetValueOrDefault(guesserId, "Someone")}, from {CurrentState})", "Meeting Shapeshift For Guessing");
+
+ switch (CurrentState)
{
- Target = target;
- CurrentState = State.TeamSelection;
- SpawnCNOs();
- break;
- }
- case State.TeamSelection:
- {
- if (!TryGetDisplay(out string display)) return;
-
- if (display == "Cancel")
+ case State.WaitingForTargetSelection:
{
- Reset();
- return;
+ Target = target;
+ CurrentState = State.TeamSelection;
+ SpawnCNOs();
+ break;
}
-
- CurrentTeamType = Enum.Parse(display, true);
- ShownRoles = Enum.GetValues().Where(x => x.GetCustomRoleTypes() == CurrentTeamType && ShowRoleOnUI(x)).ToArray();
- CurrentState = State.FirstLetterSelection;
- SpawnCNOs();
- break;
- }
- case State.FirstLetterSelection:
- {
- if (!TryGetDisplay(out string display)) return;
-
- if (display == "Cancel")
- {
- Reset();
- return;
- }
-
- CurrentRoles = ShownRoles.Select(x => (role: x, str: GetString(x.ToString()))).Where(x => display.Split('-').Any(y => x.str.StartsWith(y.Trim(), StringComparison.InvariantCultureIgnoreCase))).Select(x => x.role).ToList();
-
- if (CurrentRoles.Count == 0)
- {
- Reset();
- return;
- }
-
- if (CurrentRoles.Count == 1)
- {
- // Directly select if there's only one role
- SelectedRole = CurrentRoles[0];
- goto case State.RoleSelection;
- }
-
- CurrentState = State.RoleSelection;
- SpawnCNOs();
- break;
- }
- case State.RoleSelection:
- {
- if (SelectedRole == default(CustomRoles))
+ case State.TeamSelection:
{
if (!TryGetDisplay(out string display)) return;
@@ -1280,14 +1239,63 @@ public static class GuessManager
return;
}
- SelectedRole = Enum.Parse(display, true);
+ CurrentTeam = Enum.Parse(display, true);
+ ShownRoles = Enum.GetValues().Where(x => x.GetCustomRoleTypes() == CurrentTeam && ShowRoleOnUI(x)).ToArray();
+ CurrentState = State.FirstLetterSelection;
+ SpawnCNOs();
+ break;
}
+ case State.FirstLetterSelection:
+ {
+ if (!TryGetDisplay(out string display)) return;
- GuesserMsg(GuesserId.GetPlayer(), $"/bt {Target.PlayerId} {GetString(SelectedRole.ToString())}");
- Reset();
- break;
+ if (display == "Cancel")
+ {
+ Reset();
+ return;
+ }
+
+ CurrentRoles = ShownRoles.Select(x => (role: x, str: GetString(x.ToString()))).Where(x => display.Split('-').Any(y => x.str.StartsWith(y.Trim(), StringComparison.InvariantCultureIgnoreCase))).Select(x => x.role).ToList();
+
+ if (CurrentRoles.Count == 0)
+ {
+ Reset();
+ return;
+ }
+
+ if (CurrentRoles.Count == 1)
+ {
+ // Directly select if there's only one role
+ SelectedRole = CurrentRoles[0];
+ goto case State.RoleSelection;
+ }
+
+ CurrentState = State.RoleSelection;
+ SpawnCNOs();
+ break;
+ }
+ case State.RoleSelection:
+ {
+ if (SelectedRole == default(CustomRoles))
+ {
+ if (!TryGetDisplay(out string display)) return;
+
+ if (display == "Cancel")
+ {
+ Reset();
+ return;
+ }
+
+ SelectedRole = Enum.Parse(display, true);
+ }
+
+ GuesserMsg(guesserId.GetPlayer(), $"/bt {Target.PlayerId} {GetString(SelectedRole.ToString())}");
+ Reset();
+ break;
+ }
}
}
+ catch (Exception e) { Utils.ThrowException(e); }
return;
@@ -1302,159 +1310,163 @@ public static class GuessManager
return false;
}
- Logger.Info($"Raw display choice: {display}", $"Meeting Shapeshift For Guessing ({Main.AllPlayerNames.GetValueOrDefault(GuesserId, "Someone")})");
+ Logger.Info($"Raw display choice: {display}", $"Meeting Shapeshift For Guessing ({Main.AllPlayerNames.GetValueOrDefault(guesserId, "Someone")})");
return true;
}
}
public void SpawnCNOs()
{
- IEnumerable choices = CurrentState switch
+ try
{
- State.TeamSelection => Enum.GetNames(),
- State.FirstLetterSelection => BuildLetterGroups(ShownRoles.Select(x => GetString(x.ToString())).OrderBy(x => x)),
- State.RoleSelection => CurrentRoles.Select(x => x.ToString()),
- _ => []
- };
-
- IEnumerable namePlateIds = CurrentState switch
- {
- State.TeamSelection => ["nameplate_ripple", "nameplate_seeker", "nameplate_Polus_Lava", "nameplate_Celeste", "nameplate0001"],
- _ => Enumerable.Repeat(CurrentTeamType switch
+ IEnumerable choices = CurrentState switch
{
- CustomRoleTypes.Impostor => "nameplate_seeker",
- CustomRoleTypes.Crewmate => "nameplate_ripple",
- CustomRoleTypes.Neutral => "nameplate_Polus_Lava",
- CustomRoleTypes.Coven => "nameplate_Celeste",
- CustomRoleTypes.Addon => "nameplate0001",
- _ => ""
- }, 14)
- };
+ State.TeamSelection => Enum.GetNames(),
+ State.FirstLetterSelection => BuildLetterGroups(ShownRoles.Select(x => GetString(x.ToString())).OrderBy(x => x)),
+ State.RoleSelection => CurrentRoles.Select(x => x.ToString()),
+ _ => []
+ };
- choices = choices.Prepend("Cancel");
- namePlateIds = namePlateIds.Prepend("nameplate_candyCanePlate");
-
- (string choice, string namePlateId)[] data = choices.Zip(namePlateIds, (choice, namePlateId) => (choice, namePlateId)).ToArray();
- PlayerControl[] alivePlayerControls = Main.AllAlivePlayerControls;
- int alivePlayerControlsLength = alivePlayerControls.Length - 1;
-
- Logger.Info($"Set Up Meeting Shapeshift Menu For Guessing ({Main.AllPlayerNames.GetValueOrDefault(GuesserId, "Someone")}, {CurrentState})", "Meeting Shapeshift For Guessing");
-
- // First, use living players to show choices by changing their names
- // The local player can't be used to show a choice (-1)
-
- StringBuilder sb = new();
- int textIndex = 0;
-
- var skipped = false;
- PlayerControl guesser = GuesserId.GetPlayer();
- MessageWriter writer = MessageWriter.Get(SendOption.Reliable);
- writer.StartMessage(6);
- writer.Write(AmongUsClient.Instance.GameId);
- writer.WritePacked(guesser.OwnerId);
-
- for (var i = 0; i < alivePlayerControls.Length && (skipped ? i - 1 : i) < data.Length; i++)
- {
- string choice = data[skipped ? i - 1 : i].choice;
- string namePlateId = data[skipped ? i - 1 : i].namePlateId;
- PlayerControl pc = alivePlayerControls[i];
-
- if (pc.PlayerId == GuesserId)
+ IEnumerable namePlateIds = CurrentState switch
{
- skipped = true;
- continue;
- }
+ State.TeamSelection => ["nameplate_ripple", "nameplate_seeker", "nameplate_Polus_Lava", "nameplate_Celeste", "nameplate0001"],
+ _ => Enumerable.Repeat(CurrentTeam switch
+ {
+ CustomRoleTypes.Impostor => "nameplate_seeker",
+ CustomRoleTypes.Crewmate => "nameplate_ripple",
+ CustomRoleTypes.Neutral => "nameplate_Polus_Lava",
+ CustomRoleTypes.Coven => "nameplate_Celeste",
+ CustomRoleTypes.Addon => "nameplate0001",
+ _ => ""
+ }, 14)
+ };
- NetIdToRawDisplay[pc.NetId] = choice;
- string playerName = CurrentState == State.FirstLetterSelection ? choice : GetString(choice).ToUpper();
+ choices = choices.Prepend("Cancel");
+ namePlateIds = namePlateIds.Prepend("nameplate_candyCanePlate");
+
+ (string choice, string namePlateId)[] data = choices.Zip(namePlateIds, (choice, namePlateId) => (choice, namePlateId)).ToArray();
+ PlayerControl[] alivePlayerControls = Main.AllAlivePlayerControls;
+ int alivePlayerControlsLength = alivePlayerControls.Length - 1;
+
+ Logger.Info($"Set Up Meeting Shapeshift Menu For Guessing ({Main.AllPlayerNames.GetValueOrDefault(guesserId, "Someone")}, {CurrentState})", "Meeting Shapeshift For Guessing");
+
+ // First, use living players to show choices by changing their names
+ // The local player can't be used to show a choice (-1)
+
+ StringBuilder sb = new();
+ int textIndex = 0;
+
+ var skipped = false;
+ PlayerControl guesser = guesserId.GetPlayer();
+ MessageWriter writer = MessageWriter.Get(SendOption.Reliable);
+ writer.StartMessage(6);
+ writer.Write(AmongUsClient.Instance.GameId);
+ writer.WritePacked(guesser.OwnerId);
+
+ for (var i = 0; i < alivePlayerControls.Length && (skipped ? i - 1 : i) < data.Length; i++)
+ {
+ string choice = data[skipped ? i - 1 : i].choice;
+ string namePlateId = data[skipped ? i - 1 : i].namePlateId;
+ PlayerControl pc = alivePlayerControls[i];
+
+ if (pc.PlayerId == guesserId)
+ {
+ skipped = true;
+ continue;
+ }
+
+ NetIdToRawDisplay[pc.NetId] = choice;
+ string playerName = CurrentState == State.FirstLetterSelection ? choice : GetString(choice).ToUpper();
- sb.Append($"[{playerName}]");
- textIndex++;
-
- if (textIndex % 3 == 0) sb.AppendLine();
- else sb.Append(' ');
-
- if (writer.Length > 500)
- {
- writer.EndMessage();
- AmongUsClient.Instance.SendOrDisconnect(writer);
- writer.Clear(SendOption.Reliable);
- writer.StartMessage(6);
- writer.Write(AmongUsClient.Instance.GameId);
- writer.WritePacked(guesser.OwnerId);
- }
-
- writer.StartMessage(2);
- writer.WritePacked(pc.NetId);
- writer.Write((byte)RpcCalls.SetName);
- writer.Write(pc.Data.NetId);
- writer.Write(playerName);
- writer.Write(false);
- writer.EndMessage();
-
- writer.StartMessage(2);
- writer.WritePacked(pc.NetId);
- writer.Write((byte)RpcCalls.SetNamePlateStr);
- writer.Write(namePlateId);
- writer.Write(pc.GetNextRpcSequenceId(RpcCalls.SetNamePlateStr));
- writer.EndMessage();
- }
-
- writer.EndMessage();
- AmongUsClient.Instance.SendOrDisconnect(writer);
- writer.Recycle();
-
- // If there aren't enough living players, spawn new CNOs to show the rest of choices
-
- // Since CNOs use the local player's NetworkedPlayerInfo, and AU reads the player's name directly from it,
- // it's impossible to show vanilla players all choices accurately with CNOs.
- // No workaround found yet....
- // So we send the remaining choices in chat so the player can identify them
-
- if (data.Length >= alivePlayerControlsLength)
- {
- for (int i = alivePlayerControlsLength; i < data.Length; i++)
- {
- string choice = data[i].choice;
- string namePlateId = data[i].namePlateId;
-
- sb.Append($"[{(CurrentState == State.FirstLetterSelection ? choice : GetString(choice).ToUpper())}]");
+ sb.Append($"[{playerName}]");
textIndex++;
-
+
if (textIndex % 3 == 0) sb.AppendLine();
else sb.Append(' ');
-
- // If there's an existing CNO, reuse it
- ShapeshiftMenuElement cno;
- if (ExistingCNOs.Count + alivePlayerControlsLength > i)
- cno = ExistingCNOs[i - alivePlayerControlsLength];
- else
+ if (writer.Length > 500)
{
- cno = new ShapeshiftMenuElement(GuesserId);
- ExistingCNOs.Add(cno);
+ writer.EndMessage();
+ AmongUsClient.Instance.SendOrDisconnect(writer);
+ writer.Clear(SendOption.Reliable);
+ writer.StartMessage(6);
+ writer.Write(AmongUsClient.Instance.GameId);
+ writer.WritePacked(guesser.OwnerId);
}
-
- NetIdToRawDisplay[cno.playerControl.NetId] = choice;
- }
- // Despawn unused CNOs
- for (int i = data.Length - alivePlayerControlsLength; i < ExistingCNOs.Count; i++)
- ExistingCNOs[i].Despawn();
-
- ExistingCNOs.RemoveRange(data.Length - alivePlayerControlsLength, ExistingCNOs.Count - (data.Length - alivePlayerControlsLength));
-
- Logger.Info($"Sent {data.Length - alivePlayerControlsLength} CNOs, Reused {ExistingCNOs.Count} Existing CNOs", "Meeting Shapeshift For Guessing");
- }
- else
- {
- ExistingCNOs.ForEach(x => x.Despawn());
- ExistingCNOs.Clear();
- }
-
- Utils.SendMessage(sb.ToString().Trim(), GuesserId, GetString($"ShapeshiftGuesserUITitle.{CurrentState}"));
+ writer.StartMessage(2);
+ writer.WritePacked(pc.NetId);
+ writer.Write((byte)RpcCalls.SetName);
+ writer.Write(pc.Data.NetId);
+ writer.Write(playerName);
+ writer.Write(false);
+ writer.EndMessage();
- Logger.Info($"Spawned {ExistingCNOs.Count} CNOs, Used {alivePlayerControlsLength} Living Players, Showing {data.Length} Choices", "Meeting Shapeshift For Guessing");
+ writer.StartMessage(2);
+ writer.WritePacked(pc.NetId);
+ writer.Write((byte)RpcCalls.SetNamePlateStr);
+ writer.Write(namePlateId);
+ writer.Write(pc.GetNextRpcSequenceId(RpcCalls.SetNamePlateStr));
+ writer.EndMessage();
+ }
+
+ writer.EndMessage();
+ AmongUsClient.Instance.SendOrDisconnect(writer);
+ writer.Recycle();
+
+ // If there aren't enough living players, spawn new CNOs to show the rest of choices
+
+ // Since CNOs use the local player's NetworkedPlayerInfo, and AU reads the player's name directly from it,
+ // it's impossible to show vanilla players all choices accurately with CNOs.
+ // No workaround found yet....
+ // So we send the remaining choices in chat so the player can identify them
+
+ if (data.Length >= alivePlayerControlsLength)
+ {
+ for (int i = alivePlayerControlsLength; i < data.Length; i++)
+ {
+ string choice = data[i].choice;
+ string namePlateId = data[i].namePlateId;
+
+ sb.Append($"[{(CurrentState == State.FirstLetterSelection ? choice : GetString(choice).ToUpper())}]");
+ textIndex++;
+
+ if (textIndex % 3 == 0) sb.AppendLine();
+ else sb.Append(' ');
+
+ // If there's an existing CNO, reuse it
+ ShapeshiftMenuElement cno;
+
+ if (ExistingCNOs.Count + alivePlayerControlsLength > i)
+ cno = ExistingCNOs[i - alivePlayerControlsLength];
+ else
+ {
+ cno = new ShapeshiftMenuElement(guesserId);
+ ExistingCNOs.Add(cno);
+ }
+
+ NetIdToRawDisplay[cno.playerControl.NetId] = choice;
+ }
+
+ // Despawn unused CNOs
+ for (int i = data.Length - alivePlayerControlsLength; i < ExistingCNOs.Count; i++)
+ ExistingCNOs[i].Despawn();
+
+ ExistingCNOs.RemoveRange(data.Length - alivePlayerControlsLength, ExistingCNOs.Count - (data.Length - alivePlayerControlsLength));
+
+ Logger.Info($"Sent {data.Length - alivePlayerControlsLength} CNOs, Reused {ExistingCNOs.Count} Existing CNOs", "Meeting Shapeshift For Guessing");
+ }
+ else
+ {
+ ExistingCNOs.ForEach(x => x.Despawn());
+ ExistingCNOs.Clear();
+ }
+
+ Utils.SendMessage(sb.ToString().Trim(), guesserId, GetString($"ShapeshiftGuesserUITitle.{CurrentState}"));
+
+ Logger.Info($"Spawned {ExistingCNOs.Count} CNOs, Used {alivePlayerControlsLength} Living Players, Showing {data.Length} Choices", "Meeting Shapeshift For Guessing");
+ }
+ catch (Exception e) { Utils.ThrowException(e); }
}
// This problem goes beyond my ability to solve it perfectly, so I used AI
diff --git a/Modules/Utils.cs b/Modules/Utils.cs
index 89debaf76..15e6199c7 100644
--- a/Modules/Utils.cs
+++ b/Modules/Utils.cs
@@ -3675,9 +3675,6 @@ public static class Utils
case CustomRoles.Executioner when Executioner.Target.Remove(target.PlayerId):
Executioner.SendRPC(target.PlayerId);
break;
- case CustomRoles.Lawyer when Lawyer.Target.Remove(target.PlayerId):
- Lawyer.SendRPC(target.PlayerId);
- break;
case CustomRoles.Infection when !disconnect && !onMeeting:
Infection.OnPDdeath(targetRealKiller, target);
break;
diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs
index 6336e1425..5f26ae86c 100644
--- a/Patches/CheckGameEndPatch.cs
+++ b/Patches/CheckGameEndPatch.cs
@@ -202,7 +202,7 @@ internal static class GameEndChecker
ResetAndSetWinner(CustomWinner.Stalker);
WinnerIds.Add(pc.PlayerId);
break;
- case CustomRoles.Specter when pc.GetTaskState().RemainingTasksCount <= 0 && !pc.IsAlive() && !Options.PhantomSnatchesWin.GetBool():
+ case CustomRoles.Specter when pc.GetTaskState().RemainingTasksCount <= 0 && !pc.IsAlive() && WinnerTeam != CustomWinner.Specter:
WinnerIds.Add(pc.PlayerId);
AdditionalWinnerTeams.Add(AdditionalWinners.Specter);
break;
diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs
index bc8557e20..8c1d8d769 100644
--- a/Patches/MeetingHudPatch.cs
+++ b/Patches/MeetingHudPatch.cs
@@ -1272,7 +1272,7 @@ internal static class MeetingHudOnDestroyPatch
GuessManager.Data.Clear();
}
- AntiBlackout.SetOptimalRoleTypesToPreventBlackScreen();
+ AntiBlackout.SetOptimalRoleTypes();
RandomSpawn.CustomNetworkTransformHandleRpcPatch.HasSpawned.Clear();
Main.LastVotedPlayerInfo = null;
diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs
index 7c6c87d78..e20f5fbc5 100644
--- a/Patches/OnGameStartedPatch.cs
+++ b/Patches/OnGameStartedPatch.cs
@@ -960,6 +960,9 @@ internal static class StartGameHostPatch
if (state.SubRoles.Contains(CustomRoles.BananaMan))
Utils.RpcChangeSkin(state.Player, new());
+
+ if (state.SubRoles.Contains(CustomRoles.Venom))
+ state.SubRoles.FindAll(x => x.IncompatibleWithVenom()).ForEach(state.RemoveSubRole);
}
foreach (KeyValuePair pair in Main.PlayerStates)
diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs
index 66bdf2b4e..ad1c69465 100644
--- a/Patches/PlayerControlPatch.cs
+++ b/Patches/PlayerControlPatch.cs
@@ -2043,6 +2043,11 @@ internal static class FixedUpdatePatch
if (partnerPlayer == null) return;
Main.IsLoversDead = true;
+
+ if (Main.PlayerStates.TryGetValue(deathId, out var deadState) && deadState.deathReason == PlayerState.DeathReason.Disconnected) return;
+
+ var deadPlayer = deathId.GetPlayer();
+ if (deadPlayer == null || deadPlayer.Data == null || deadPlayer.Data.Disconnected) return;
if (Lovers.LoverDieConsequence.GetValue() == 2)
{
diff --git a/Patches/PlayerJoinAndLeftPatch.cs b/Patches/PlayerJoinAndLeftPatch.cs
index 38d713e4a..31c4f1352 100644
--- a/Patches/PlayerJoinAndLeftPatch.cs
+++ b/Patches/PlayerJoinAndLeftPatch.cs
@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
-using System.Threading.Tasks;
using AmongUs.Data;
using AmongUs.GameOptions;
using AmongUs.InnerNet.GameDataMessages;
@@ -48,27 +47,24 @@ internal static class OnGameJoinedPatch
Utils.DirtyName = [];
LateTask.New(Achievements.ShowWaitingAchievements, 8f, log: false);
-
+
if (!ClearedLogs)
{
LateTask.New(() =>
{
if (!HudManager.InstanceExists || AmongUsClient.Instance.IsGameStarted) return;
-
- var result = CleanOldItems();
-
+
+ (int Files, int Folders) result = CleanOldItems();
+
if (result.Files > 0 || result.Folders > 0)
{
Prompt.Show(string.Format(GetString("Promt.DeleteOldLogs"), result.Files, result.Folders), () =>
{
- LateTask.New(() =>
- {
- result = CleanOldItems(dryRun: false);
- HudManager.Instance.ShowPopUp(string.Format(GetString("LogDeletionResults"), result.Files, result.Folders));
- }, 0.5f, log: false);
+ result = CleanOldItems(false);
+ HudManager.Instance.ShowPopUp(string.Format(GetString("LogDeletionResults"), result.Files, result.Folders));
}, () => { });
}
-
+
ClearedLogs = true;
}, 5f, log: false);
}
@@ -89,7 +85,7 @@ internal static class OnGameJoinedPatch
LateTask.New(() =>
{
JoiningGame = false;
-
+
Options.AutoSetFactionMinMaxSettings();
if (BanManager.CheckEACList(PlayerControl.LocalPlayer.FriendCode, PlayerControl.LocalPlayer.GetClient().GetHashedPuid()) && GameStates.IsOnlineGame)
@@ -103,7 +99,7 @@ internal static class OnGameJoinedPatch
Main.Instance.StartCoroutine(OptionShower.GetText());
}, 1f, "OnGameJoinedPatch");
-
+
LateTask.New(() =>
{
if (Main.NormalOptions != null && Mathf.Approximately(Main.NormalOptions.KillCooldown, 25f))
@@ -148,7 +144,7 @@ internal static class OnGameJoinedPatch
ChatCommands.GameModePollCommand(PlayerControl.LocalPlayer, "/gmpoll", ["/gmpoll"]);
}
}
-
+
if (Options.AutoMPollCommandAfterJoin.GetBool() && !Options.RandomMapsMode.GetBool())
{
Main.Instance.StartCoroutine(CoRoutine());
@@ -201,7 +197,7 @@ internal static class OnGameJoinedPatch
catch (Exception e) { Utils.ThrowException(e); }
CustomGameMode nextGM = Options.AutoGMRotationCompiled[Options.AutoGMRotationIndex];
-
+
float timer;
if (nextGM != CustomGameMode.All) timer = 0f;
else if (Options.AutoGMPollCommandAfterJoin.GetBool()) timer = Options.AutoGMPollCommandCooldown.GetInt() - 10;
@@ -235,15 +231,16 @@ internal static class OnGameJoinedPatch
// Written with AI because I don't want it to delete the wrong files
///
- /// Cleans files and folders older than `days` in the EHR_Logs folder.
- /// Default: dryRun = true (shows what would be deleted).
+ /// Cleans files and folders older than `days` in the EHR_Logs folder.
+ /// Default: dryRun = true (shows what would be deleted).
///
private static (int Files, int Folders) CleanOldItems(bool dryRun = true, int days = 7)
{
#if ANDROID
- return (0, 0); // Not supported on Android
+ return (0, 0); // Not supported on Android
#endif
string path;
+
try
{
var f = $"{Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)}/EHR_Logs";
@@ -263,7 +260,8 @@ internal static class OnGameJoinedPatch
}
// Safety checks: ensure target folder name is exactly "EHR_Logs"
- var folderName = Path.GetFileName(path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
+ string folderName = Path.GetFileName(path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
+
if (!string.Equals(folderName, "EHR_Logs", StringComparison.OrdinalIgnoreCase))
{
Logger.Error($"[ERROR] Target folder name is '{folderName}' (expected 'EHR_Logs'). Aborting for safety.", "CleanOldItems");
@@ -276,90 +274,130 @@ internal static class OnGameJoinedPatch
return (0, 0);
}
- var threshold = DateTime.Now - TimeSpan.FromDays(days);
- Logger.Msg($"Threshold: delete items last written before {threshold:O}", "CleanOldItems");
+ DateTime thresholdUtc = DateTime.UtcNow - TimeSpan.FromDays(days);
+ Logger.Msg($"Threshold (UTC): delete items last written before {thresholdUtc:O}", "CleanOldItems");
Logger.Msg(dryRun
? "Running in dry run mode — no files or folders will be deleted."
: "Running for real — files and folders will be deleted.", "CleanOldItems");
- int filesDeleted = 0;
- int foldersDeleted = 0;
+ var filesDeleted = 0;
+ var foldersDeleted = 0;
var failedDeletes = new List();
// 1) Delete files older than threshold (walk all files recursively)
try
{
- foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
+ foreach (string file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
{
try
{
- DateTime lastWrite = File.GetLastWriteTime(file);
- if (lastWrite < threshold)
+ DateTime lastWriteUtc = File.GetLastWriteTimeUtc(file);
+
+ if (lastWriteUtc < thresholdUtc)
{
Logger.Warn(dryRun
- ? $"[DRY] Would delete file: {file} (LastWrite: {lastWrite:O})"
- : $"[DEL] Deleting file: {file} (LastWrite: {lastWrite:O})", "CleanOldItems");
+ ? $"[DRY] Would delete file: {file} (LastWriteUtc: {lastWriteUtc:O})"
+ : $"[DEL] Deleting file: {file} (LastWriteUtc: {lastWriteUtc:O})", "CleanOldItems");
if (!dryRun)
{
File.SetAttributes(file, FileAttributes.Normal); // remove read-only to avoid exceptions
File.Delete(file);
+ filesDeleted++;
+ }
+ else
+ {
+ // keep dry-run count behavior (so the user sees how many would be affected)
+ filesDeleted++;
}
- filesDeleted++;
}
}
catch (Exception exFile)
{
- string msg = $"Failed to delete file '{file}': {exFile.Message}";
+ var msg = $"Failed to delete file '{file}': {exFile.Message}";
Logger.Error(msg, "CleanOldItems");
failedDeletes.Add(msg);
// continue with other files
}
}
}
- catch (Exception exEnum)
- {
- Logger.Error($"Failed enumerating files: {exEnum.Message}", "CleanOldItems");
- }
+ catch (Exception exEnum) { Logger.Error($"Failed enumerating files: {exEnum.Message}", "CleanOldItems"); }
// 2) Attempt to delete directories that are empty AND older than threshold.
// We process directories from deepest to shallowest so we can remove empty parent dirs.
try
{
- var allDirectories = Directory
+ List allDirectories = Directory
.EnumerateDirectories(path, "*", SearchOption.AllDirectories)
- // order by path depth descending
+ // compute depth in a robust way: count directory separators for the platform
.OrderByDescending(d => d.Count(c => c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar))
.ToList();
- foreach (var dir in allDirectories)
+ foreach (string dir in allDirectories)
{
try
{
- // Skip if directory now contains any entries
- if (Directory.EnumerateFileSystemEntries(dir).Any())
- continue;
+ // re-check emptiness at time of delete: top-level only to avoid O(n^2)
+ bool hasEntries = Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.TopDirectoryOnly).Any();
- DateTime lastWrite = Directory.GetLastWriteTime(dir);
- DateTime creation = Directory.GetCreationTime(dir);
+ if (hasEntries)
+ {
+ // Diagnostic: list top-level entries so we can see hidden/system files
+ try
+ {
+ List entries = Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.TopDirectoryOnly).Take(10).ToList();
+ Logger.Msg($"Skipping non-empty dir: {dir}. Top-level entries (up to 10): {string.Join(", ", entries)}", "CleanOldItems");
+ }
+ catch { Logger.Msg($"Skipping non-empty dir: {dir}. Failed to list entries (possible permission issue).", "CleanOldItems"); }
+
+ continue;
+ }
+
+ DateTime lastWriteUtc = Directory.GetLastWriteTimeUtc(dir);
+ DateTime creationUtc = Directory.GetCreationTimeUtc(dir);
// Only delete if the directory is older than threshold by last write OR creation time (safer)
- if (lastWrite < threshold || creation < threshold)
+ if (lastWriteUtc < thresholdUtc || creationUtc < thresholdUtc)
{
Logger.Warn(dryRun
- ? $"[DRY] Would delete empty folder: {dir} (LastWrite: {lastWrite:O}, Creation: {creation:O})"
- : $"[DEL] Deleting empty folder: {dir} (LastWrite: {lastWrite:O}, Creation: {creation:O})", "CleanOldItems");
+ ? $"[DRY] Would delete empty folder: {dir} (LastWriteUtc: {lastWriteUtc:O}, CreationUtc: {creationUtc:O})"
+ : $"[DEL] Deleting empty folder: {dir} (LastWriteUtc: {lastWriteUtc:O}, CreationUtc: {creationUtc:O})", "CleanOldItems");
if (!dryRun)
{
- Directory.Delete(dir, false); // false -> directory must be empty
+ try
+ {
+ Directory.Delete(dir, false); // false -> directory must be empty
+ foldersDeleted++;
+ }
+ catch (Exception exDel)
+ {
+ var msg = $"Failed to delete directory '{dir}': {exDel.Message}";
+ Logger.Error(msg, "CleanOldItems");
+ failedDeletes.Add(msg);
+
+ // Diagnostic: if delete failed because directory not empty, log the top-level entries
+ try
+ {
+ List entries = Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.TopDirectoryOnly).Take(10).ToList();
+ Logger.Msg($"Directory '{dir}' appears non-empty during deletion. Top-level entries (up to 10): {string.Join(", ", entries)}", "CleanOldItems");
+ }
+ catch
+ {
+ /* ignore */
+ }
+ }
+ }
+ else
+ {
+ // dry-run: increment the folder count to show how many *would* be deleted
+ foldersDeleted++;
}
- foldersDeleted++;
}
}
catch (Exception exDir)
{
- string msg = $"Failed to delete directory '{dir}': {exDir.Message}";
+ var msg = $"Failed processing directory '{dir}': {exDir.Message}";
Logger.Error(msg, "CleanOldItems");
failedDeletes.Add(msg);
}
@@ -368,30 +406,24 @@ internal static class OnGameJoinedPatch
// Optionally, check the root target folder itself: if empty and old, you might want to delete it.
// Here we will NOT delete the root EHR_Logs folder itself to be extra safe.
}
- catch (Exception exEnumDirs)
- {
- Logger.Error($"Failed enumerating directories: {exEnumDirs.Message}", "CleanOldItems");
- }
+ catch (Exception exEnumDirs) { Logger.Error($"Failed enumerating directories: {exEnumDirs.Message}", "CleanOldItems"); }
// Summary
Logger.Msg("=== Summary ===", "CleanOldItems");
Logger.Msg($"Files matched and processed: {filesDeleted}", "CleanOldItems");
Logger.Msg($"Folders matched and processed: {foldersDeleted}", "CleanOldItems");
+
if (failedDeletes.Count > 0)
{
Logger.Msg($"Failures ({failedDeletes.Count}):", "CleanOldItems");
- foreach (var f in failedDeletes) Logger.Msg(f, "CleanOldItems");
+ foreach (string f in failedDeletes) Logger.Msg(f, "CleanOldItems");
}
else
- {
Logger.Msg("No failures reported.", "CleanOldItems");
- }
if (dryRun)
- {
Logger.Msg("Dry run complete.", "CleanOldItems");
- }
-
+
return (filesDeleted, foldersDeleted);
}
}
@@ -485,7 +517,7 @@ internal static class OnPlayerJoinedPatch
{
Main.SayStartTimes.Remove(client.Id);
Main.SayBanwordsTimes.Remove(client.Id);
-
+
if (GameStates.IsLobby && !OnGameJoinedPatch.JoiningGame)
LateTask.New(Options.AutoSetFactionMinMaxSettings, 2f, log: false);
}
@@ -502,7 +534,7 @@ internal static class OnPlayerLeftPatch
if (AmongUsClient.Instance.AmHost && GameStates.IsInGame && data != null && data.Character != null)
{
ExtendedPlayerControl.TempExiled.Remove(data.Character.PlayerId);
-
+
switch (Options.CurrentGameMode)
{
case CustomGameMode.HideAndSeek:
@@ -593,7 +625,7 @@ internal static class OnPlayerLeftPatch
Utils.DirtyName.Add(PlayerControl.LocalPlayer.PlayerId);
}, 2.5f, "Repeat Despawn", false);
}
-
+
if (GameStates.IsLobby)
Options.AutoSetFactionMinMaxSettings();
}
@@ -840,7 +872,7 @@ internal static class SetColorPatch
// Next 2: from https://github.com/EnhancedNetwork/TownofHost-Enhanced/blob/main/Patches/LobbyPatch.cs
[HarmonyPatch(typeof(PlayerMaterial), nameof(PlayerMaterial.SetColors), typeof(int), typeof(Material))]
-static class PlayerMaterialPatch
+internal static class PlayerMaterialPatch
{
public static void Prefix([HarmonyArgument(0)] ref int colorId)
{
@@ -850,15 +882,17 @@ static class PlayerMaterialPatch
}
[HarmonyPatch(typeof(NetworkedPlayerInfo), nameof(NetworkedPlayerInfo.Init))]
-static class NetworkedPlayerInfoInitPatch
+internal static class NetworkedPlayerInfoInitPatch
{
public static void Postfix(NetworkedPlayerInfo __instance)
{
- foreach (var outfit in __instance.Outfits)
+ foreach (Il2CppSystem.Collections.Generic.KeyValuePair outfit in __instance.Outfits)
{
if (outfit.Value != null)
+ {
if (outfit.Value.ColorId < 0 || outfit.Value.ColorId >= Palette.PlayerColors.Length)
outfit.Value.ColorId = 0;
+ }
}
}
}
\ No newline at end of file
diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json
index 327e08818..2de93178f 100644
--- a/Resources/Lang/en_US.json
+++ b/Resources/Lang/en_US.json
@@ -834,7 +834,7 @@
"JuggernautInfoLong": "(Neutrals):\\nAs the Juggernaut, your kill cooldown decreases with each kill you make.\\n\\nKill everyone to win.",
"VirusInfoLong": "(Neutrals):\\nThe virus' goal is to eliminate other players. When a crewmate is killed, their corpse is infected, and the reporting crewmate joins the virus team or dies if the Virus isn't voted out. The Virus team wins if they outnumber the Crewmate team.",
"PursuerInfoLong": "(Neutrals):\\nAs the Pursuer, you can use your ability on someone to make them shoot a blank when they try to kill (= reset their kill cooldown).\\n\\nTo win, survive to the end of the game.",
- "SpecterInfoLong": "(Neutrals):\\nAs the Specter, your job is to get killed and finish your tasks. You can do your tasks while alive. You cannot win if you're alive. If you get killed, you win with the winning team if your tasks are completed.",
+ "SpecterInfoLong": "(Neutrals):\\nAs the Specter, your job is to get killed and finish your tasks. You can do your tasks while alive. You cannot win if you're alive. If you complete all tasks while you're alive, then get killed, you win with the winning team at the end.",
"JinxInfoLong": "(Neutrals):\\nAs the Jinx, whenever you are attacked, you jinx them, resulting in them dying to a jinx. This has limited uses.\\n\\nKill off everyone to win.",
"MaverickInfoLong": "(Neutrals):\\nAs the Maverick, you can kill much like a Serial Killer. You win with the winning team rather than alone. If you die, you lose.",
"RitualistInfoLong": "(Neutrals):\\nAs the Ritualist, you can reveal the roles of other players using your kill button.\\n\\nSingle click: Reveal role\\nDouble click: Kill\\n\\nIf you run out of reveal uses, your kill button functions normally.",
@@ -2406,7 +2406,7 @@
"Message.YTPlanSelectFailed": "You cannot be assigned as {0}.\\nIt may be because you don't have this role enabled, or this role does not support being assigned.",
"Message.YTPlanCanNotFindRoleThePlayerEnter": "Could not find the role you searching\\nUse command /r to show role list",
"Message.YTPlanNotice": "Note: The [YouTuber Plan] is enabled in this lobby, which means the Host can specify their role in the next game to make it easier to get content. In case the Host abuses this feature, please exit the game or report it.\\nCurrent Creator Credentials:",
- "Warning.InvalidRpc": "Kicked {0} because an invalid RPC was received.\\nPlease check that no mods other than EHR are installed.",
+ "Warning.InvalidRpc": "Kicked {0} because an invalid RPC was received. Please check that no mods other than EHR are installed.",
"Warning.TooManyRPCs": "Kicked {0} because too many RPCs were received from them.",
"Warning.NoModHost": "EHR is not installed on the host",
"Warning.MismatchedVersion": "{0} has a different version than {1}",
diff --git a/Roles/Crewmate/Analyst.cs b/Roles/Crewmate/Analyst.cs
index ec9790c0d..6d3234b1f 100644
--- a/Roles/Crewmate/Analyst.cs
+++ b/Roles/Crewmate/Analyst.cs
@@ -119,7 +119,7 @@ internal class Analyst : RoleBase
public override void SetKillCooldown(byte id)
{
- Main.AllPlayerKillCooldown[id] = id.GetAbilityUseLimit() > 0 ? CD.GetFloat() : 300f;
+ Main.AllPlayerKillCooldown[id] = CD.GetFloat();
}
public static void OnAnyoneEnterVent(PlayerControl pc)
diff --git a/Roles/Crewmate/DonutDelivery.cs b/Roles/Crewmate/DonutDelivery.cs
index 669ed9245..845e8c3f5 100644
--- a/Roles/Crewmate/DonutDelivery.cs
+++ b/Roles/Crewmate/DonutDelivery.cs
@@ -84,7 +84,7 @@ public class DonutDelivery : RoleBase
public override void SetKillCooldown(byte playerId)
{
- Main.AllPlayerKillCooldown[playerId] = playerId.GetAbilityUseLimit() > 0 ? CD.GetFloat() : 300f;
+ Main.AllPlayerKillCooldown[playerId] = CD.GetFloat();
}
public override bool CanUseKillButton(PlayerControl pc)
diff --git a/Roles/Crewmate/Escort.cs b/Roles/Crewmate/Escort.cs
index 9ff796a13..e053a4123 100644
--- a/Roles/Crewmate/Escort.cs
+++ b/Roles/Crewmate/Escort.cs
@@ -53,7 +53,7 @@ public class Escort : RoleBase
public override void SetKillCooldown(byte playerId)
{
- Main.AllPlayerKillCooldown[playerId] = playerId.GetAbilityUseLimit() > 0 ? CD.GetFloat() : 300f;
+ Main.AllPlayerKillCooldown[playerId] = CD.GetFloat();
}
public override bool CanUseKillButton(PlayerControl pc)
diff --git a/Roles/Crewmate/Gaulois.cs b/Roles/Crewmate/Gaulois.cs
index a16a4d944..b1bca442d 100644
--- a/Roles/Crewmate/Gaulois.cs
+++ b/Roles/Crewmate/Gaulois.cs
@@ -58,7 +58,7 @@ public class Gaulois : RoleBase
{
if (!IsEnable) return;
- Main.AllPlayerKillCooldown[playerId] = playerId.GetAbilityUseLimit() > 0 ? CD.GetFloat() : 300f;
+ Main.AllPlayerKillCooldown[playerId] = CD.GetFloat();
}
public override bool CanUseKillButton(PlayerControl pc)
diff --git a/Roles/Crewmate/Medic.cs b/Roles/Crewmate/Medic.cs
index e69f0eae5..b81172a29 100644
--- a/Roles/Crewmate/Medic.cs
+++ b/Roles/Crewmate/Medic.cs
@@ -142,7 +142,7 @@ public class Medic : RoleBase
public override void SetKillCooldown(byte id)
{
- Main.AllPlayerKillCooldown[id] = CanUseKillButton(Utils.GetPlayerById(id)) ? CD.GetFloat() : 300f;
+ Main.AllPlayerKillCooldown[id] = CD.GetFloat();
}
public static bool InProtect(byte id)
diff --git a/Roles/Crewmate/Vigilante.cs b/Roles/Crewmate/Vigilante.cs
index d2665ff9c..63304a2c9 100644
--- a/Roles/Crewmate/Vigilante.cs
+++ b/Roles/Crewmate/Vigilante.cs
@@ -44,7 +44,7 @@ public class Vigilante : RoleBase
public override void SetKillCooldown(byte id)
{
- Main.AllPlayerKillCooldown[id] = IsKilled(id) ? 300f : KCD.GetFloat();
+ Main.AllPlayerKillCooldown[id] = KCD.GetFloat();
}
public override bool CanUseImpostorVentButton(PlayerControl pc)
diff --git a/Roles/Impostor/Bomber.cs b/Roles/Impostor/Bomber.cs
index 324f6b337..8bf37b614 100644
--- a/Roles/Impostor/Bomber.cs
+++ b/Roles/Impostor/Bomber.cs
@@ -83,7 +83,7 @@ internal class Bomber : RoleBase
public override void SetKillCooldown(byte id)
{
- Main.AllPlayerKillCooldown[id] = !IsNuker && BomberCanKill.GetBool() ? BomberKillCD.GetFloat() : 300f;
+ Main.AllPlayerKillCooldown[id] = BomberKillCD.GetFloat();
}
public override void ApplyGameOptions(IGameOptions opt, byte playerId)
diff --git a/Roles/Impostor/Sapper.cs b/Roles/Impostor/Sapper.cs
index a6679567d..87594e823 100644
--- a/Roles/Impostor/Sapper.cs
+++ b/Roles/Impostor/Sapper.cs
@@ -72,11 +72,6 @@ public class Sapper : RoleBase
}
}
- public override void SetKillCooldown(byte id)
- {
- Main.AllPlayerKillCooldown[id] = 300f;
- }
-
public override bool CanUseSabotage(PlayerControl pc)
{
return CanSabotage.GetBool();
diff --git a/Roles/Impostor/Silencer.cs b/Roles/Impostor/Silencer.cs
index 0f38653a8..534a16a26 100644
--- a/Roles/Impostor/Silencer.cs
+++ b/Roles/Impostor/Silencer.cs
@@ -78,7 +78,7 @@ public class Silencer : RoleBase
public override bool OnCheckMurder(PlayerControl killer, PlayerControl target)
{
- if (SilenceMode.GetValue() == 1 || ForSilencer.Count >= 1) return true;
+ if (SilenceMode.GetValue() >= 1 || ForSilencer.Count >= 1) return true;
return killer.CheckDoubleTrigger(target, () =>
{
diff --git a/Roles/Neutral/Cultist.cs b/Roles/Neutral/Cultist.cs
index 574dff0cf..52ee3c0cd 100644
--- a/Roles/Neutral/Cultist.cs
+++ b/Roles/Neutral/Cultist.cs
@@ -81,7 +81,7 @@ public class Cultist : RoleBase
public override void SetKillCooldown(byte id)
{
- Main.AllPlayerKillCooldown[id] = id.GetAbilityUseLimit() >= 1 ? CharmCooldown.GetFloat() + ((CharmMax.GetInt() - id.GetAbilityUseLimit()) * CharmCooldownIncrese.GetFloat()) : 300f;
+ Main.AllPlayerKillCooldown[id] = CharmCooldown.GetFloat() + ((CharmMax.GetInt() - id.GetAbilityUseLimit()) * CharmCooldownIncrese.GetFloat());
}
public override bool CanUseKillButton(PlayerControl player)
diff --git a/Roles/Neutral/Follower.cs b/Roles/Neutral/Follower.cs
index 7c3039de9..c02320075 100644
--- a/Roles/Neutral/Follower.cs
+++ b/Roles/Neutral/Follower.cs
@@ -105,12 +105,7 @@ public class Follower : RoleBase
public override void SetKillCooldown(byte id)
{
- if (BetTimes < 1)
- {
- Main.AllPlayerKillCooldown[id] = 300f;
- return;
- }
-
+ if (BetTimes < 1) return;
float cd = BetCooldown.GetFloat();
cd += (MaxBetTimes.GetInt() - BetTimes) * BetCooldownIncrese.GetFloat();
cd = Math.Min(cd, MaxBetCooldown.GetFloat());
diff --git a/Roles/Neutral/Lawyer.cs b/Roles/Neutral/Lawyer.cs
index 4cbf75272..357f559e7 100644
--- a/Roles/Neutral/Lawyer.cs
+++ b/Roles/Neutral/Lawyer.cs
@@ -65,29 +65,32 @@ public class Lawyer : RoleBase
PlayerIdList.Add(playerId);
LawyerId = playerId;
- try
+ LateTask.New(() =>
{
- if (AmongUsClient.Instance.AmHost)
+ try
{
- List targetList = [];
- targetList.AddRange(from target in Main.AllPlayerControls where playerId != target.PlayerId where CanTargetImpostor.GetBool() || !target.Is(CustomRoleTypes.Impostor) where CanTargetNeutralKiller.GetBool() || !target.IsNeutralKiller() where CanTargetCrewmate.GetBool() || !target.Is(CustomRoleTypes.Crewmate) where CanTargetCoven.GetBool() || !target.Is(CustomRoleTypes.Coven) where CanTargetJester.GetBool() || !target.Is(CustomRoles.Jester) where !target.Is(CustomRoleTypes.Neutral) || target.IsNeutralKiller() || target.Is(CustomRoles.Jester) where target.GetCustomRole() is not (CustomRoles.GM or CustomRoles.SuperStar or CustomRoles.Curser) where Main.LoversPlayers.TrueForAll(x => x.PlayerId != playerId) select target);
-
- if (targetList.Count == 0)
+ if (AmongUsClient.Instance.AmHost)
{
- ChangeRole(Utils.GetPlayerById(playerId));
- return;
+ List targetList = [];
+ targetList.AddRange(from target in Main.AllPlayerControls where playerId != target.PlayerId where CanTargetImpostor.GetBool() || !target.Is(CustomRoleTypes.Impostor) where CanTargetNeutralKiller.GetBool() || !target.IsNeutralKiller() where CanTargetCrewmate.GetBool() || !target.Is(CustomRoleTypes.Crewmate) where CanTargetCoven.GetBool() || !target.Is(CustomRoleTypes.Coven) where CanTargetJester.GetBool() || !target.Is(CustomRoles.Jester) where !target.Is(CustomRoleTypes.Neutral) || target.IsNeutralKiller() || target.Is(CustomRoles.Jester) where target.GetCustomRole() is not (CustomRoles.GM or CustomRoles.SuperStar or CustomRoles.Curser) where Main.LoversPlayers.TrueForAll(x => x.PlayerId != playerId) select target);
+
+ if (targetList.Count == 0)
+ {
+ ChangeRole(Utils.GetPlayerById(playerId));
+ return;
+ }
+
+ PlayerControl selectedTarget = targetList.RandomElement();
+ Target[playerId] = selectedTarget.PlayerId;
+ SendRPC(playerId, selectedTarget.PlayerId, "SetTarget");
+ Logger.Info($"{Utils.GetPlayerById(playerId)?.GetNameWithRole().RemoveHtmlTags()}:{selectedTarget.GetNameWithRole().RemoveHtmlTags()}", "Lawyer");
+
+ if (!TargetKnowsLawyer.GetBool()) return;
+ LateTask.New(() => selectedTarget.Notify(string.Format(Translator.GetString("YourLawyerIsNotify"), LawyerId.ColoredPlayerName())), 18f, log: false);
}
-
- PlayerControl selectedTarget = targetList.RandomElement();
- Target[playerId] = selectedTarget.PlayerId;
- SendRPC(playerId, selectedTarget.PlayerId, "SetTarget");
- Logger.Info($"{Utils.GetPlayerById(playerId)?.GetNameWithRole().RemoveHtmlTags()}:{selectedTarget.GetNameWithRole().RemoveHtmlTags()}", "Lawyer");
-
- if (!TargetKnowsLawyer.GetBool()) return;
- LateTask.New(() => selectedTarget.Notify(string.Format(Translator.GetString("YourLawyerIsNotify"), LawyerId.ColoredPlayerName())), 18f, log: false);
}
- }
- catch (Exception ex) { Utils.ThrowException(ex); }
+ catch (Exception ex) { Utils.ThrowException(ex); }
+ }, 0.5f, log: false);
}
public override void Remove(byte playerId)
@@ -96,7 +99,7 @@ public class Lawyer : RoleBase
Target.Remove(playerId);
}
- public static void SendRPC(byte lawyerId, byte targetId = 0x73, string Progress = "")
+ private static void SendRPC(byte lawyerId, byte targetId = 0x73, string Progress = "")
{
MessageWriter writer;
@@ -110,7 +113,6 @@ public class Lawyer : RoleBase
break;
case "":
if (!AmongUsClient.Instance.AmHost) return;
-
writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.RemoveLawyerTarget, SendOption.Reliable);
writer.Write(lawyerId);
AmongUsClient.Instance.FinishRpcImmediately(writer);