diff --git a/EHR.sln.DotSettings b/EHR.sln.DotSettings
index d1e7dca8a..4b5e2e1d3 100644
--- a/EHR.sln.DotSettings
+++ b/EHR.sln.DotSettings
@@ -82,6 +82,23 @@
True
True
False
+ True
+ True
+ True
+ Reformat into LateTask
+ True
+ constant("0.2f")
+ 0
+ True
+ True
+ 2.0
+ InCSharpFile
+ latetask
+ True
+ LateTask.New(() =>
+{
+ $SELECTION$
+}, $DELAY$);
True
True
True
@@ -100,7 +117,7 @@
$SELECTION$
}
catch { }
- constant("System.Exception")
+ suggestVariableType()
-1
-1
try
diff --git a/Main.cs b/Main.cs
index 93ed82ed1..8381585e8 100644
--- a/Main.cs
+++ b/Main.cs
@@ -41,8 +41,8 @@ public class Main : BasePlugin
private const string DebugKeyHash = "c0fd562955ba56af3ae20d7ec9e64c664f0facecef4b3e366e109306adeae29d";
private const string DebugKeySalt = "59687b";
public const string PluginGuid = "com.gurge44.endlesshostroles";
- public const string PluginVersion = "7.8.0";
- public const string PluginDisplayVersion = "7.8.0";
+ public const string PluginVersion = "7.9.0";
+ public const string PluginDisplayVersion = "7.9.0";
public const bool TestBuild = false;
public const string NeutralColor = "#ffab1b";
diff --git a/Modules/AntiBlackout.cs b/Modules/AntiBlackout.cs
index a2b6a9bfb..c0281de3b 100644
--- a/Modules/AntiBlackout.cs
+++ b/Modules/AntiBlackout.cs
@@ -117,7 +117,7 @@ public static class AntiBlackout
}
catch (Exception e) { Utils.ThrowException(e); }
}
- }, 0.2f, "Set Desync Roles");
+ }, 0.2f, "Set Desync Roles", log: false);
}
catch (Exception e) { Utils.ThrowException(e); }
}
diff --git a/Modules/CustomRpcSender.cs b/Modules/CustomRpcSender.cs
index 6d8580ad0..eff424052 100644
--- a/Modules/CustomRpcSender.cs
+++ b/Modules/CustomRpcSender.cs
@@ -166,36 +166,21 @@ public class CustomRpcSender
if (currentState == State.InRootPackedMessage)
{
- static int GetPackedIntSize(int value)
- {
- uint v = (uint)value;
- int count = 0;
-
- do
- {
- v >>= 7;
- count++;
- }
- while (v != 0);
-
- return count;
- }
-
- if (3 + GetPackedIntSize(AmongUsClient.Instance.GameId) >= stream.Length)
+ if (stream.Length <= 11)
dispose = true;
else
EndMessage();
}
- if (currentState != State.Ready)
+ if (!dispose && currentState != State.Ready)
{
if (currentState == State.Finished)
{
- Logger.Warn($"Tried to send RPC but \"{name}\" is already Finished", "CustomRpcSender.Warn");
+ Logger.Warn($"Tried to send Message but \"{name}\" is already Finished", "CustomRpcSender.Warn");
return;
}
- var errorMsg = $"Tried to send RPC but State is not Ready (in: \"{name}\", state: {currentState})";
+ var errorMsg = $"Tried to send Message but State is not Ready (in: \"{name}\", state: {currentState})";
if (isUnsafe)
Logger.Warn(errorMsg, "CustomRpcSender.Warn");
@@ -242,7 +227,7 @@ public class CustomRpcSender
{
if (currentState != State.InRpc)
{
- var errorMsg = $"Tried to write RPC, but State is not InRpc (in: \"{name}\")";
+ var errorMsg = $"Tried to write into RPC, but State is not InRpc (in: \"{name}\")";
if (isUnsafe)
Logger.Warn(errorMsg, "CustomRpcSender.Warn");
@@ -273,7 +258,7 @@ public class CustomRpcSender
if (currentState == State.InRootPackedMessage && targetClientId < 0)
{
- var errorMsg = $"Tried to start RPC, but State is InRootPackedMessage and the requested targetClientId is negative. Only GameDataTo messages can be started in this state. (in: \"{name}\")";
+ var errorMsg = $"Tried to start Message, but State is InRootPackedMessage and the requested targetClientId is negative. Only GameDataTo messages can be started in this state. (in: \"{name}\")";
if (isUnsafe)
Logger.Warn(errorMsg, "CustomRpcSender.Warn");
@@ -323,7 +308,7 @@ public class CustomRpcSender
{
if (currentState != State.Ready)
{
- var errorMsg = $"Tried to start Message but State is not Ready (in: \"{name}\")";
+ var errorMsg = $"Tried to start Packed Message but State is not Ready (in: \"{name}\")";
if (isUnsafe)
Logger.Warn(errorMsg, "CustomRpcSender.Warn");
@@ -351,7 +336,7 @@ public class CustomRpcSender
{
if (currentState is not State.InRootMessage and not State.InRootPackedMessage)
{
- var errorMsg = $"Tried to exit Message but State is not InRootMessage or InRootPackedMessage (in: \"{name}\")";
+ var errorMsg = $"Tried to end Message but State is not InRootMessage or InRootPackedMessage (in: \"{name}\")";
if (isUnsafe)
Logger.Warn(errorMsg, "CustomRpcSender.Warn");
@@ -418,8 +403,9 @@ public class CustomRpcSender
if (messages >= AmongUsClient.Instance.GetMaxMessagePackingLimit())
{
+ int targetClientId = currentRpcTarget;
EndMessage(startNew: true);
- StartMessage(currentRpcTarget);
+ StartMessage(targetClientId);
}
messages++;
@@ -436,7 +422,7 @@ public class CustomRpcSender
{
if (currentState != State.InRpc)
{
- var errorMsg = $"Tried to terminate RPC but State is not InRpc (in: \"{name}\")";
+ var errorMsg = $"Tried to end RPC but State is not InRpc (in: \"{name}\")";
if (isUnsafe)
Logger.Warn(errorMsg, "CustomRpcSender.Warn");
@@ -966,6 +952,28 @@ public static class CustomRpcSenderExtensions
return true;
}
+
+ public bool RpcExitVentDesync(PlayerPhysics physics, int ventId, PlayerControl seer)
+ {
+ if (!physics) return false;
+
+ int clientId = seer.OwnerId;
+
+ if (AmongUsClient.Instance.ClientId == clientId)
+ {
+ physics.StopAllCoroutines();
+ physics.StartCoroutine(physics.CoExitVent(ventId));
+ return false;
+ }
+
+ if (clientId < 0) return false;
+
+ sender.AutoStartRpc(physics.NetId, RpcCalls.ExitVent, clientId);
+ sender.WritePacked(ventId);
+ sender.EndRpc();
+
+ return true;
+ }
}
}
diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs
index a50595706..04f49d845 100644
--- a/Modules/ExtendedPlayerControl.cs
+++ b/Modules/ExtendedPlayerControl.cs
@@ -25,6 +25,7 @@ internal static class ExtendedPlayerControl
public static readonly HashSet CancelBlackScreenFix = [];
private static readonly List ResultBuffer = [];
public static readonly HashSet TempExiled = [];
+ public static readonly Dictionary NameWithRoleCache = [];
extension(PlayerControl player)
{
@@ -570,6 +571,8 @@ internal static class ExtendedPlayerControl
var players = Main.CachedAllPlayerControls();
+ HashSet<(PlayerControl target, RoleTypes roleType, int seerClientId)> setRoleCallsMap = [];
+
switch (oldRoleIsDesync, newRoleIsDesync)
{
// Desync role to normal role
@@ -591,7 +594,7 @@ internal static class ExtendedPlayerControl
// Set role type for seer
StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(seer.PlayerId, player.PlayerId)] = (rememberRoleType, newCustomRole);
- player.RpcSetRoleDesync(rememberRoleType, seerClientId);
+ setRoleCallsMap.Add((player, rememberRoleType, seerClientId));
if (self) continue;
@@ -609,13 +612,13 @@ internal static class ExtendedPlayerControl
if (!playerIsKiller && seer.Is(Team.Impostor)) rememberRoleType = RoleTypes.ImpostorGhost;
StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(player.PlayerId, seer.PlayerId)] = (seerCustomRole.IsDesyncRole() ? RoleTypes.Crewmate : seerRoleType, seerCustomRole);
- seer.RpcSetRoleDesync(rememberRoleType, player.OwnerId);
+ setRoleCallsMap.Add((seer, rememberRoleType, player.OwnerId));
continue;
}
// Set role type for player
StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(player.PlayerId, seer.PlayerId)] = (rememberRoleType, seerCustomRole);
- seer.RpcSetRoleDesync(rememberRoleType, player.OwnerId);
+ setRoleCallsMap.Add((seer, rememberRoleType, player.OwnerId));
}
break;
@@ -643,7 +646,7 @@ internal static class ExtendedPlayerControl
rememberRoleType = newRoleVN is CustomRoles.Noisemaker ? RoleTypes.Noisemaker : RoleTypes.Crewmate;
StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(seer.PlayerId, player.PlayerId)] = (rememberRoleType, newCustomRole);
- player.RpcSetRoleDesync(rememberRoleType, seerClientId);
+ setRoleCallsMap.Add((player, rememberRoleType, seerClientId));
if (self) continue;
@@ -656,13 +659,13 @@ internal static class ExtendedPlayerControl
rememberRoleType = RoleTypes.CrewmateGhost;
StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(player.PlayerId, seer.PlayerId)] = (seerCustomRole.GetVNRole() is CustomRoles.Noisemaker ? RoleTypes.Noisemaker : RoleTypes.Crewmate, seerCustomRole);
- seer.RpcSetRoleDesync(rememberRoleType, player.OwnerId);
+ setRoleCallsMap.Add((seer, rememberRoleType, player.OwnerId));
continue;
}
// Set role type for player
StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(player.PlayerId, seer.PlayerId)] = (rememberRoleType, seerCustomRole);
- seer.RpcSetRoleDesync(rememberRoleType, player.OwnerId);
+ setRoleCallsMap.Add((seer, rememberRoleType, player.OwnerId));
}
break;
@@ -685,12 +688,54 @@ internal static class ExtendedPlayerControl
rememberRoleType = newRoleType;
StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(seer.PlayerId, player.PlayerId)] = (rememberRoleType, newCustomRole);
- player.RpcSetRoleDesync(rememberRoleType, seerClientId);
+ setRoleCallsMap.Add((player, rememberRoleType, seerClientId));
}
break;
}
}
+
+ foreach (var targetGroup in setRoleCallsMap.GroupBy(x => x.target))
+ {
+ Dictionary counts = [];
+
+ RoleTypes globalRole = default;
+ int maxCount = 0;
+
+ foreach (var call in targetGroup)
+ {
+ int count = counts.TryGetValue(call.roleType, out int c) ? ++c : 1;
+
+ counts[call.roleType] = count;
+
+ if (count > maxCount)
+ {
+ maxCount = count;
+ globalRole = call.roleType;
+ }
+ }
+
+ int total = targetGroup.Count();
+
+ if (maxCount > 1 && 1 + total - maxCount < total)
+ {
+ targetGroup.Key.RpcSetRoleGlobal(globalRole);
+
+ LateTask.New(() =>
+ {
+ foreach (var call in targetGroup)
+ {
+ if (call.roleType != globalRole)
+ targetGroup.Key.RpcSetRoleDesync(call.roleType, call.seerClientId);
+ }
+ }, 0.2f);
+ }
+ else
+ {
+ foreach (var call in targetGroup)
+ targetGroup.Key.RpcSetRoleDesync(call.roleType, call.seerClientId);
+ }
+ }
if (loggerRoleMap)
{
@@ -1090,8 +1135,9 @@ internal static class ExtendedPlayerControl
{
try
{
+ if (NameWithRoleCache.TryGetValue(player?.PlayerId ?? 250, out string cache)) return cache;
bool addRoleName = GameStates.IsInGame && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.StopAndGo and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters and not CustomGameMode.RoomRush and not CustomGameMode.Quiz and not CustomGameMode.TheMindGame and not CustomGameMode.BedWars and not CustomGameMode.Deathrace and not CustomGameMode.Mingle and not CustomGameMode.Snowdown;
- return $"{player?.Data?.PlayerName}" + (addRoleName ? $" ({player?.GetAllRoleName(forUser).RemoveHtmlTags().Replace('\n', ' ')})" : string.Empty);
+ return NameWithRoleCache[player?.PlayerId ?? 250] = $"{player?.Data?.PlayerName}" + (addRoleName ? $" ({player?.GetAllRoleName(forUser).RemoveHtmlTags().Replace('\n', ' ')})" : string.Empty);
}
catch (Exception e)
{
diff --git a/Modules/GameState.cs b/Modules/GameState.cs
index f2b40ef8c..add42edbd 100644
--- a/Modules/GameState.cs
+++ b/Modules/GameState.cs
@@ -145,6 +145,8 @@ public class PlayerState(byte playerId)
MainRole = role;
Role.Add(PlayerId);
+
+ ExtendedPlayerControl.NameWithRoleCache.Remove(PlayerId);
Logger.Info($"ID {PlayerId} ({Player.GetRealName()}) => {role}, CountTypes => {countTypes}", "SetMainRole");
@@ -158,10 +160,7 @@ public class PlayerState(byte playerId)
if (role.IsVanilla() || role.IsVanillaEHR())
Main.AbilityUseLimit.Remove(PlayerId);
- }
-
- if (Main.IntroDestroyed && GameStates.InGame)
- {
+
if (PlayerId == PlayerControl.LocalPlayer.PlayerId && GameStates.IsInTask)
{
HudManager.Instance.SetHudActive(true);
@@ -239,6 +238,8 @@ public class PlayerState(byte playerId)
SetAddonCountTypes(role);
Logger.Info($" ID {PlayerId} ({Player?.GetRealName()}) => {role}, CountTypes => {countTypes}", "SetSubRole");
+
+ ExtendedPlayerControl.NameWithRoleCache.Remove(PlayerId);
}
private void SetAddonCountTypes(CustomRoles role)
@@ -361,6 +362,8 @@ public class PlayerState(byte playerId)
}
Utils.SendRPC(CustomRPC.RemoveSubRole, PlayerId, 1, (int)role);
+
+ ExtendedPlayerControl.NameWithRoleCache.Remove(PlayerId);
}
public void SetDead()
diff --git a/Patches/ChatControlPatch.cs b/Patches/ChatControlPatch.cs
index 8d219a6f5..74af61158 100644
--- a/Patches/ChatControlPatch.cs
+++ b/Patches/ChatControlPatch.cs
@@ -320,7 +320,7 @@ public static class ChatManager
if (!AmongUsClient.Instance.AmHost) return;
PlayerControl player = Main.AllAlivePlayerControlsCount == 0 ? PlayerControl.LocalPlayer : Main.CachedAlivePlayerControls().MinBy(x => x.PlayerId);
if (!player) return;
- if (targets.Count == 0 || targets.Count >= Main.AllAlivePlayerControlsCount) SendEmptyMessage(null);
+ if (targets == null || targets.Count == 0 || targets.Count >= Main.AllAlivePlayerControlsCount) SendEmptyMessage(null);
else targets.Do(SendEmptyMessage);
return;
diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs
index fd53c3fd0..c35e6424b 100644
--- a/Patches/OnGameStartedPatch.cs
+++ b/Patches/OnGameStartedPatch.cs
@@ -200,6 +200,7 @@ internal static class ChangeRoleSettings
ExtendedPlayerControl.TempExiled.Clear();
Utils.CachedRoleSettings.Clear();
MeetingHudCastVotePatch.ShouldCancelVoteList.Clear();
+ ExtendedPlayerControl.NameWithRoleCache.Clear();
MeetingTimeManager.Init();
Main.DefaultCrewmateVision = Main.RealOptionsData.GetFloat(FloatOptionNames.CrewLightMod);
diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs
index 40132e770..fa07d8b7e 100644
--- a/Patches/PlayerControlPatch.cs
+++ b/Patches/PlayerControlPatch.cs
@@ -1312,7 +1312,16 @@ internal static class ReportDeadBodyPatch
}); }
catch (Exception e) { ThrowException(e); }
- try { if (Lovers.PrivateChat.GetBool() && Main.LoversPlayers.Count > 0) ChatManager.ClearChat(Main.EnumerateAlivePlayerControls().ExceptBy(Main.LoversPlayers.ConvertAll(x => x.PlayerId), x => x.PlayerId).ToArray()); }
+ 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();
@@ -1391,18 +1400,21 @@ internal static class ReportDeadBodyPatch
QuizMaster.Data.NumMeetings++;
}
- if (Main.IsLoversDead && Lovers.LoverDieConsequence.GetValue() == 1 && Main.LoversPlayers.Exists(x => x && x.IsAlive()))
+ if (Main.IsLoversDead && Lovers.LoverDieConsequence.GetValue() == 1)
{
- PlayerControl aliveLover = Main.LoversPlayers.First(x => x && x.IsAlive());
+ PlayerControl aliveLover = Main.LoversPlayers.Find(x => x && x.IsAlive());
- switch (Lovers.LoverSuicideTime.GetValue())
+ if (aliveLover)
{
- case 1:
- aliveLover.Suicide(PlayerState.DeathReason.FollowingSuicide);
- break;
- case 2:
- CheckForEndVotingPatch.TryAddAfterMeetingDeathPlayers(PlayerState.DeathReason.FollowingSuicide, aliveLover.PlayerId);
- break;
+ switch (Lovers.LoverSuicideTime.GetValue())
+ {
+ case 1:
+ aliveLover.Suicide(PlayerState.DeathReason.FollowingSuicide);
+ break;
+ case 2:
+ CheckForEndVotingPatch.TryAddAfterMeetingDeathPlayers(PlayerState.DeathReason.FollowingSuicide, aliveLover.PlayerId);
+ break;
+ }
}
}
}
@@ -2065,8 +2077,19 @@ internal static class FixedUpdatePatch
if (!lowLoad)
{
- Main.LoversPlayers.RemoveAll(x => !x);
- if (!Main.HasJustStarted) Main.LoversPlayers.DoIf(x => !x.Is(CustomRoles.Lovers), x => x.RpcSetCustomRole(CustomRoles.Lovers));
+ 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()))
diff --git a/Roles/Standard/Crewmate/Power/Alchemist.cs b/Roles/Standard/Crewmate/Power/Alchemist.cs
index 8eaf3c6b2..0f87330f9 100644
--- a/Roles/Standard/Crewmate/Power/Alchemist.cs
+++ b/Roles/Standard/Crewmate/Power/Alchemist.cs
@@ -254,7 +254,10 @@ public class Alchemist : RoleBase
{
InvisTimer = null;
if (!pc || !pc.IsAlive()) return;
- Main.EnumeratePlayerControls().Without(pc).Do(x => instance.RpcExitVentDesync(ventId, x));
+ bool hasValue = false;
+ CustomRpcSender sender = CustomRpcSender.Create("Alchemist RpcExitVentDesync calls", SendOption.Reliable).StartPackedMessage();
+ Main.EnumeratePlayerControls().Without(pc).Do(x => hasValue |= sender.RpcExitVentDesync(instance, ventId, x));
+ sender.SendMessage(dispose: !hasValue);
pc.Notify(GetString("SwooperInvisStateOut"));
}, onTick: pc.IsModdedClient() ? null : () => pc.Notify(string.Format(GetString("SwooperInvisStateCountdown"), (int)Math.Ceiling(InvisTimer.Remaining.TotalSeconds)), overrideAll: true), onCanceled: () => InvisTimer = null);
diff --git a/Roles/Standard/Impostor/Concealing/Swooper.cs b/Roles/Standard/Impostor/Concealing/Swooper.cs
index e152f0df9..a0e9c9113 100644
--- a/Roles/Standard/Impostor/Concealing/Swooper.cs
+++ b/Roles/Standard/Impostor/Concealing/Swooper.cs
@@ -153,7 +153,7 @@ public class Swooper : RoleBase
if (!AmongUsClient.Instance.AmHost) return;
var player = SwooperId.GetPlayer();
- if (player == null || !player.IsAlive()) return;
+ if (!player || !player.IsAlive()) return;
if (!player.IsModdedClient() && !(UsedRole != CustomRoles.Chameleon && UsePhantomBasis.GetBool() && (UsedRole != CustomRoles.Wraith || UsePhantomBasisForNKs.GetBool())))
player.Notify(GetString("SwooperCanVent"), 10f);
@@ -162,7 +162,7 @@ public class Swooper : RoleBase
if (!AmongUsClient.Instance.AmHost) return;
var player = SwooperId.GetPlayer();
- if (player == null || !player.IsAlive()) return;
+ if (!player || !player.IsAlive()) return;
if (!(UsePhantomBasis.GetBool() && (UsedRole != CustomRoles.Wraith || UsePhantomBasisForNKs.GetBool())))
player.Notify(string.Format(GetString("CDPT"), (int)Math.Ceiling(CooldownTimer.Remaining.TotalSeconds)), 3f, true);
@@ -178,7 +178,12 @@ public class Swooper : RoleBase
InvisTimer = null;
if (UsedRole == CustomRoles.Chameleon && !UsePets.GetBool())
- Main.EnumeratePlayerControls().Without(player).Do(x => player.MyPhysics.RpcExitVentDesync(ventId, x));
+ {
+ bool hasValue = false;
+ CustomRpcSender sender = CustomRpcSender.Create("Swooper RpcExitVentDesync calls", SendOption.Reliable).StartPackedMessage();
+ Main.EnumeratePlayerControls().Without(player).Do(x => hasValue |= sender.RpcExitVentDesync(player.MyPhysics, ventId, x));
+ sender.SendMessage(dispose: !hasValue);
+ }
else
player.RpcMakeVisible(phantom: UsedRole == CustomRoles.Swooper);
@@ -268,7 +273,7 @@ public class Swooper : RoleBase
public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false)
{
- if (!hud || seer == null || seer.PlayerId != SwooperId || !GameStates.IsInTask || ExileController.Instance || !seer.IsAlive()) return string.Empty;
+ if (!hud || seer.PlayerId != SwooperId || meeting || !seer.IsAlive()) return string.Empty;
Suffix.Clear();