From f2ac62231861a2259adcf2becf20b34d4211a176 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Tue, 12 May 2026 14:33:58 +0200 Subject: [PATCH] Reduce allocations, CNO pooling, code improvements --- CTA/CustomTeamAssigner/CustomRoles.cs | 2 +- Gamemodes/CustomHnS.cs | 21 +- Gamemodes/HotPotato.cs | 10 +- Gamemodes/KingOfTheZones.cs | 102 ++++++--- Gamemodes/NaturalDisasters.cs | 215 +++++++++--------- Gamemodes/Quiz.cs | 2 +- Gamemodes/RoomRush.cs | 2 +- Gamemodes/TheMindGame.cs | 2 +- Modules/CustomNetObject.cs | 178 +++++++++------ Modules/CustomRoleSelector.cs | 4 +- Modules/CustomSabotage.cs | 4 +- Modules/ErrorText.cs | 2 + Modules/ExtendedPlayerControl.cs | 45 ++-- Modules/Extensions/CollectionExtensions.cs | 10 - Modules/GuessManager.cs | 66 +++--- Modules/LocateArrow.cs | 5 +- Modules/NameNotifyManager.cs | 22 +- Modules/TargetArrow.cs | 8 +- Modules/Utils.cs | 37 +-- Patches/HudPatch.cs | 2 +- Patches/IntroPatch.cs | 7 +- Patches/MeetingHudPatch.cs | 8 +- Patches/TextBoxPatch.cs | 6 +- Resources/Lang/en_US.jsonc | 1 - Roles/Standard/Addons/Common/Asthmatic.cs | 5 +- Roles/Standard/Addons/Impostor/Damocles.cs | 18 -- Roles/Standard/Crewmate/Chaos/Randomizer.cs | 13 +- Roles/Standard/Crewmate/Chaos/Tornado.cs | 9 +- Roles/Standard/Crewmate/Investigate/Druid.cs | 9 +- Roles/Standard/Crewmate/Investigate/Scout.cs | 12 +- .../Crewmate/Miscellaneous/PortalMaker.cs | 4 +- Roles/Standard/Crewmate/Support/Benefactor.cs | 9 +- Roles/Standard/Crewmate/Support/Crusader.cs | 3 +- .../Standard/Crewmate/Support/TimeManager.cs | 2 +- Roles/Standard/Ghost/Haunter.cs | 8 +- .../Standard/Impostor/Concealing/Anonymous.cs | 10 +- .../Standard/Impostor/Concealing/Lightning.cs | 12 +- Roles/Standard/Impostor/Concealing/Stealth.cs | 2 +- Roles/Standard/Impostor/Killing/Mafioso.cs | 39 +--- .../Impostor/Miscellaneous/EvilTracker.cs | 6 +- Roles/Standard/Impostor/Support/Devourer.cs | 8 +- Roles/Standard/Neutral/Evil/Workaholic.cs | 4 +- Roles/Standard/Neutral/Killing/HeadHunter.cs | 2 +- Roles/Standard/Neutral/Killing/Magician.cs | 31 ++- Roles/Standard/Neutral/Killing/Pelican.cs | 2 +- .../Standard/Neutral/Killing/SoulCollector.cs | 5 +- Roles/Standard/Neutral/Killing/Spider.cs | 5 +- 47 files changed, 539 insertions(+), 440 deletions(-) diff --git a/CTA/CustomTeamAssigner/CustomRoles.cs b/CTA/CustomTeamAssigner/CustomRoles.cs index d5da6ad29..c0de42c18 100644 --- a/CTA/CustomTeamAssigner/CustomRoles.cs +++ b/CTA/CustomTeamAssigner/CustomRoles.cs @@ -234,7 +234,6 @@ public enum CustomRoles Medic, Medium, Merchant, - Telecommunication, Mole, Monarch, Mortician, @@ -277,6 +276,7 @@ public enum CustomRoles Swapper, Tar, TaskManager, + Telecommunication, Telekinetic, Tether, TimeManager, diff --git a/Gamemodes/CustomHnS.cs b/Gamemodes/CustomHnS.cs index 76f6b6ba4..2acc56e7f 100644 --- a/Gamemodes/CustomHnS.cs +++ b/Gamemodes/CustomHnS.cs @@ -552,23 +552,24 @@ internal static class CustomHnS try { - var validIds = new HashSet(); - - foreach (var pc in Main.EnumeratePlayerControls()) - validIds.Add(pc.PlayerId); - - var toRemove = new List(); + List toRemove = null; foreach (var id in PlayerRoles.Keys) { - if (!validIds.Contains(id)) + if (!id.GetPlayer()) + { + toRemove ??= []; toRemove.Add(id); + } } - foreach (var id in toRemove) + if (toRemove != null) { - PlayerRoles.Remove(id); - Utils.SendRPC(CustomRPC.HNSSync, 2, id); + foreach (var id in toRemove) + { + PlayerRoles.Remove(id); + Utils.SendRPC(CustomRPC.HNSSync, 2, id); + } } } catch { } diff --git a/Gamemodes/HotPotato.cs b/Gamemodes/HotPotato.cs index 4d5430286..1fd04d040 100644 --- a/Gamemodes/HotPotato.cs +++ b/Gamemodes/HotPotato.cs @@ -109,8 +109,7 @@ internal static class HotPotato switch (reader.ReadPackedInt32()) { case 1: - int timeLeft = reader.ReadPackedInt32(); - HotPotatoState.TimeLeft = timeLeft; + HotPotatoState.TimeLeft = reader.ReadPackedInt32(); break; case 2: HotPotatoState.HolderID = reader.ReadByte(); @@ -119,6 +118,11 @@ internal static class HotPotato } } + public static void RecordDeath(byte id, int time) + { + SurvivalTimes[id] = time; + } + public static int GetKillInterval() { return Time.GetInt() + Main.CurrentMap switch @@ -159,7 +163,7 @@ internal static class HotPotato if (HotPotatoState.TimeLeft <= 0) { holder.Suicide(); - SurvivalTimes[HotPotatoState.HolderID] = (HotPotatoState.RoundNum - 1) * GetKillInterval(); + RecordDeath(HotPotatoState.HolderID, (HotPotatoState.RoundNum - 1) * GetKillInterval()); PassHotPotato(); if (holder.AmOwner) diff --git a/Gamemodes/KingOfTheZones.cs b/Gamemodes/KingOfTheZones.cs index 91bed6d33..3e0e30373 100644 --- a/Gamemodes/KingOfTheZones.cs +++ b/Gamemodes/KingOfTheZones.cs @@ -747,51 +747,81 @@ public static class KingOfTheZones } else { - List added = []; - List removed = []; - - foreach ((SystemTypes zone, long moveTS) in ZoneMoveSchedules) + if (atOnce) { - if (moveTS > now) continue; - - try + if (Zones.Count > 0 && ZoneMoveSchedules.TryGetValue(Zones[0], out long moveTS) && moveTS <= now) { - SystemTypes newZone = AllRooms.Except(Zones).RandomElement(); + IEnumerable newZones = AllRooms.Except(Zones).TakeRandom(Zones.Count); - int index = Zones.IndexOf(zone); - Zones.RemoveAt(index); - Zones.Insert(index, newZone); + Zones.Clear(); + ZoneDomination.Clear(); - added.Add(newZone); - - removed.Add(zone); - ZoneDomination.Remove(zone); - - Logger.Info($"Zone moved: {zone} => {newZone}", "KOTZ"); + foreach (SystemTypes newZone in newZones) + { + Zones.Add(newZone); + ZoneDomination[newZone] = KOTZTeam.None; + ZoneMoveSchedules[newZone] = now + moveTime + downtime; + if (downtime > 0) ZoneDowntimeExpire[newZone] = now + downtime; + } } - catch (Exception e) { Utils.ThrowException(e); } } - - removed.ForEach(x => ZoneMoveSchedules.Remove(x)); - - added.ForEach(x => + else { - ZoneMoveSchedules[x] = now + moveTime + downtime; - if (downtime > 0) ZoneDowntimeExpire[x] = now + downtime; - }); + SystemTypes? added = null; + SystemTypes? removed = null; + + foreach ((SystemTypes zone, long moveTS) in ZoneMoveSchedules) + { + if (moveTS > now) continue; + + try + { + SystemTypes newZone = AllRooms.Except(Zones).RandomElement(); + + int index = Zones.IndexOf(zone); + Zones.RemoveAt(index); + Zones.Insert(index, newZone); + + added = newZone; + removed = zone; + ZoneDomination.Remove(zone); + + Logger.Info($"Zone moved: {zone} => {newZone}", "KOTZ"); + break; + } + catch (Exception e) { Utils.ThrowException(e); } + } + + if (removed.HasValue) ZoneMoveSchedules.Remove(removed.Value); + + if (added.HasValue) + { + ZoneDomination[added.Value] = KOTZTeam.None; + ZoneMoveSchedules[added.Value] = now + moveTime + downtime; + if (downtime > 0) ZoneDowntimeExpire[added.Value] = now + downtime; + } + } } if (downtime > 0 && ZoneDowntimeExpire.Count > 0) { - List toRemove = []; - - foreach ((SystemTypes zone, long downtimeExpire) in ZoneDowntimeExpire) + if (atOnce) { - if (downtimeExpire > now) continue; - toRemove.Add(zone); + if (Zones.Count > 0 && ZoneDowntimeExpire.TryGetValue(Zones[0], out long expireTS) && expireTS <= now) + ZoneDowntimeExpire.Clear(); } + else + { + SystemTypes? toRemove = null; - toRemove.ForEach(x => ZoneDowntimeExpire.Remove(x)); + foreach ((SystemTypes zone, long downtimeExpire) in ZoneDowntimeExpire) + { + if (downtimeExpire > now) continue; + toRemove = zone; + } + + if (toRemove.HasValue) ZoneDowntimeExpire.Remove(toRemove.Value); + } } } } @@ -803,15 +833,16 @@ public static class KingOfTheZones { if (SpawnProtectionTimes.Count > 0) { - List toRemove = []; + List toRemove = null; foreach ((byte id, long protectionTS) in SpawnProtectionTimes) { if (protectionTS > now) continue; + toRemove ??= []; toRemove.Add(id); } - toRemove.ForEach(x => SpawnProtectionTimes.Remove(x)); + toRemove?.ForEach(x => SpawnProtectionTimes.Remove(x)); } } catch (Exception e) { Utils.ThrowException(e); } @@ -822,7 +853,7 @@ public static class KingOfTheZones { if (RespawnTimes.Count > 0) { - List toRemove = []; + List toRemove = null; foreach ((byte id, long respawnTS) in RespawnTimes) { @@ -846,12 +877,13 @@ public static class KingOfTheZones } catch (Exception e) { Utils.ThrowException(e); } + toRemove ??= []; toRemove.Add(id); Logger.Info($"{Main.AllPlayerNames[id]} respawned", "KOTZ"); } - toRemove.ForEach(x => RespawnTimes.Remove(x)); + toRemove?.ForEach(x => RespawnTimes.Remove(x)); } } catch (Exception e) { Utils.ThrowException(e); } diff --git a/Gamemodes/NaturalDisasters.cs b/Gamemodes/NaturalDisasters.cs index 9365c53c2..a46a450de 100644 --- a/Gamemodes/NaturalDisasters.cs +++ b/Gamemodes/NaturalDisasters.cs @@ -19,7 +19,7 @@ namespace EHR.Gamemodes; public static class NaturalDisasters { private const float Range = 1.5f; - private static List AllDisasters = []; + private static Dictionary AllDisasters = []; private static readonly List ActiveDisasters = []; private static readonly List PreparingDisasters = []; public static readonly Dictionary SurvivalTimes = []; @@ -38,7 +38,6 @@ public static class NaturalDisasters private static readonly string[] LimitReachedOptions = [ - "ND_LimitReachedOptions.OnlySpawnInstantDisasters", "ND_LimitReachedOptions.RemoveRandom", "ND_LimitReachedOptions.RemoveOldest" ]; @@ -55,9 +54,9 @@ public static class NaturalDisasters return ActiveDisasters; } - public static List GetAllDisasters() + public static IEnumerable GetAllDisasters() { - return AllDisasters; + return AllDisasters.Values; } public static int FrequencyOfDisasters => DisasterFrequency.GetInt(); @@ -90,7 +89,7 @@ public static class NaturalDisasters .SetParent(LimitMaximumDisastersAtOnce) .SetColor(color); - WhenLimitIsReached = new StringOptionItem(id++, "ND_WhenLimitIsReached", LimitReachedOptions, 2, TabGroup.GameSettings) + WhenLimitIsReached = new StringOptionItem(id++, "ND_WhenLimitIsReached", LimitReachedOptions, 1, TabGroup.GameSettings) .SetGameMode(gameMode) .SetParent(LimitMaximumDisastersAtOnce) .SetColor(color); @@ -110,20 +109,24 @@ public static class NaturalDisasters LoadAllDisasters(); - AllDisasters.ConvertAll(x => x.Name).ForEach(x => DisasterSpawnChances[x] = new IntegerOptionItem(id++, "ND_Disaster.SpawnChance", new(0, 100, 5), 50, TabGroup.GameSettings) - .SetGameMode(gameMode) - .SetColor(color) - .SetValueFormat(OptionFormat.Percent) - .AddReplacement(("{disaster}", Translator.GetString($"ND_{x}")))); + foreach ((string name, Type type) in AllDisasters) + { + DisasterSpawnChances[name] = new IntegerOptionItem(id++, "ND_Disaster.SpawnChance", new(0, 100, 5), 50, TabGroup.GameSettings) + .SetGameMode(gameMode) + .SetColor(color) + .SetHeader(true) + .SetValueFormat(OptionFormat.Percent) + .AddReplacement(("{disaster}", Translator.GetString($"ND_{name}"))); - AllDisasters.ForEach(x => x.GetMethod("SetupOwnCustomOption")?.Invoke(null, null)); + type.GetMethod("SetupOwnCustomOption")?.Invoke(null, null); + } } public static void LoadAllDisasters() { AllDisasters = Main.AllTypes .Where(x => x.IsClass && !x.IsAbstract && x.IsSubclassOf(typeof(Disaster))) - .ToList(); + .ToDictionary(x => x.Name, x => x); } public static void OnGameStart() @@ -164,26 +167,31 @@ public static class NaturalDisasters public static void RebuildSuffixText() { - var allRooms = ShipStatus.Instance.AllRooms; - var collapsedRooms = BuildingCollapse.CollapsedRooms; - string cb; - - if (allRooms.Count / 2 <= collapsedRooms.Count) + try { - SystemTypes[] remainingRooms = allRooms.Select(x => x.RoomId).Where(x => x is not (SystemTypes.Hallway or SystemTypes.Outside) && !x.ToString().Contains("Decontamination")).Except(collapsedRooms.ConvertAll(x => x.RoomId)).ToArray(); - cb = string.Format(Translator.GetString("AvailableBuildings"), remainingRooms.Length > 0 - ? remainingRooms.Select(x => Translator.GetString($"{x}")).Distinct().Join() - : $"<#ff0000>{Translator.GetString("None")}"); - } - else - { - cb = string.Format(Translator.GetString("CollapsedBuildings"), collapsedRooms.Count > 0 - ? collapsedRooms.Select(x => Translator.GetString($"{x.RoomId}")).Distinct().Join() - : Translator.GetString("None")); - } + var allRooms = ShipStatus.Instance.AllRooms; + var collapsedRooms = BuildingCollapse.CollapsedRooms; + string cb; - string ts = ActiveDisasters.Exists(x => x is Thunderstorm) ? $"\n{Translator.GetString("OngoingThunderstorm")}" : string.Empty; - SuffixText = $"{cb}{ts}"; + if (allRooms.Count / 2 <= collapsedRooms.Count) + { + SystemTypes[] remainingRooms = allRooms.Select(x => x.RoomId).Where(x => x is not (SystemTypes.Hallway or SystemTypes.Outside or SystemTypes.Decontamination2 or SystemTypes.Decontamination3)).Except(collapsedRooms.ConvertAll(x => x.RoomId)).ToArray(); + cb = string.Format(Translator.GetString("AvailableBuildings"), remainingRooms.Length > 0 + ? remainingRooms.Select(x => Translator.GetString($"{x}")).Distinct().Join() + : $"<#ff0000>{Translator.GetString("None")}"); + } + else + { + cb = string.Format(Translator.GetString("CollapsedBuildings"), collapsedRooms.Count > 0 + ? collapsedRooms.Select(x => Translator.GetString($"{x.RoomId}")).Distinct().Join() + : Translator.GetString("None")); + } + + string ts = ActiveDisasters.Exists(x => x is Thunderstorm) ? $"\n{Translator.GetString("OngoingThunderstorm")}" : string.Empty; + string rp = string.Format(Translator.GetString("ND_RemainingPlayers"), Utils.ColorString(Utils.GetRoleColor(CustomRoles.NDPlayer), Main.AllAlivePlayerControls.Count.ToString())); + SuffixText = $"{cb}{ts}\n{rp}"; + } + catch (Exception e) { Utils.ThrowException(e); } } public static int SurvivalTime(byte id) @@ -191,9 +199,13 @@ public static class NaturalDisasters return SurvivalTimes.GetValueOrDefault(id, 0); } + private static readonly List<(byte id, PlayerState.DeathReason deathReason, long recordTimeStamp)> DeathMessageQueue = []; + public static void RecordDeath(PlayerControl pc, PlayerState.DeathReason deathReason) { - SurvivalTimes[pc.PlayerId] = (int)(Utils.TimeStamp - IntroCutsceneDestroyPatch.IntroDestroyTS - FixedUpdatePatch.WaitTime); + RebuildSuffixText(); + long now = Utils.TimeStamp; + SurvivalTimes[pc.PlayerId] = (int)(now - IntroCutsceneDestroyPatch.IntroDestroyTS - FixedUpdatePatch.WaitTime); string message = Translator.GetString($"ND_DRLaughMessage-{IRandom.Instance.Next(4)}.{deathReason}"); Color color = DeathReasonColor(deathReason); @@ -201,10 +213,7 @@ public static class NaturalDisasters LateTask.New(() => pc.Notify(message, 20f), 1f, $"{pc.GetRealName()} died with the reason {deathReason}, survived for {SurvivalTime(pc.PlayerId)} seconds"); Utils.SendRPC(CustomRPC.NaturalDisastersSync, pc.PlayerId, SurvivalTimes[pc.PlayerId]); - var aapc = Main.AllAlivePlayerControls; - string remaining = string.Format(Translator.GetString("ND_RemainingPlayers"), Utils.ColorString(Utils.GetRoleColor(CustomRoles.NDPlayer), aapc.Count.ToString())); - string msgOthers = string.Format(Translator.GetString($"ND_DRLaughMessageOthers-{IRandom.Instance.Next(4)}.{deathReason}"), pc.PlayerId.ColoredPlayerName()); - aapc.NotifyPlayers($"<#ff0000>[╳] {Utils.ColorString(color, msgOthers)} {remaining}", 10f); + DeathMessageQueue.Add((pc.PlayerId, deathReason, now)); } private static Color DeathReasonColor(PlayerState.DeathReason deathReason) @@ -240,7 +249,6 @@ public static class NaturalDisasters public static class FixedUpdatePatch { public static readonly Stopwatch LastDisasterTimer = new(); - private static readonly Stopwatch FailSafeTimer = new(); private static long LastSync = Utils.TimeStamp; public static int WaitTime; @@ -269,46 +277,40 @@ public static class NaturalDisasters UpdatePreparingDisasters(); - for (var index = 0; index < ActiveDisasters.Count; index++) - { - if (ActiveDisasters[index].Update()) - index--; - } + for (int index = ActiveDisasters.Count - 1; index >= 0; index--) + ActiveDisasters[index].Update(); Sinkhole.OnFixedUpdate(); BuildingCollapse.OnFixedUpdate(); if (LimitMaximumDisastersAtOnce.GetBool()) { - int numDisasters = ActiveDisasters.Count + PreparingDisasters.Count + Sinkhole.Sinkholes.Count; + int numDisasters = ActiveDisasters.Count + Sinkhole.Sinkholes.Count; if (numDisasters > MaximumDisastersAtOnce.GetInt()) { - switch (WhenLimitIsReached.GetValue()) + if (Sinkhole.Sinkholes.Count > 0) { - case 0: + Sinkhole.RemoveRandomSinkhole(); + } + else + { + switch (WhenLimitIsReached.GetValue()) { - AllDisasters.RemoveAll(x => x.Name is "Earthquake" or "VolcanoEruption" or "Tornado" or "Thunderstorm" or "SandStorm" or "Tsunami"); - break; - } - case 1 or 2 when Sinkhole.Sinkholes.Count > 0: - { - Sinkhole.RemoveRandomSinkhole(); - break; - } - case 1: - { - Disaster remove = PreferRemovingThunderstorm.GetBool() && ActiveDisasters.FindFirst(x => x is Thunderstorm, out Disaster thunderstorm) ? thunderstorm : ActiveDisasters.RandomElement(); - remove?.Duration = 0; - remove?.RemoveIfExpired(); - break; - } - case 2: - { - Disaster oldest = ActiveDisasters.MinBy(x => x.StartTimeStamp); - oldest.Duration = 0; - oldest.RemoveIfExpired(); - break; + case 0: + { + Disaster remove = PreferRemovingThunderstorm.GetBool() && ActiveDisasters.FindFirst(x => x is Thunderstorm, out Disaster thunderstorm) ? thunderstorm : ActiveDisasters.RandomElement(); + remove?.Duration = 0; + remove?.RemoveIfExpired(); + break; + } + case 1: + { + Disaster oldest = ActiveDisasters.MinBy(x => x.StartTimeStamp); + oldest.Duration = 0; + oldest.RemoveIfExpired(); + break; + } } } } @@ -324,40 +326,36 @@ public static class NaturalDisasters if (LastDisasterTimer.Elapsed.TotalSeconds >= frequency) { LastDisasterTimer.Restart(); - List disasters = AllDisasters.SelectMany(x => Enumerable.Repeat(x, DisasterSpawnChances[x.Name].GetInt() / 5)).ToList(); + Dictionary pool = AllDisasters.ToDictionary(x => x.Key, x => x.Value); + if (ActiveDisasters.Exists(x => x is Thunderstorm)) - disasters.RemoveAll(x => x.Name == "Thunderstorm"); + pool.Remove("Thunderstorm"); + + (SystemTypes CollapsingRoom, Vector2 WarningPosition) buildingCollapseInfo = (default(SystemTypes), default(Vector2)); + PlainShipRoom[] nonCollapsedRooms = ShipStatus.Instance.AllRooms.Where(x => x.RoomId is not (SystemTypes.Hallway or SystemTypes.Outside or SystemTypes.Decontamination2 or SystemTypes.Decontamination3)).Except(BuildingCollapse.CollapsedRooms).ToArray(); - if ((Main.LIMap ? ShipStatus.Instance.AllRooms.Count : RandomSpawn.SpawnMap.GetSpawnMap().Positions.Count - (Main.CurrentMap == MapNames.Polus ? 3 : 1)) <= BuildingCollapse.CollapsedRooms.Count) - disasters.RemoveAll(x => x.Name == "BuildingCollapse"); + if (nonCollapsedRooms.Length == 0) + { + pool.Remove("BuildingCollapse"); + } + else + { + PlainShipRoom collapsingRoom = nonCollapsedRooms.RandomElement(); + buildingCollapseInfo = (collapsingRoom.RoomId, (Main.LIMap ? [] : RandomSpawn.SpawnMap.GetSpawnMap().Positions).TryGetValue(collapsingRoom.RoomId, out Vector2 spawnLocation) ? spawnLocation : collapsingRoom.transform.position); + } - Type disaster = disasters.RandomElement(); + Type disaster = pool.SelectMany(x => Enumerable.Repeat(x.Value, DisasterSpawnChances[x.Key].GetInt() / 5)).RandomElement(); if (disaster.Name == "Thunderstorm") { _ = new Thunderstorm(Vector2.zero, null); return; } - - KeyValuePair roomKvp; - - if (Main.LIMap) - { - var plainShipRoom = ShipStatus.Instance.AllRooms.Where(x => x.RoomId is not (SystemTypes.Hallway or SystemTypes.Outside) && !x.RoomId.ToString().Contains("Decontamination")).Except(BuildingCollapse.CollapsedRooms).RandomElement(); - roomKvp = new KeyValuePair(plainShipRoom.RoomId, plainShipRoom.transform.position); - } - else - { - roomKvp = RandomSpawn.SpawnMap.GetSpawnMap().Positions - .Where(x => x.Key is not (SystemTypes.Hallway or SystemTypes.Outside) && !x.Key.ToString().Contains("Decontamination")) - .ExceptBy(BuildingCollapse.CollapsedRooms.ConvertAll(x => x.RoomId), x => x.Key) - .RandomElement(); - } var aapc = Main.AllAlivePlayerControls; bool bc = disaster.Name == "BuildingCollapse"; - bool spawnOnPlayer = aapc.Count > 0 && DisasterSpawnMode.GetValue() switch + bool spawnOnPlayer = !bc && aapc.Count > 0 && DisasterSpawnMode.GetValue() switch { 0 => true, 1 => false, @@ -365,25 +363,13 @@ public static class NaturalDisasters }; Vector2 position = bc - ? roomKvp.Value + ? buildingCollapseInfo.WarningPosition : spawnOnPlayer ? aapc.RandomElement().Pos() - : RandomInRoomPosition(); + : new(Random.Range(MapBounds.X.Left, MapBounds.X.Right), Random.Range(MapBounds.Y.Bottom, MapBounds.Y.Top)); - SystemTypes? room = bc ? roomKvp.Key : null; + SystemTypes? room = bc ? buildingCollapseInfo.CollapsingRoom : null; AddPreparingDisaster(position, disaster.Name, room); - - static Vector2 RandomInRoomPosition() - { - FailSafeTimer.Start(); - Vector2 vector2; - - do vector2 = new(Random.Range(MapBounds.X.Left, MapBounds.X.Right), Random.Range(MapBounds.Y.Top, MapBounds.Y.Bottom)); - while (FailSafeTimer.ElapsedMilliseconds < 3 && !vector2.GetPlainShipRoom()); - - FailSafeTimer.Reset(); - return vector2; - } } if (now - LastSync >= 15) @@ -399,28 +385,36 @@ public static class NaturalDisasters LastSync = now; Utils.MarkEveryoneDirtySettings(); } + + if (DeathMessageQueue.Count > 0 && DeathMessageQueue.FindFirst(x => x.recordTimeStamp + 3 <= now, out var expired)) + { + var names = string.Join(", ", DeathMessageQueue.Where(x => x.deathReason == expired.deathReason).Select(x => x.id.ColoredPlayerName())); + string msgOthers = string.Format(Translator.GetString($"ND_DRLaughMessageOthers-{IRandom.Instance.Next(4)}.{expired.deathReason}"), names); + Main.EnumerateAlivePlayerControls().NotifyPlayers($"<#ff0000>[╳] {Utils.ColorString(DeathReasonColor(expired.deathReason), msgOthers)}"); + DeathMessageQueue.RemoveAll(x => x.deathReason == expired.deathReason); + } } public static void UpdatePreparingDisasters() { - for (var index = 0; index < PreparingDisasters.Count; index++) + for (var index = PreparingDisasters.Count - 1; index >= 0; index--) { NaturalDisaster naturalDisaster = PreparingDisasters[index]; naturalDisaster.Update(); - if (float.IsNaN(naturalDisaster.SpawnTimer)) + if (!naturalDisaster.SpawnTimer.IsRunning) { - Type type = AllDisasters.Find(d => d.Name == naturalDisaster.DisasterName); + Type type = AllDisasters[naturalDisaster.DisasterName]; Activator.CreateInstance(type, naturalDisaster.Position, naturalDisaster); PreparingDisasters.RemoveAt(index); - index--; } } } public static void AddPreparingDisaster(Vector2 position, string disasterName, SystemTypes? room) { - PreparingDisasters.Add(new(position, DisasterWarningTime.GetFloat(), Sprite(disasterName), disasterName, room)); + try { PreparingDisasters.Add(new(position, DisasterWarningTime.GetInt(), Sprite(disasterName), disasterName, room)); } + catch (Exception e) { Utils.ThrowException(e); } } } @@ -447,6 +441,9 @@ public static class NaturalDisasters case CustomGameMode.Mingle: Mingle.HandleDisconnect(); goto default; + case CustomGameMode.HotPotato: + HotPotato.RecordDeath(pc.PlayerId, (int)(Utils.TimeStamp - IntroCutsceneDestroyPatch.IntroDestroyTS - FixedUpdatePatch.WaitTime)); + goto default; default: pc.Suicide(deathReason); return; @@ -515,7 +512,6 @@ public static class NaturalDisasters Color color = Utils.GetRoleColor(CustomRoles.NDPlayer); DurationOpt = new IntegerOptionItem(id, "ND_Earthquake.DurationOpt", new(1, 120, 1), 30, TabGroup.GameSettings) - .SetHeader(true) .SetGameMode(CustomGameMode.NaturalDisasters) .SetColor(color) .SetValueFormat(OptionFormat.Seconds); @@ -612,7 +608,6 @@ public static class NaturalDisasters Color color = Utils.GetRoleColor(CustomRoles.NDPlayer); FlowStepDelay = new FloatOptionItem(id, "ND_VolcanoEruption.FlowStepDelay", new(0.5f, 5f, 0.5f), 1f, TabGroup.GameSettings) - .SetHeader(true) .SetGameMode(CustomGameMode.NaturalDisasters) .SetColor(color) .SetValueFormat(OptionFormat.Seconds); @@ -679,7 +674,6 @@ public static class NaturalDisasters Color color = Utils.GetRoleColor(CustomRoles.NDPlayer); DurationOpt = new IntegerOptionItem(id, "ND_Tornado.DurationOpt", new(1, 120, 1), 20, TabGroup.GameSettings) - .SetHeader(true) .SetGameMode(CustomGameMode.NaturalDisasters) .SetColor(color) .SetValueFormat(OptionFormat.Seconds); @@ -789,7 +783,6 @@ public static class NaturalDisasters Color color = Utils.GetRoleColor(CustomRoles.NDPlayer); DurationOpt = new IntegerOptionItem(id, "ND_Thunderstorm.DurationOpt", new(1, 120, 1), 20, TabGroup.GameSettings) - .SetHeader(true) .SetGameMode(CustomGameMode.NaturalDisasters) .SetColor(color) .SetValueFormat(OptionFormat.Seconds); @@ -864,7 +857,6 @@ public static class NaturalDisasters Color color = Utils.GetRoleColor(CustomRoles.NDPlayer); DurationOpt = new IntegerOptionItem(id, "ND_SandStorm.DurationOpt", new(1, 120, 1), 30, TabGroup.GameSettings) - .SetHeader(true) .SetGameMode(CustomGameMode.NaturalDisasters) .SetColor(color) .SetValueFormat(OptionFormat.Seconds); @@ -1021,7 +1013,6 @@ public static class NaturalDisasters Color color = Utils.GetRoleColor(CustomRoles.NDPlayer); MovingSpeed = new FloatOptionItem(id, "ND_Tsunami.MovingSpeed", new(0.25f, 10f, 0.25f), 2f, TabGroup.GameSettings) - .SetHeader(true) .SetGameMode(CustomGameMode.NaturalDisasters) .SetColor(color) .SetValueFormat(OptionFormat.Multiplier); diff --git a/Gamemodes/Quiz.cs b/Gamemodes/Quiz.cs index 8ec0a8d50..460443175 100644 --- a/Gamemodes/Quiz.cs +++ b/Gamemodes/Quiz.cs @@ -502,7 +502,7 @@ public static class Quiz var spectators = aapc.ExceptBy(dyingPlayers, x => x.PlayerId).ToArray(); spectators.Do(x => x.ExileTemporarily()); yield return new WaitForSecondsRealtime(0.2f); - var stillLiving = dyingPlayers.ToValidPlayers().FindAll(x => x.IsAlive()); + var stillLiving = dyingPlayers.ToValidPlayers().Where(x => x.IsAlive()).ToList(); stillLiving.ForEach(x => x.RpcChangeRoleBasis(CustomRoles.SerialKiller)); Utils.SendRPC(CustomRPC.QuizSync, AllowKills); diff --git a/Gamemodes/RoomRush.cs b/Gamemodes/RoomRush.cs index eab7ab3c3..e8b4b2caf 100644 --- a/Gamemodes/RoomRush.cs +++ b/Gamemodes/RoomRush.cs @@ -36,7 +36,7 @@ public static class RoomRush private static HashSet DonePlayers = []; private static bool GameGoing; - private static DateTime GameStartDateTime; + public static DateTime GameStartDateTime; private static RandomSpawn.SpawnMap Map; diff --git a/Gamemodes/TheMindGame.cs b/Gamemodes/TheMindGame.cs index 1e98f768d..a26cf60d4 100644 --- a/Gamemodes/TheMindGame.cs +++ b/Gamemodes/TheMindGame.cs @@ -613,7 +613,7 @@ public static class TheMindGame Round = 5; HiddenPoints.Clear(); - EjectedPlayers.ToValidPlayers().FindAll(x => !x.IsAlive()).ForEach(x => x.RpcRevive()); + EjectedPlayers.ToValidPlayers().Where(x => !x.IsAlive()).Do(x => x.RpcRevive()); yield return NotifyEveryone("TMG.Notify.Round4End", 3); if (Stop) yield break; diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs index a475695de..c5acc62a7 100644 --- a/Modules/CustomNetObject.cs +++ b/Modules/CustomNetObject.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using AmongUs.InnerNet.GameDataMessages; using EHR.Gamemodes; @@ -28,13 +29,15 @@ namespace EHR private string Sprite; private int SnapToSendFrameCount; + + private bool IsPooled; public void RpcChangeSprite(string sprite) { if (!AmongUsClient.Instance.AmHost) return; - if (this is not NaturalDisaster nd || nd.SpawnTimer <= 0f) Logger.Info($" Change Custom Net Object {GetType().Name} (ID {Id}) sprite", "CNO.RpcChangeSprite"); + if (this is not NaturalDisaster nd || !nd.SpawnTimer.IsRunning) Logger.Info($" Change Custom Net Object {GetType().Name} (ID {Id}) sprite", "CNO.RpcChangeSprite"); - bool notImportant = this is BedWarsItemGenerator || (this is NaturalDisaster { SpawnTimer: > 1f } && Options.CurrentGameMode != CustomGameMode.NaturalDisasters && GameStates.CurrentServerType == GameStates.ServerType.Vanilla); + bool notImportant = this is BedWarsItemGenerator || (this is NaturalDisaster n && n.SpawnTimer.Elapsed.TotalSeconds < n.TotalWarningTime - 1 && Options.CurrentGameMode != CustomGameMode.NaturalDisasters && GameStates.CurrentServerType == GameStates.ServerType.Vanilla); SendOption channel = notImportant ? SendOption.None : SendOption.Reliable; DataFlagRateLimiter.Enqueue(() => @@ -92,15 +95,55 @@ namespace EHR public void TP(Vector2 position) { Position = position; + SnapToSendFrameCount = 30; + } + + private bool TryReusePooledObject(string sprite, Vector2 position) + { + try + { + if (this is not NaturalDisaster) return false; + + CustomNetObject pooled = AllObjects.Find(x => x.IsPooled && x.playerControl); + if (pooled == null) return false; + + Logger.Info($" Reusing pooled Custom Net Object NaturalDisaster (ID {pooled.Id})", "CNO.CreateNetObject"); + + playerControl = pooled.playerControl; + Id = pooled.Id; + IsPooled = false; + + AllObjects.Remove(pooled); + AllObjects.Add(this); + + TP(position); + RpcChangeSprite(sprite); + + return true; + } + catch (Exception e) + { + Utils.ThrowException(e); + return false; + } } - public void Despawn() + public void Despawn(bool canPool = true) { if (!AmongUsClient.Instance.AmHost) return; - Logger.Info($" Despawn Custom Net Object {GetType().Name} (ID {Id})", "CNO.Despawn"); try { + if (canPool && this is NaturalDisaster && playerControl) + { + Logger.Info($" Pooled Custom Net Object {GetType().Name} (ID {Id})", "CNO.Despawn"); + IsPooled = true; + TP(new Vector2(50f, 50f)); + return; + } + + Logger.Info($" Despawn Custom Net Object {GetType().Name} (ID {Id})", "CNO.Despawn"); + if (playerControl) { MessageWriter writer = MessageWriter.Get(SendOption.Reliable); @@ -168,7 +211,7 @@ namespace EHR return false; } - if (this is not ShapeshiftMenuElement) + if (this is not ShapeshiftMenuElement && player.IsNonHostModdedClient()) { LateTask.New(() => { @@ -220,7 +263,7 @@ namespace EHR protected void CreateNetObject(string sprite, Vector2 position) { - if (GameStates.IsEnded || !AmongUsClient.Instance.AmHost) return; + if (GameStates.IsEnded || !AmongUsClient.Instance.AmHost || TryReusePooledObject(sprite, position)) return; Logger.Info($" Create Custom Net Object {GetType().Name} (ID {MaxId + 1}) at {position} - Time since game start: {Utils.TimeStamp - IntroCutsceneDestroyPatch.IntroDestroyTS}s", "CNO.CreateNetObject"); Main.Instance.StartCoroutine(CoRoutine()); @@ -303,50 +346,53 @@ namespace EHR qa = DataFlagRateLimiter.Enqueue(() => { - int messages = 0; - int packingLimit = AmongUsClient.Instance.GetMaxMessagePackingLimit(); - MessageWriter stream = MessageWriter.Get(SendOption.Reliable); - stream.StartMessage(26); - stream.WritePacked(AmongUsClient.Instance.GameId); - - foreach (PlayerControl pc in Main.EnumeratePlayerControls()) + if (PlayerControl.AllPlayerControls.Count > 1) { - if (pc.AmOwner) continue; + int messages = 0; + int packingLimit = AmongUsClient.Instance.GetMaxMessagePackingLimit(); + MessageWriter stream = MessageWriter.Get(SendOption.Reliable); + stream.StartMessage(26); + stream.WritePacked(AmongUsClient.Instance.GameId); - if (stream.Length > 500 || messages + 3 > packingLimit) + foreach (PlayerControl pc in Main.EnumeratePlayerControls()) { + if (pc.AmOwner) continue; + + if (stream.Length > 500 || messages + 3 > packingLimit) + { + stream.EndMessage(); + AmongUsClient.Instance.SendOrDisconnect(stream); + stream.Clear(SendOption.Reliable); + stream.StartMessage(26); + stream.WritePacked(AmongUsClient.Instance.GameId); + } + + stream.StartMessage(6); + stream.Write(AmongUsClient.Instance.GameId); + stream.WritePacked(pc.OwnerId); + stream.StartMessage(1); + stream.WritePacked(playerControl.NetId); + stream.Write(pc.PlayerId); stream.EndMessage(); - AmongUsClient.Instance.SendOrDisconnect(stream); - stream.Clear(SendOption.Reliable); - stream.StartMessage(26); - stream.WritePacked(AmongUsClient.Instance.GameId); + stream.StartMessage(2); + stream.WritePacked(playerControl.NetId); + stream.Write((byte)RpcCalls.MurderPlayer); + stream.WriteNetObject(playerControl); + stream.Write((int)MurderResultFlags.FailedError); + stream.EndMessage(); + stream.StartMessage(1); + stream.WritePacked(playerControl.NetId); + stream.Write((byte)254); + stream.EndMessage(); + stream.EndMessage(); + + messages += 3; } - stream.StartMessage(6); - stream.Write(AmongUsClient.Instance.GameId); - stream.WritePacked(pc.OwnerId); - stream.StartMessage(1); - stream.WritePacked(playerControl.NetId); - stream.Write(pc.PlayerId); stream.EndMessage(); - stream.StartMessage(2); - stream.WritePacked(playerControl.NetId); - stream.Write((byte)RpcCalls.MurderPlayer); - stream.WriteNetObject(playerControl); - stream.Write((int)MurderResultFlags.FailedError); - stream.EndMessage(); - stream.StartMessage(1); - stream.WritePacked(playerControl.NetId); - stream.Write((byte)254); - stream.EndMessage(); - stream.EndMessage(); - - messages += 3; + AmongUsClient.Instance.SendOrDisconnect(stream); + stream.Recycle(); } - - stream.EndMessage(); - AmongUsClient.Instance.SendOrDisconnect(stream); - stream.Recycle(); playerControl.CachedPlayerData = PlayerControl.LocalPlayer.Data; }, calls: 2); // WAITING FOR THE SLOTHS TO VERIFY THIS @@ -441,20 +487,20 @@ namespace EHR public static void FixedUpdate() { - foreach (CustomNetObject cno in AllObjects.ToArray()) - cno?.OnFixedUpdate(); + for (int index = AllObjects.Count - 1; index >= 0; index--) + AllObjects[index]?.OnFixedUpdate(); } public static CustomNetObject Get(int id) { - return AllObjects.FirstOrDefault(x => x.Id == id); + return AllObjects.Find(x => x.Id == id); } public static void Reset() { try { - AllObjects.ToArray().Do(x => x.Despawn()); + AllObjects.ToArray().Do(x => x.Despawn(canPool: false)); AllObjects.Clear(); } catch (Exception e) { Utils.ThrowException(e); } @@ -607,51 +653,56 @@ namespace EHR public sealed class NaturalDisaster : CustomNetObject { - public NaturalDisaster(Vector2 position, float time, string sprite, string disasterName, SystemTypes? room) + public NaturalDisaster(Vector2 position, int time, string sprite, string disasterName, SystemTypes? room) { string name = Translator.GetString($"ND_{disasterName}"); - string warning = $"{Math.Ceiling(time):N0}\n{name}"; + string warning = $"{time}\n{name}"; if (room.HasValue) { warning = $"<#ff4444>{warning}"; - Main.EnumerateAlivePlayerControls().DoIf(x => x.IsInRoom(room.Value), x => x.ReactorFlash()); + + try { Main.EnumerateAlivePlayerControls().DoIf(x => x.IsInRoom(room.Value), x => x.ReactorFlash()); } + catch (Exception e) { Utils.ThrowException(e); } } - SpawnTimer = time; + TotalWarningTime = time; DisasterSprite = sprite; DisasterName = disasterName; DisasterNameTranslated = name; Room = room; + + SpawnTimer = Stopwatch.StartNew(); CreateNetObject(warning, position); } public SystemTypes? Room { get; } public string DisasterName { get; } - public float SpawnTimer { get; private set; } + public Stopwatch SpawnTimer { get; } + public int TotalWarningTime { get; } private string DisasterSprite { get; } - private int TimeInt => (int)Math.Ceiling(SpawnTimer); + private int TimeInt => (int)(SpawnTimer.Elapsed.TotalSeconds); + private int PreviousTimeInt { get; set; } private string DisasterNameTranslated { get; } public void Update() { - if (float.IsNaN(SpawnTimer)) return; + if (!SpawnTimer.IsRunning) return; + + int newTime = TimeInt; - int oldTime = TimeInt; - SpawnTimer -= Time.fixedDeltaTime; - - if (SpawnTimer <= 0f) + if (newTime >= TotalWarningTime) { + SpawnTimer.Stop(); if (!Room.HasValue) RpcChangeSprite(DisasterSprite); - SpawnTimer = float.NaN; } else { - int newTime = TimeInt; - if (oldTime == newTime) return; - string warning = $"{newTime:N0}\n{DisasterNameTranslated}"; + if (PreviousTimeInt == newTime) return; + PreviousTimeInt = newTime; + string warning = $"{TotalWarningTime - newTime}\n{DisasterNameTranslated}"; if (Room.HasValue) warning = $"<#ff4444>{warning}"; RpcChangeSprite(warning); } @@ -660,7 +711,7 @@ namespace EHR internal sealed class Lightning : CustomNetObject { - private float Timer = 5f; + private readonly Stopwatch Timer = Stopwatch.StartNew(); public Lightning(Vector2 position) { @@ -670,8 +721,7 @@ namespace EHR protected override void OnFixedUpdate() { base.OnFixedUpdate(); - Timer -= Time.fixedDeltaTime; - if (Timer <= 0) Despawn(); + if (Timer.Elapsed.TotalSeconds >= 3f) Despawn(); } } diff --git a/Modules/CustomRoleSelector.cs b/Modules/CustomRoleSelector.cs index 97a111ecb..8939bba8a 100644 --- a/Modules/CustomRoleSelector.cs +++ b/Modules/CustomRoleSelector.cs @@ -81,7 +81,7 @@ internal static class CustomRoleSelector Dictionary> roles = []; Enum.GetValues().Do(x => roles[x] = []); - foreach (byte id in Main.SetRoles.Keys.Where(id => Utils.GetPlayerById(id) == null).ToArray()) Main.SetRoles.Remove(id); + foreach (byte id in Main.SetRoles.Keys.Where(id => !Utils.GetPlayerById(id)).ToArray()) Main.SetRoles.Remove(id); (bool Spawning, bool OneIsImp) loversData = (Lovers.LegacyLovers.GetBool(), rd.Next(100) < Lovers.LovingImpostorSpawnChance.GetInt()); loversData.Spawning &= rd.Next(100) < Options.CustomAdtRoleSpawnRate[CustomRoles.Lovers].GetInt(); @@ -559,7 +559,7 @@ internal static class CustomRoleSelector { if (Options.CurrentGameMode != CustomGameMode.Standard) return; - foreach (byte id in Main.SetAddOns.Keys.Where(id => Utils.GetPlayerById(id) == null).ToArray()) Main.SetAddOns.Remove(id); + foreach (byte id in Main.SetAddOns.Keys.Where(id => !Utils.GetPlayerById(id)).ToArray()) Main.SetAddOns.Remove(id); AddonRolesList = []; diff --git a/Modules/CustomSabotage.cs b/Modules/CustomSabotage.cs index 8432acd52..8b33ec951 100644 --- a/Modules/CustomSabotage.cs +++ b/Modules/CustomSabotage.cs @@ -85,9 +85,9 @@ public abstract class CustomSabotage public static void UpdateAll() { - foreach (CustomSabotage sabotage in Instances.ToArray()) + for (int index = Instances.Count - 1; index >= 0; index--) { - try { sabotage.Update(); } + try { Instances[index].Update(); } catch (Exception e) { Utils.ThrowException(e); } } diff --git a/Modules/ErrorText.cs b/Modules/ErrorText.cs index 24659ad86..70e59644d 100644 --- a/Modules/ErrorText.cs +++ b/Modules/ErrorText.cs @@ -25,6 +25,8 @@ public class ErrorText : MonoBehaviour public void Update() { + if (AllErrors.Count == 0) return; + AllErrors.ForEach(err => err.IncreaseTimer()); ErrorData[] ToRemove = AllErrors.Where(err => err.ErrorLevel <= 1 && 30f < err.Timer).ToArray(); diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index dc32340d3..859922a63 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -170,7 +170,8 @@ internal static class ExtendedPlayerControl public void FreezeForOthers() { if (!AmongUsClient.Instance.AmHost) return; - + + bool hasValue = false; CustomRpcSender sender = CustomRpcSender.Create($"SnapTo Freeze ({player.GetNameWithRole()})", SendOption.Reliable); sender.StartPackedMessage(); @@ -188,16 +189,17 @@ internal static class ExtendedPlayerControl .EndRpc(); sender.EndMessage(); NumSnapToCallsThisRound += 2; + hasValue = true; } - sender.SendMessage(); + sender.SendMessage(dispose: !hasValue); player.Visible = false; } - public void SetChatVisible(bool visible, MessageWriter packedWriter = null) + public bool SetChatVisible(bool visible, MessageWriter packedWriter = null) { - if (!AmongUsClient.Instance.AmHost) return; + if (!AmongUsClient.Instance.AmHost) return false; Logger.Info($"Setting the chat {(visible ? "visible" : "hidden")} for {player.GetNameWithRole()}", "SetChatVisible"); @@ -205,7 +207,7 @@ internal static class ExtendedPlayerControl { HudManager.Instance.Chat.SetVisible(visible); HudManager.Instance.Chat.HideBanButton(); - return; + return false; } if (player.IsModdedClient()) @@ -213,12 +215,12 @@ internal static class ExtendedPlayerControl var msg = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetChatVisible, SendOption.Reliable, player.OwnerId); msg.Write(visible); AmongUsClient.Instance.FinishRpcImmediately(msg); - return; + return false; } if (packedWriter == null) DataFlagRateLimiter.Enqueue(Action, calls: 3); else Action(); - return; + return true; void Action() { @@ -409,14 +411,11 @@ internal static class ExtendedPlayerControl if (setRoleMap) { - foreach ((byte seerID, byte targetID) in StartGameHostPatch.RpcSetRoleReplacer.RoleMap.Keys.ToArray()) + foreach ((byte seerID, byte targetID) in StartGameHostPatch.RpcSetRoleReplacer.RoleMap.Keys.Where(x => x.TargetID == player.PlayerId).ToArray()) { - if (targetID == player.PlayerId) - { - var value = StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(seerID, targetID)]; - value.RoleType = roleTypes; - StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(seerID, targetID)] = value; - } + var value = StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(seerID, targetID)]; + value.RoleType = roleTypes; + StartGameHostPatch.RpcSetRoleReplacer.RoleMap[(seerID, targetID)] = value; } } } @@ -1514,7 +1513,8 @@ internal static class ExtendedPlayerControl AmongUsClient.Instance.FinishRpcImmediately(writer); NotifyRoles(SpecifyTarget: player); } - + + bool hasValue = false; var sender = CustomRpcSender.Create("RpcMakeInvisible", SendOption.Reliable); sender.StartPackedMessage(); @@ -1534,9 +1534,10 @@ internal static class ExtendedPlayerControl sender.EndMessage(); NumSnapToCallsThisRound += 2; + hasValue = true; } - sender.SendMessage(); + sender.SendMessage(dispose: !hasValue); Logger.Info($"Made {player.GetNameWithRole()} invisible", "RpcMakeInvisible"); } @@ -1565,7 +1566,8 @@ internal static class ExtendedPlayerControl AmongUsClient.Instance.FinishRpcImmediately(writer); NotifyRoles(SpecifyTarget: player); } - + + bool hasValue = false; var sender = CustomRpcSender.Create("RpcMakeVisible", SendOption.Reliable); sender.StartPackedMessage(); @@ -1589,9 +1591,10 @@ internal static class ExtendedPlayerControl sender.EndMessage(); NumSnapToCallsThisRound += 3; + hasValue = true; } - sender.SendMessage(); + sender.SendMessage(dispose: !hasValue); Logger.Info($"Made {player.GetNameWithRole()} visible", "RpcMakeVisible"); } @@ -1600,7 +1603,8 @@ internal static class ExtendedPlayerControl if (!AmongUsClient.Instance.AmHost) return; if (!Main.Invisible.Contains(player.PlayerId)) return; if (phantom && Options.CurrentGameMode != CustomGameMode.Standard) return; - + + bool hasValue = false; var sender = CustomRpcSender.Create("RpcResetInvisibility", SendOption.Reliable); sender.StartPackedMessage(); @@ -1636,9 +1640,10 @@ internal static class ExtendedPlayerControl sender.EndMessage(); NumSnapToCallsThisRound += 4; + hasValue = true; } - sender.SendMessage(); + sender.SendMessage(dispose: !hasValue); Logger.Info($"Reset invisibility for {player.GetNameWithRole()}", "RpcResetInvisibility"); } diff --git a/Modules/Extensions/CollectionExtensions.cs b/Modules/Extensions/CollectionExtensions.cs index a2b012884..c66afbee3 100644 --- a/Modules/Extensions/CollectionExtensions.cs +++ b/Modules/Extensions/CollectionExtensions.cs @@ -484,16 +484,6 @@ public static class CollectionExtensions { return playerIds.Select(Utils.GetPlayer).Where(x => x); } - - /// - /// Converts a list of player IDs to a list of instances - /// - /// - /// - public static List ToValidPlayers(this List playerIds) - { - return playerIds.ConvertAll(Utils.GetPlayer).FindAll(x => x); - } #endregion diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs index 8a24a0dc6..3c2f6b7ae 100644 --- a/Modules/GuessManager.cs +++ b/Modules/GuessManager.cs @@ -1195,35 +1195,47 @@ public static class GuessManager [SuppressMessage("ReSharper", "UnusedMember.Local")] public static void Postfix() { - if (TextTemplate) Object.Destroy(TextTemplate.gameObject); - if (GuesserUI) Object.Destroy(GuesserUI); - - foreach (List roleButtonsValue in RoleButtons.Values) - { - foreach (Transform transform in roleButtonsValue) - { - if (transform?.gameObject) - Object.Destroy(transform.gameObject); - } - } - - foreach (SpriteRenderer spriteRenderer in RoleSelectButtons.Values) - { - if (spriteRenderer?.gameObject) - Object.Destroy(spriteRenderer.gameObject); - } - - foreach (SpriteRenderer spriteRenderer in PageButtons) - { - if (spriteRenderer?.gameObject) - Object.Destroy(spriteRenderer.gameObject); - } - + if (TextTemplate && TextTemplate.gameObject) Object.Destroy(TextTemplate.gameObject); TextTemplate = null; + + if (GuesserUI) Object.Destroy(GuesserUI); GuesserUI = null; - RoleButtons.Clear(); - RoleSelectButtons.Clear(); - PageButtons.Clear(); + + if (RoleButtons != null) + { + foreach (List roleButtonsValue in RoleButtons.Values) + { + foreach (Transform transform in roleButtonsValue) + { + if (transform && transform.gameObject) + Object.Destroy(transform.gameObject); + } + } + + RoleButtons = null; + } + + if (RoleSelectButtons != null) + { + foreach (SpriteRenderer spriteRenderer in RoleSelectButtons.Values) + { + if (spriteRenderer && spriteRenderer.gameObject) + Object.Destroy(spriteRenderer.gameObject); + } + + RoleSelectButtons = null; + } + + if (PageButtons != null) + { + foreach (SpriteRenderer spriteRenderer in PageButtons) + { + if (spriteRenderer && spriteRenderer.gameObject) + Object.Destroy(spriteRenderer.gameObject); + } + + PageButtons = null; + } } } diff --git a/Modules/LocateArrow.cs b/Modules/LocateArrow.cs index a9bab9ea3..f1341eac4 100644 --- a/Modules/LocateArrow.cs +++ b/Modules/LocateArrow.cs @@ -72,6 +72,7 @@ internal static class LocateArrow { ArrowInfo arrowInfo = new(seer, locate); List removeList = new(LocateArrows.Keys.Where(k => k.Equals(arrowInfo))); + if (removeList.Count == 0) return; removeList.ForEach(a => LocateArrows.Remove(a)); Utils.SendRPC(CustomRPC.Arrow, false, 2, seer, locate); @@ -85,8 +86,8 @@ internal static class LocateArrow public static void RemoveAllTarget(byte seer) { List removeList = new(LocateArrows.Keys.Where(k => k.From == seer)); + if (removeList.Count == 0) return; removeList.ForEach(a => LocateArrows.Remove(a)); - Utils.SendRPC(CustomRPC.Arrow, false, 3, seer); Logger.Info($"Removed all locate arrows for: {seer} ({seer.GetPlayer()?.GetRealName()})", "LocateArrow"); } @@ -129,7 +130,7 @@ internal static class LocateArrow var update = false; - foreach (ArrowInfo arrowInfo in arrowList.ToArray()) + foreach (ArrowInfo arrowInfo in arrowList) { Vector3 loc = arrowInfo.To; diff --git a/Modules/NameNotifyManager.cs b/Modules/NameNotifyManager.cs index 8737e585c..d6e376968 100644 --- a/Modules/NameNotifyManager.cs +++ b/Modules/NameNotifyManager.cs @@ -60,21 +60,29 @@ public static class NameNotifyManager if (now == LastUpdate) return; LastUpdate = now; - List toNotify = []; + List toNotify = null; foreach ((byte id, Dictionary notifies) in Notifies) { - List toRemove = []; + List toRemove = null; - notifies.DoIf(x => x.Value <= now, x => toRemove.Add(x.Key)); + notifies.DoIf(x => x.Value <= now, x => + { + toRemove ??= []; + toRemove.Add(x.Key); + }); - toRemove.ForEach(x => notifies.Remove(x)); - if (toRemove.Count > 0) toNotify.Add(id); + if (toRemove != null) + { + toRemove.ForEach(x => notifies.Remove(x)); + toNotify ??= []; + toNotify.Add(id); + } } - if (toNotify.Count == 0 || !AmongUsClient.Instance.AmHost) return; + if (!AmongUsClient.Instance.AmHost) return; - toNotify.ToValidPlayers().ForEach(x => Utils.NotifyRoles(SpecifySeer: x, SpecifyTarget: x)); + toNotify?.ToValidPlayers().Do(x => Utils.NotifyRoles(SpecifySeer: x, SpecifyTarget: x)); } public static bool GetNameNotify(PlayerControl player, out string name) diff --git a/Modules/TargetArrow.cs b/Modules/TargetArrow.cs index be5244496..f70d6bccc 100644 --- a/Modules/TargetArrow.cs +++ b/Modules/TargetArrow.cs @@ -72,7 +72,8 @@ internal static class TargetArrow { ArrowInfo arrowInfo = new(seer, target); List removeList = new(TargetArrows.Keys.Where(k => k.Equals(arrowInfo))); - foreach (ArrowInfo a in removeList.ToArray()) TargetArrows.Remove(a); + if (removeList.Count == 0) return; + removeList.ForEach(a => TargetArrows.Remove(a)); Utils.SendRPC(CustomRPC.Arrow, true, 2, seer, target); Logger.Info($"Removed target arrow: {seer} ({seer.GetPlayer()?.GetRealName()}) => {target} ({target.GetPlayer()?.GetRealName()})", "TargetArrow"); @@ -85,7 +86,8 @@ internal static class TargetArrow public static void RemoveAllTarget(byte seer) { List removeList = new(TargetArrows.Keys.Where(k => k.From == seer)); - foreach (ArrowInfo arrowInfo in removeList.ToArray()) TargetArrows.Remove(arrowInfo); + if (removeList.Count == 0) return; + removeList.ForEach(a => TargetArrows.Remove(a)); Utils.SendRPC(CustomRPC.Arrow, true, 3, seer); Logger.Info($"Removed all target arrows for {seer} ({seer.GetPlayer()?.GetRealName()})", "TargetArrow"); @@ -128,7 +130,7 @@ internal static class TargetArrow var update = false; - foreach (ArrowInfo arrowInfo in arrowList.ToArray()) + foreach (ArrowInfo arrowInfo in arrowList) { byte targetId = arrowInfo.To; PlayerControl target = Utils.GetPlayerById(targetId); diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 59e547584..9441abb42 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -2549,19 +2549,30 @@ public static class Utils public static PlayerControl GetPlayerById(int playerId, bool fast = true) { - if (playerId is > byte.MaxValue or < byte.MinValue) return null; - - if (fast && GameStates.InGame && Main.PlayerStates.TryGetValue((byte)playerId, out PlayerState state) && state.Player) return state.Player; - - if (playerId == PlayerControl.LocalPlayer.PlayerId) return PlayerControl.LocalPlayer; - - foreach (var pc in PlayerControl.AllPlayerControls) + try { - if (pc.PlayerId == playerId) - return pc; - } + if (playerId == PlayerControl.LocalPlayer.PlayerId) + return PlayerControl.LocalPlayer; - return null; + byte id = (byte)playerId; + + if (fast && GameStates.InGame && + Main.PlayerStates.TryGetValue(id, out PlayerState state) && + state.Player) { return state.Player; } + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc.PlayerId == id) + return pc; + } + + return null; + } + catch (Exception e) + { + ThrowException(e); + return null; + } } public static void SetupLongRoleDescriptions() @@ -4534,8 +4545,8 @@ public static class Utils packedWriter.WritePacked(AmongUsClient.Instance.GameId); } - pc.SetChatVisible(visible, packedWriter); - messages++; + if (pc.SetChatVisible(visible, packedWriter)) + messages++; } packedWriter.EndMessage(); diff --git a/Patches/HudPatch.cs b/Patches/HudPatch.cs index adde524cb..3d9e5c4c1 100644 --- a/Patches/HudPatch.cs +++ b/Patches/HudPatch.cs @@ -1170,7 +1170,7 @@ internal static class TaskPanelBehaviourPatch { Dictionary SummaryText3 = new Dictionary(); - foreach (byte id in Main.PlayerStates.Keys.ToArray()) + foreach (byte id in Main.PlayerStates.Keys) { string name = Main.AllPlayerNames[id].RemoveHtmlTags().Replace("\r\n", string.Empty); var summary = $"{Utils.GetProgressText(id)} {Utils.ColorString(Main.PlayerColors[id], name)}"; diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index 33bfeb71a..c68fe49cf 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -340,7 +340,7 @@ internal static class SetUpRoleTextPatch { LateTask.New(() => { - if (AmongUsClient.Instance.IsGameOver || GameStates.IsLobby || lp == null) return; + if (AmongUsClient.Instance.IsGameOver || GameStates.IsLobby || !lp) return; lp.SetName(Main.AllPlayerNames[lp.PlayerId]); }, 1f, "Reset Name For Modded Client"); } @@ -1272,7 +1272,7 @@ internal static class IntroCutsceneDestroyPatch try { - System.Collections.Generic.List spectators = ChatCommands.Spectators.ToList().ToValidPlayers(); + System.Collections.Generic.List spectators = ChatCommands.Spectators.ToValidPlayers().ToList(); if (Main.GM.Value) spectators.Add(PlayerControl.LocalPlayer); spectators.ForEach(x => @@ -1374,6 +1374,9 @@ internal static class IntroCutsceneDestroyPatch case CustomGameMode.StopAndGo: StopAndGo.RoundTimer = Stopwatch.StartNew(); break; + case CustomGameMode.RoomRush: + RoomRush.GameStartDateTime = DateTime.Now; + break; } } diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index 9b344d456..2fa9a9e75 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -769,7 +769,9 @@ internal static class MeetingHudStartPatch if (MeetingStates.FirstMeeting && CustomRoles.Workaholic.RoleExist() && Workaholic.WorkaholicGiveAdviceAlive.GetBool() && !Workaholic.WorkaholicCannotWinAtDeath.GetBool() /* && !Options.GhostIgnoreTasks.GetBool()*/) { - foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls().Where(x => x.Is(CustomRoles.Workaholic)).ToArray()) Workaholic.WorkaholicAlive.Add(pc.PlayerId); + foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls()) + if (pc.Is(CustomRoles.Workaholic)) + Workaholic.WorkaholicAlive.Add(pc.PlayerId); List workaholicAliveList = []; workaholicAliveList.AddRange(Workaholic.WorkaholicAlive.Select(whId => Main.AllPlayerNames[whId])); @@ -781,7 +783,9 @@ internal static class MeetingHudStartPatch // Bait Notify if (MeetingStates.FirstMeeting && CustomRoles.Bait.RoleExist() && Options.BaitNotification.GetBool()) { - foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls().Where(x => x.Is(CustomRoles.Bait)).ToArray()) Main.BaitAlive.Add(pc.PlayerId); + foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls()) + if (pc.Is(CustomRoles.Bait)) + Main.BaitAlive.Add(pc.PlayerId); List baitAliveList = []; baitAliveList.AddRange(Main.BaitAlive.Select(whId => Main.AllPlayerNames[whId])); diff --git a/Patches/TextBoxPatch.cs b/Patches/TextBoxPatch.cs index b8cb9bfc0..ebba0e857 100644 --- a/Patches/TextBoxPatch.cs +++ b/Patches/TextBoxPatch.cs @@ -380,9 +380,9 @@ public static class TextBoxPatch public static void OnMeetingStart() { - PlaceHolderText?.transform.SetAsLastSibling(); - CommandInfoText?.transform.SetAsLastSibling(); - AdditionalInfoText?.transform.SetAsLastSibling(); + if (PlaceHolderText) PlaceHolderText.transform.SetAsLastSibling(); + if (CommandInfoText) CommandInfoText.transform.SetAsLastSibling(); + if (AdditionalInfoText) AdditionalInfoText.transform.SetAsLastSibling(); } [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.Update))] diff --git a/Resources/Lang/en_US.jsonc b/Resources/Lang/en_US.jsonc index 46220e114..b6a3b71b9 100644 --- a/Resources/Lang/en_US.jsonc +++ b/Resources/Lang/en_US.jsonc @@ -4700,7 +4700,6 @@ "ND_LimitMaximumDisastersAtOnce": "Limit Maximum Active Disasters at Once", "ND_MaximumDisastersAtOnce": "Maximum Active Disasters at Once", "ND_WhenLimitIsReached": "When the Limit is Reached", - "ND_LimitReachedOptions.OnlySpawnInstantDisasters": "Only Spawn Instant Disasters", "ND_LimitReachedOptions.RemoveOldest": "Remove Oldest Disaster", "ND_LimitReachedOptions.RemoveRandom": "Remove Random Disaster", "ND_DisasterSpawnMode": "Disaster Spawn Mode", diff --git a/Roles/Standard/Addons/Common/Asthmatic.cs b/Roles/Standard/Addons/Common/Asthmatic.cs index d8fab770a..b00921637 100644 --- a/Roles/Standard/Addons/Common/Asthmatic.cs +++ b/Roles/Standard/Addons/Common/Asthmatic.cs @@ -81,7 +81,7 @@ internal class Asthmatic : IAddon { if (Timers.Count == 0) return; - List toRemove = []; + List toRemove = null; foreach ((byte id, Counter counter) in Timers) { @@ -89,6 +89,7 @@ internal class Asthmatic : IAddon if (state.IsDead || !state.SubRoles.Contains(CustomRoles.Asthmatic) || state.MainRole == CustomRoles.Pestilence) { + toRemove ??= []; toRemove.Add(id); continue; } @@ -96,7 +97,7 @@ internal class Asthmatic : IAddon counter.Update(); } - if (toRemove.Count == 0) return; + if (toRemove == null) return; foreach (byte id in toRemove) { diff --git a/Roles/Standard/Addons/Impostor/Damocles.cs b/Roles/Standard/Addons/Impostor/Damocles.cs index 5633b0418..aed7b98f0 100644 --- a/Roles/Standard/Addons/Impostor/Damocles.cs +++ b/Roles/Standard/Addons/Impostor/Damocles.cs @@ -94,16 +94,6 @@ public class Damocles : IAddon MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncDamoclesTimer, SendOption.Reliable); writer.Write(playerId); writer.Write(Timer[playerId]); - writer.Write(LastUpdate[playerId].ToString()); - List pev = PreviouslyEnteredVents.GetValueOrDefault(playerId, []); - writer.Write(pev.Count); - - if (pev.Count > 0) - { - foreach (int vent in pev.ToArray()) - writer.Write(vent); - } - AmongUsClient.Instance.FinishRpcImmediately(writer); } @@ -111,14 +101,6 @@ public class Damocles : IAddon { byte playerId = reader.ReadByte(); Timer[playerId] = reader.ReadInt32(); - LastUpdate[playerId] = long.Parse(reader.ReadString()); - int elements = reader.ReadInt32(); - - if (elements > 0) - { - for (var i = 0; i < elements; i++) - PreviouslyEnteredVents[playerId].Add(reader.ReadInt32()); - } } public static void OnMurder(byte id) diff --git a/Roles/Standard/Crewmate/Chaos/Randomizer.cs b/Roles/Standard/Crewmate/Chaos/Randomizer.cs index 084bbf8e4..f2ac710aa 100644 --- a/Roles/Standard/Crewmate/Chaos/Randomizer.cs +++ b/Roles/Standard/Crewmate/Chaos/Randomizer.cs @@ -749,16 +749,11 @@ internal class Randomizer : RoleBase { long now = Utils.TimeStamp; - foreach (KeyValuePair item in effects.ToArray()) + foreach (KeyValuePair item in effects.Where(item => item.Value.StartTimeStamp + item.Value.Duration < now).ToArray()) { - if (item.Value.StartTimeStamp + item.Value.Duration < now) - { - if (item.Key.IsVisionChangingEffect()) RevertVisionChangesForPlayer(pc, true); - - if (item.Key.IsSpeedChangingEffect()) RevertSpeedChangesForPlayer(pc, true); - - effects.Remove(item.Key); - } + if (item.Key.IsVisionChangingEffect()) RevertVisionChangesForPlayer(pc, true); + if (item.Key.IsSpeedChangingEffect()) RevertSpeedChangesForPlayer(pc, true); + effects.Remove(item.Key); } } } diff --git a/Roles/Standard/Crewmate/Chaos/Tornado.cs b/Roles/Standard/Crewmate/Chaos/Tornado.cs index 94bd1f109..466f87af1 100644 --- a/Roles/Standard/Crewmate/Chaos/Tornado.cs +++ b/Roles/Standard/Crewmate/Chaos/Tornado.cs @@ -134,7 +134,9 @@ internal class Tornado : RoleBase float tornadoRange = TornadoRange.GetFloat(); int tornadoDuration = TornadoDuration.GetInt(); - foreach (KeyValuePair<(Vector2 Location, string RoomName), long> tornado in Tornados.ToArray()) + List<(Vector2 Location, string RoomName)> toRemove = null; + + foreach (KeyValuePair<(Vector2 Location, string RoomName), long> tornado in Tornados) { if (FastVector2.DistanceWithinRange(tornado.Key.Location, pc.Pos(), tornadoRange)) { @@ -148,11 +150,14 @@ internal class Tornado : RoleBase if (tornado.Value + tornadoDuration < now) { - Tornados.Remove(tornado.Key); + toRemove ??= []; + toRemove.Add(tornado.Key); SendRPCAddTornado(false, tornado.Key.Location, tornado.Key.RoomName); NotifyRoles(SpecifySeer: TornadoPC, SpecifyTarget: TornadoPC); } } + + toRemove?.ForEach(x => Tornados.Remove(x)); } else { diff --git a/Roles/Standard/Crewmate/Investigate/Druid.cs b/Roles/Standard/Crewmate/Investigate/Druid.cs index f1d25ec9e..53163abbb 100644 --- a/Roles/Standard/Crewmate/Investigate/Druid.cs +++ b/Roles/Standard/Crewmate/Investigate/Druid.cs @@ -166,16 +166,21 @@ public class Druid : RoleBase { if (!GameStates.IsInTask || Triggers.Count <= 0 || PlayerIdList.Contains(pc.PlayerId)) return; - foreach (KeyValuePair trigger in Triggers.ToArray()) + List toRemove = null; + + foreach (KeyValuePair trigger in Triggers) { if (FastVector2.DistanceWithinRange(trigger.Key, pc.Pos(), 1.5f)) { - Triggers.Remove(trigger.Key); + toRemove ??= []; + toRemove.Add(trigger.Key); DruidPC.Notify(string.Format(GetString("DruidTriggerTriggered"), GetFormattedRoomName(trigger.Value), GetFormattedVectorText(trigger.Key))); SendRPCAddTrigger(false, DruidPC.PlayerId, trigger.Key); CustomNetObject.Get(TriggerIds[trigger.Key])?.Despawn(); } } + + toRemove?.ForEach(x => Triggers.Remove(x)); } public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) diff --git a/Roles/Standard/Crewmate/Investigate/Scout.cs b/Roles/Standard/Crewmate/Investigate/Scout.cs index 597850deb..bdf654458 100644 --- a/Roles/Standard/Crewmate/Investigate/Scout.cs +++ b/Roles/Standard/Crewmate/Investigate/Scout.cs @@ -98,7 +98,7 @@ public class Scout : RoleBase public static string GetTargetMark(PlayerControl seer, PlayerControl target) { - return !(seer == null || target == null) && TrackerTarget.ContainsKey(seer.PlayerId) && TrackerTarget[seer.PlayerId].Contains(target.PlayerId) ? Utils.ColorString(seer.GetRoleColor(), "◀") : string.Empty; + return TrackerTarget.ContainsKey(seer.PlayerId) && TrackerTarget[seer.PlayerId].Contains(target.PlayerId) ? Utils.ColorString(seer.GetRoleColor(), "◀") : string.Empty; } public override bool OnVote(PlayerControl player, PlayerControl target) @@ -124,7 +124,7 @@ public class Scout : RoleBase public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) { - if (seer == null || seer.PlayerId != TrackerId || target != null && seer.PlayerId != target.PlayerId || !TrackerTarget.ContainsKey(seer.PlayerId) || meeting || hud) return string.Empty; + if (seer.PlayerId != TrackerId || seer.PlayerId != target.PlayerId || !TrackerTarget.ContainsKey(seer.PlayerId) || meeting || hud) return string.Empty; return TrackerTarget[seer.PlayerId].Aggregate(string.Empty, (current, trackTarget) => current + Utils.ColorString(CanGetColoredArrow.GetBool() ? Main.PlayerColors[trackTarget] : Color.white, TargetArrow.GetArrows(seer, trackTarget))) + LocateArrow.GetArrows(seer); } @@ -137,15 +137,13 @@ public class Scout : RoleBase public static string GetArrowAndLastRoom(PlayerControl seer, PlayerControl target) { - if (seer == null || target == null) return string.Empty; - Color roleColor = Utils.GetRoleColor(CustomRoles.Scout); string text = Utils.ColorString(roleColor, TargetArrow.GetArrows(seer, target.PlayerId)); text += Utils.ColorString(roleColor, LocateArrow.GetArrows(seer)); PlainShipRoom room = Main.PlayerStates[target.PlayerId].LastRoom; - if (room == null) + if (!room) text += Utils.ColorString(Color.gray, "@" + GetString("FailToTrack")); else text += Utils.ColorString(roleColor, "@" + GetString(room.RoomId.ToString())); @@ -155,7 +153,7 @@ public class Scout : RoleBase public static void OnPlayerDeath(PlayerControl player) { - if (player == null) return; + if (!player) return; foreach (KeyValuePair> kvp in TrackerTarget) { @@ -178,7 +176,7 @@ public class Scout : RoleBase { PlayerControl pc = Utils.GetPlayerById(id); - if (pc == null || !pc.IsAlive()) + if (!pc || !pc.IsAlive()) { kvp.Value.Remove(id); TargetArrow.Remove(kvp.Key, id); diff --git a/Roles/Standard/Crewmate/Miscellaneous/PortalMaker.cs b/Roles/Standard/Crewmate/Miscellaneous/PortalMaker.cs index efa30ea71..a367987a9 100644 --- a/Roles/Standard/Crewmate/Miscellaneous/PortalMaker.cs +++ b/Roles/Standard/Crewmate/Miscellaneous/PortalMaker.cs @@ -41,9 +41,9 @@ public class PortalMaker : RoleBase public override void OnPet(PlayerControl pc) { - if (Marks.Count == 2) + if (Marks.Count >= 2) { - if (!PetToRemovePortals.GetBool()) return; + if (!PetToRemovePortals.GetBool() && Marks.Count == 2) return; Marks.Clear(); CustomNetObject.AllObjects.OfType().Do(x => x.Despawn()); pc.Notify(Translator.GetString("MarksCleared")); diff --git a/Roles/Standard/Crewmate/Support/Benefactor.cs b/Roles/Standard/Crewmate/Support/Benefactor.cs index 841fdfe01..2bf76a622 100644 --- a/Roles/Standard/Crewmate/Support/Benefactor.cs +++ b/Roles/Standard/Crewmate/Support/Benefactor.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using static EHR.Translator; namespace EHR.Roles; @@ -78,15 +77,15 @@ internal class Benefactor : RoleBase } else { - foreach (byte benefactorId in TaskIndex.Keys.ToArray()) + foreach ((byte benefactorId, List taskIndexes) in TaskIndex) { - if (TaskIndex[benefactorId].Contains(task.Index)) + if (taskIndexes.Contains(task.Index)) { PlayerControl benefactorPC = Utils.GetPlayerById(benefactorId); - if (benefactorPC == null) continue; + if (!benefactorPC) continue; player.Notify(GetString("BenefactorTargetGotShieldNotify")); - TaskIndex[benefactorId].Remove(task.Index); + taskIndexes.Remove(task.Index); ShieldedPlayers.Add(playerId); LateTask.New(() => ShieldedPlayers.Remove(playerId), ShieldDuration.GetInt()); Logger.Info($"{player.GetAllRoleName()} got a shield because the task was marked by {benefactorPC.GetNameWithRole()}", "Benefactor"); diff --git a/Roles/Standard/Crewmate/Support/Crusader.cs b/Roles/Standard/Crewmate/Support/Crusader.cs index 5b4027ba9..e22f98061 100644 --- a/Roles/Standard/Crewmate/Support/Crusader.cs +++ b/Roles/Standard/Crewmate/Support/Crusader.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using AmongUs.GameOptions; namespace EHR.Roles; @@ -92,7 +93,7 @@ public class Crusader : RoleBase return; } - keepGameGoing = ForCrusade.ToValidPlayers().Exists(x => x.IsAlive()); + keepGameGoing = ForCrusade.ToValidPlayers().Any(x => x.IsAlive()); countsAs = 1; } } \ No newline at end of file diff --git a/Roles/Standard/Crewmate/Support/TimeManager.cs b/Roles/Standard/Crewmate/Support/TimeManager.cs index dcefa8e7b..aaf7e4ff5 100644 --- a/Roles/Standard/Crewmate/Support/TimeManager.cs +++ b/Roles/Standard/Crewmate/Support/TimeManager.cs @@ -56,7 +56,7 @@ public class TimeManager : RoleBase { var sec = 0; - foreach (byte playerId in PlayerIdList.ToArray()) + foreach (byte playerId in PlayerIdList) { if (Utils.GetPlayerById(playerId).Is(CustomRoles.Madmate)) sec -= AdditionalTime(playerId); diff --git a/Roles/Standard/Ghost/Haunter.cs b/Roles/Standard/Ghost/Haunter.cs index d104309ac..b7c5ed446 100644 --- a/Roles/Standard/Ghost/Haunter.cs +++ b/Roles/Standard/Ghost/Haunter.cs @@ -123,12 +123,12 @@ internal class Haunter : IGhostRole AllHauntedPlayers.UnionWith(targets); - var targetPcs = targets.ToValidPlayers(); + var targetPcs = targets.ToValidPlayers().ToArray(); - targetPcs.ForEach(x => x.Notify(Translator.GetString("HaunterRevealedYou"), 10f)); - WarnedImps.ToValidPlayers().ForEach(x => x.Notify(Translator.GetString("HaunterFinishedTasks"), 10f)); + targetPcs.Do(x => x.Notify(Translator.GetString("HaunterRevealedYou"), 10f)); + WarnedImps.ToValidPlayers().Do(x => x.Notify(Translator.GetString("HaunterFinishedTasks"), 10f)); - targetPcs.ForEach(x => Utils.NotifyRoles(SpecifyTarget: x)); + targetPcs.Do(x => Utils.NotifyRoles(SpecifyTarget: x)); } public void Update(PlayerControl pc) diff --git a/Roles/Standard/Impostor/Concealing/Anonymous.cs b/Roles/Standard/Impostor/Concealing/Anonymous.cs index 092634727..72dda8b9d 100644 --- a/Roles/Standard/Impostor/Concealing/Anonymous.cs +++ b/Roles/Standard/Impostor/Concealing/Anonymous.cs @@ -76,7 +76,7 @@ public class Anonymous : RoleBase public static void AddDeadBody(PlayerControl target) { - if (target != null && !DeadBodyList.Contains(target.PlayerId)) DeadBodyList.Add(target.PlayerId); + if (target && !DeadBodyList.Contains(target.PlayerId)) DeadBodyList.Add(target.PlayerId); } public override bool OnShapeshift(PlayerControl pc, PlayerControl ssTarget, bool shapeshifting) @@ -87,12 +87,14 @@ public class Anonymous : RoleBase var targetId = byte.MaxValue; - foreach (byte db in DeadBodyList.ToArray()) + foreach (byte db in DeadBodyList) { PlayerControl dp = Utils.GetPlayerById(db); - if (dp == null || dp.GetRealKiller() == null) continue; + if (!dp) continue; + PlayerControl realKiller = dp.GetRealKiller(); + if (!realKiller) continue; - if (dp.GetRealKiller().PlayerId == pc.PlayerId) targetId = db; + if (realKiller.PlayerId == pc.PlayerId) targetId = db; } if (targetId == byte.MaxValue && DeadBodyList.Count > 0) targetId = DeadBodyList.RandomElement(); diff --git a/Roles/Standard/Impostor/Concealing/Lightning.cs b/Roles/Standard/Impostor/Concealing/Lightning.cs index c567b9ca1..ac942c549 100644 --- a/Roles/Standard/Impostor/Concealing/Lightning.cs +++ b/Roles/Standard/Impostor/Concealing/Lightning.cs @@ -107,7 +107,7 @@ public class Lightning : RoleBase public static bool CheckLightningMurder(PlayerControl killer, PlayerControl target, bool force = false) { - if (killer == null || target == null || (!killer.Is(CustomRoles.Lightning) && !force)) return false; + if (!killer.Is(CustomRoles.Lightning) && !force) return false; if (IsGhost(target)) return false; @@ -155,11 +155,11 @@ public class Lightning : RoleBase List deList = []; - foreach (byte ghost in GhostPlayer.ToArray()) + foreach (byte ghost in GhostPlayer) { PlayerControl gs = Utils.GetPlayerById(ghost); - if (gs == null || !gs.IsAlive() || gs.Data.Disconnected) + if (!gs || !gs.IsAlive() || gs.Data.Disconnected) { //deList.Add(gs.PlayerId); // This will always result in a null reference exception continue; @@ -181,7 +181,7 @@ public class Lightning : RoleBase { GhostPlayer.RemoveAll(deList.Contains); - foreach (byte gs in deList.ToArray()) + foreach (byte gs in deList) { SendRPC(gs); Utils.NotifyRoles(SpecifySeer: Utils.GetPlayerById(gs)); @@ -193,10 +193,10 @@ public class Lightning : RoleBase { if (!(IsEnable || CustomRoles.Lightning.IsEnable())) return; - foreach (byte ghost in GhostPlayer.ToArray()) + foreach (byte ghost in GhostPlayer) { PlayerControl gs = Utils.GetPlayerById(ghost); - if (gs == null) continue; + if (!gs) continue; CheckForEndVotingPatch.TryAddAfterMeetingDeathPlayers(PlayerState.DeathReason.Quantization, gs.PlayerId); gs.SetRealKiller(RealKiller[gs.PlayerId]); diff --git a/Roles/Standard/Impostor/Concealing/Stealth.cs b/Roles/Standard/Impostor/Concealing/Stealth.cs index 8ba470c4d..af325cfd1 100644 --- a/Roles/Standard/Impostor/Concealing/Stealth.cs +++ b/Roles/Standard/Impostor/Concealing/Stealth.cs @@ -63,7 +63,7 @@ public sealed class Stealth : RoleBase private IEnumerable FindPlayersInSameRoom(PlayerControl killedPlayer) { PlainShipRoom room = killedPlayer.GetPlainShipRoom(); - if (room == null) return null; + if (!room) return null; Collider2D roomArea = room.roomArea; SystemTypes roomName = room.RoomId; diff --git a/Roles/Standard/Impostor/Killing/Mafioso.cs b/Roles/Standard/Impostor/Killing/Mafioso.cs index d7a018919..72f728cd3 100644 --- a/Roles/Standard/Impostor/Killing/Mafioso.cs +++ b/Roles/Standard/Impostor/Killing/Mafioso.cs @@ -123,13 +123,6 @@ public class Mafioso : RoleBase writer.Write(MafiosoId); writer.Write(Tier); writer.Write(XP); - writer.Write(PreviouslyUsedVents.Count); - - if (PreviouslyUsedVents.Count > 0) - { - foreach (int vent in PreviouslyUsedVents.ToArray()) - writer.Write(vent); - } AmongUsClient.Instance.FinishRpcImmediately(writer); } @@ -138,14 +131,6 @@ public class Mafioso : RoleBase { Tier = reader.ReadInt32(); XP = reader.ReadInt32(); - PreviouslyUsedVents.Clear(); - int elements = reader.ReadInt32(); - - if (elements > 0) - { - for (var i = 0; i < elements; i++) - PreviouslyUsedVents.Add(reader.ReadInt32()); - } } private void SendRPCSyncPistolCD() @@ -156,7 +141,6 @@ public class Mafioso : RoleBase writer.Write(MafiosoId); writer.Write(Pistol1CD); writer.Write(Pistol2CD); - writer.Write(lastUpdate.ToString()); AmongUsClient.Instance.FinishRpcImmediately(writer); } @@ -164,7 +148,6 @@ public class Mafioso : RoleBase { Pistol1CD = reader.ReadInt32(); Pistol2CD = reader.ReadInt32(); - lastUpdate = long.Parse(reader.ReadString()); } public override void OnFixedUpdate(PlayerControl pc) @@ -201,21 +184,21 @@ public class Mafioso : RoleBase public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) { - if (!GameStates.IsInTask || target == null || killer == null || !killer.Is(CustomRoles.Mafioso) || Tier < 3 || !IsEnable) return true; + if (!GameStates.IsInTask || !killer.Is(CustomRoles.Mafioso) || Tier < 3 || !IsEnable) return true; if (Pistol1CD > 0 && Pistol2CD > 0) return false; - int KCD = Tier >= 4 ? (int)Math.Round(AdjustedDefaultKillCooldown) : (int)Math.Round(AdjustedDefaultKillCooldown * 1.5); - KCD++; + int kcd = Tier >= 4 ? (int)Math.Round(AdjustedDefaultKillCooldown) : (int)Math.Round(AdjustedDefaultKillCooldown * 1.5); + kcd++; if (Pistol1CD <= 0) { - Pistol1CD = KCD; + Pistol1CD = kcd; NotifyRoles(SpecifySeer: killer, SpecifyTarget: killer); } else if (Pistol2CD <= 0) { - Pistol2CD = KCD; + Pistol2CD = kcd; NotifyRoles(SpecifySeer: killer, SpecifyTarget: killer); } @@ -224,13 +207,13 @@ public class Mafioso : RoleBase LateTask.New(() => { // ReSharper disable once ConditionIsAlwaysTrueOrFalse ---- Can be null since it's a task that completes later - if (target != null && target.IsAlive() && GameStates.IsInTask) target.Suicide(PlayerState.DeathReason.Kill, killer); + if (target && target.IsAlive() && GameStates.IsInTask) target.Suicide(PlayerState.DeathReason.Kill, killer); }, Delay.GetInt(), "Mafioso Tier 5 Kill Delay"); return false; } - if (Pistol1CD > 1 && Pistol2CD > 1) LateTask.New(() => { killer.SetKillCooldown(Math.Min(Pistol1CD, Pistol2CD) - 1); }, 0.1f, "Mafioso SetKillCooldown"); + if (Pistol1CD > 1 && Pistol2CD > 1) LateTask.New(() => killer.SetKillCooldown(Math.Min(Pistol1CD, Pistol2CD) - 1), 0.1f, "Mafioso SetKillCooldown"); return true; } @@ -240,10 +223,10 @@ public class Mafioso : RoleBase if (!IsEnable) return; PreviouslyUsedVents.Clear(); - int KCD = Tier >= 4 ? (int)Math.Round(AdjustedDefaultKillCooldown) : (int)Math.Round(AdjustedDefaultKillCooldown * 1.5); - KCD++; - Pistol1CD = KCD; - Pistol2CD = KCD; + int kcd = Tier >= 4 ? (int)Math.Round(AdjustedDefaultKillCooldown) : (int)Math.Round(AdjustedDefaultKillCooldown * 1.5); + kcd++; + Pistol1CD = kcd; + Pistol2CD = kcd; lastUpdate = TimeStamp; SendRPC(); SendRPCSyncPistolCD(); diff --git a/Roles/Standard/Impostor/Miscellaneous/EvilTracker.cs b/Roles/Standard/Impostor/Miscellaneous/EvilTracker.cs index 9addf07a2..84bd04077 100644 --- a/Roles/Standard/Impostor/Miscellaneous/EvilTracker.cs +++ b/Roles/Standard/Impostor/Miscellaneous/EvilTracker.cs @@ -115,8 +115,8 @@ public class EvilTracker : RoleBase public static void OnAnyoneMurder(PlayerControl killer, PlayerControl target) { - if (CanSeeKillFlash && killer != null && killer.Is(CustomRoleTypes.Impostor) && killer != target && !PlayerIdList.Contains(killer.PlayerId)) - PlayerIdList.ToValidPlayers().ForEach(x => x.KillFlash()); + if (CanSeeKillFlash && killer.Is(CustomRoleTypes.Impostor) && killer != target && !PlayerIdList.Contains(killer.PlayerId)) + PlayerIdList.ToValidPlayers().Do(x => x.KillFlash()); } public override bool OnShapeshift(PlayerControl shapeshifter, PlayerControl target, bool shapeshifting) @@ -227,7 +227,7 @@ public class EvilTracker : RoleBase string text = Utils.ColorString(Palette.ImpostorRed, TargetArrow.GetArrows(seer, target.PlayerId)); PlainShipRoom room = Main.PlayerStates[target.PlayerId].LastRoom; - if (room == null) + if (!room) text += Utils.ColorString(Color.gray, "@" + GetString("FailToTrack")); else text += Utils.ColorString(Palette.ImpostorRed, "@" + GetString(room.RoomId.ToString())); diff --git a/Roles/Standard/Impostor/Support/Devourer.cs b/Roles/Standard/Impostor/Support/Devourer.cs index 9c6a2a457..4e7e93cb6 100644 --- a/Roles/Standard/Impostor/Support/Devourer.cs +++ b/Roles/Standard/Impostor/Support/Devourer.cs @@ -107,18 +107,18 @@ public class Devourer : RoleBase return false; } - public static void OnDevourerDied(byte Devourer) + public static void OnDevourerDied(byte devourer) { - if (Main.PlayerStates[Devourer].Role is not Devourer { IsEnable: true } dv) return; + if (Main.PlayerStates[devourer].Role is not Devourer { IsEnable: true } dv) return; - foreach (byte player in dv.PlayerSkinsCosumed.ToArray()) + foreach (byte player in dv.PlayerSkinsCosumed) { Camouflage.PlayerSkins[player] = OriginalPlayerSkins[player]; if (!Camouflage.IsCamouflage) { PlayerControl pc = Main.EnumerateAlivePlayerControls().FirstOrDefault(a => a.PlayerId == player); - if (pc == null) continue; + if (!pc) continue; SetSkin(pc, OriginalPlayerSkins[player]); } diff --git a/Roles/Standard/Neutral/Evil/Workaholic.cs b/Roles/Standard/Neutral/Evil/Workaholic.cs index 4e1f81d86..4d41765ed 100644 --- a/Roles/Standard/Neutral/Evil/Workaholic.cs +++ b/Roles/Standard/Neutral/Evil/Workaholic.cs @@ -76,7 +76,9 @@ internal class Workaholic : RoleBase { Logger.Info("Workaholic Tasks Finished", "Workaholic"); RPC.PlaySoundRPC(player.PlayerId, Sounds.KillSound); - foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls().Where(pc => pc.PlayerId != player.PlayerId).ToArray()) pc.Suicide(pc.PlayerId == player.PlayerId ? PlayerState.DeathReason.Overtired : PlayerState.DeathReason.Ashamed, player); + + foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls()) + pc.Suicide(pc.PlayerId == player.PlayerId ? PlayerState.DeathReason.Overtired : PlayerState.DeathReason.Ashamed, player); CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Workaholic); CustomWinnerHolder.WinnerIds.Add(player.PlayerId); diff --git a/Roles/Standard/Neutral/Killing/HeadHunter.cs b/Roles/Standard/Neutral/Killing/HeadHunter.cs index fcc54b08d..db77386db 100644 --- a/Roles/Standard/Neutral/Killing/HeadHunter.cs +++ b/Roles/Standard/Neutral/Killing/HeadHunter.cs @@ -81,7 +81,7 @@ public class HeadHunter : RoleBase MessageWriter writer = Utils.CreateRPC(CustomRPC.SyncHeadHunter); writer.Write(HeadHunterId); writer.Write(Targets.Count); - foreach (byte target in Targets.ToArray()) writer.Write(target); + foreach (byte target in Targets) writer.Write(target); Utils.EndRPC(writer); } diff --git a/Roles/Standard/Neutral/Killing/Magician.cs b/Roles/Standard/Neutral/Killing/Magician.cs index 0c205602b..eedeaf16a 100644 --- a/Roles/Standard/Neutral/Killing/Magician.cs +++ b/Roles/Standard/Neutral/Killing/Magician.cs @@ -349,26 +349,35 @@ public class Magician : RoleBase if (Bombs.Count > 0) { - foreach (KeyValuePair bomb in Bombs.Where(bomb => bomb.Value + BombDelay.GetInt() < now).ToArray()) + List toRemove = null; + + foreach (KeyValuePair bomb in Bombs) { - foreach (PlayerControl tg in FastVector2.GetPlayersInRange(bomb.Key, BombRadius.GetFloat())) + if (bomb.Value + BombDelay.GetInt() < now) { - if (tg.PlayerId == pc.PlayerId) + foreach (PlayerControl tg in FastVector2.GetPlayersInRange(bomb.Key, BombRadius.GetFloat())) { - LateTask.New(() => + if (tg.PlayerId == pc.PlayerId) { - if (!GameStates.IsEnded) pc.Suicide(PlayerState.DeathReason.Bombed); - }, 0.5f, "Magician Bomb Suicide"); - continue; + LateTask.New(() => + { + if (!GameStates.IsEnded) + pc.Suicide(PlayerState.DeathReason.Bombed); + }, 0.5f, "Magician Bomb Suicide"); + continue; + } + + tg.Suicide(PlayerState.DeathReason.Bombed, pc); } - tg.Suicide(PlayerState.DeathReason.Bombed, pc); + toRemove ??= []; + toRemove.Add(bomb.Key); + pc.Notify(GetString("MagicianBombExploded")); } - - Bombs.Remove(bomb.Key); - pc.Notify(GetString("MagicianBombExploded")); } + toRemove?.ForEach(x => Bombs.Remove(x)); + var sb = new StringBuilder(); long[] list = [.. Bombs.Values]; foreach (long x in list) sb.Append(string.Format(GetString("MagicianBombExlodesIn"), BombDelay.GetInt() - (now - x) + 1)); diff --git a/Roles/Standard/Neutral/Killing/Pelican.cs b/Roles/Standard/Neutral/Killing/Pelican.cs index 46d74dab6..38a66ce74 100644 --- a/Roles/Standard/Neutral/Killing/Pelican.cs +++ b/Roles/Standard/Neutral/Killing/Pelican.cs @@ -85,7 +85,7 @@ public class Pelican : RoleBase if (playerId != byte.MaxValue) { writer.Write(EatenList[playerId].Count); - foreach (byte el in EatenList[playerId].ToArray()) writer.Write(el); + foreach (byte el in EatenList[playerId]) writer.Write(el); } AmongUsClient.Instance.FinishRpcImmediately(writer); diff --git a/Roles/Standard/Neutral/Killing/SoulCollector.cs b/Roles/Standard/Neutral/Killing/SoulCollector.cs index 2f2bad492..03b89a74e 100644 --- a/Roles/Standard/Neutral/Killing/SoulCollector.cs +++ b/Roles/Standard/Neutral/Killing/SoulCollector.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using AmongUs.GameOptions; using Hazel; @@ -108,9 +109,9 @@ public class SoulCollector : RoleBase { var sender = CustomRpcSender.Create("SoulCollector Post Exile", SendOption.Reliable); sender.StartMessage(); - List toExile = ToExile.ToValidPlayers(); + PlayerControl[] toExile = ToExile.ToValidPlayers().ToArray(); ToExile = []; - toExile.ForEach(x => + toExile.Do(x => { Main.AllPlayerSpeed[x.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); sender.RpcExiled(x, autoStartRpc: false); diff --git a/Roles/Standard/Neutral/Killing/Spider.cs b/Roles/Standard/Neutral/Killing/Spider.cs index 275cb7c87..95c8ef580 100644 --- a/Roles/Standard/Neutral/Killing/Spider.cs +++ b/Roles/Standard/Neutral/Killing/Spider.cs @@ -143,7 +143,7 @@ public class Spider : RoleBase public override void OnFixedUpdate(PlayerControl pc) { long now = Utils.TimeStamp; - List toRemove = []; + List toRemove = null; foreach ((Vector2 pos, Dictionary trapped) in Webs) { @@ -152,6 +152,7 @@ public class Spider : RoleBase if (trapped.Values.Min() <= now) { + toRemove ??= []; toRemove.Add(pos); LateTask.New(() => { @@ -165,7 +166,7 @@ public class Spider : RoleBase } } - toRemove.ForEach(x => + toRemove?.ForEach(x => { Webs.Remove(x); LocateArrow.Remove(pc.PlayerId, x);