diff --git a/.editorconfig b/.editorconfig index 1f3fff443..39bb8427d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ dotnet_diagnostic.ide0060.severity = none csharp_using_directive_placement = outside_namespace:silent csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_braces = true:silent -csharp_style_namespace_declarations = block_scoped:silent +csharp_style_namespace_declarations = file_scoped:none csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent csharp_style_prefer_primary_constructors = true:suggestion diff --git a/Modules/AntiBlackout.cs b/Modules/AntiBlackout.cs index e857a8ef0..def3c16c8 100644 --- a/Modules/AntiBlackout.cs +++ b/Modules/AntiBlackout.cs @@ -107,7 +107,19 @@ namespace EHR IsDeadCache.Clear(); IsCached = false; - if (doSend) SendGameData(); + + if (doSend) + { + SendGameData(); + LateTask.New(RestoreIsDeadByExile, 0.3f, "AntiBlackOut.RestoreIsDeadByExile"); + } + } + + private static void RestoreIsDeadByExile() + { + var sender = CustomRpcSender.Create("AntiBlackout.RestoreIsDeadByExile", SendOption.Reliable); + Main.AllPlayerControls.DoIf(x => x.Data.IsDead && !x.Data.Disconnected, x => sender.AutoStartRpc(x.NetId, (byte)RpcCalls.Exiled).EndRpc()); + sender.SendMessage(); } public static void SendGameData([CallerMemberName] string callerMethodName = "") diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 9842adeaa..1e716f55a 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -1485,7 +1485,7 @@ namespace EHR public static void RpcExileV2(this PlayerControl player) { player.Exiled(); - MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(player.NetId, (byte)RpcCalls.Exiled, SendOption.None); + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(player.NetId, (byte)RpcCalls.Exiled, SendOption.Reliable); AmongUsClient.Instance.FinishRpcImmediately(writer); FixedUpdatePatch.LoversSuicide(player.PlayerId); } diff --git a/Modules/GameState.cs b/Modules/GameState.cs index d4480aa8c..b73b41f7c 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -533,7 +533,7 @@ namespace EHR { get { - if (!IsOnlineGame) return false; + if (IsLocalGame && !IsNotJoined) return true; const string domain = "among.us"; diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 01d46d1d4..9b511a77e 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -148,10 +148,14 @@ other: ∟ ⌠ ⌡ ╬ ╨ ▓ ▒ ░ « » █ ▄ ▌▀▐│ ┤ ╡ ╢ } } - if (AmongUsClient.Instance.AmHost) + switch (AmongUsClient.Instance.AmHost) { - nt.SnapTo(location, (ushort)(nt.lastSequenceId + 328)); - nt.SetDirtyBit(uint.MaxValue); + case true: + nt.SnapTo(location, (ushort)(nt.lastSequenceId + 328)); + nt.SetDirtyBit(uint.MaxValue); + break; + case false when !nt.AmOwner: + return false; } var newSid = (ushort)(nt.lastSequenceId + 8); @@ -3090,88 +3094,94 @@ other: ∟ ⌠ ⌡ ╬ ╨ ▓ ▒ ░ « » █ ▄ ▌▀▐│ ┤ ╡ ╢ public static string SummaryTexts(byte id, bool disableColor = true, bool check = false) { - //var RolePos = TranslationController.Instance.currentLanguage.languageID is SupportedLangs.English or SupportedLangs.Russian ? 37 : 34; - //var KillsPos = TranslationController.Instance.currentLanguage.languageID is SupportedLangs.English or SupportedLangs.Russian ? 14 : 12; - string name = Main.AllPlayerNames[id].RemoveHtmlTags().Replace("\r\n", string.Empty); - - if (id == PlayerControl.LocalPlayer.PlayerId) - name = DataManager.player.Customization.Name; - else - name = GetPlayerById(id)?.Data.PlayerName ?? name; - - TaskState taskState = Main.PlayerStates[id].TaskState; - string TaskCount; - - if (taskState.HasTasks) + try { - NetworkedPlayerInfo info = GetPlayerInfoById(id); - Color TaskCompleteColor = HasTasks(info) ? Color.green : Color.cyan; - Color NonCompleteColor = HasTasks(info) ? Color.yellow : Color.white; + string name = Main.AllPlayerNames[id].RemoveHtmlTags().Replace("\r\n", string.Empty); - if (Workhorse.IsThisRole(id)) NonCompleteColor = Workhorse.RoleColor; + if (id == PlayerControl.LocalPlayer.PlayerId) + name = DataManager.player.Customization.Name; + else + name = GetPlayerById(id)?.Data.PlayerName ?? name; - Color NormalColor = taskState.IsTaskFinished ? TaskCompleteColor : NonCompleteColor; + TaskState taskState = Main.PlayerStates[id].TaskState; + string TaskCount; - if (Main.PlayerStates.TryGetValue(id, out PlayerState ps)) + if (taskState.HasTasks) { - NormalColor = ps.MainRole switch + NetworkedPlayerInfo info = GetPlayerInfoById(id); + Color TaskCompleteColor = HasTasks(info) ? Color.green : Color.cyan; + Color NonCompleteColor = HasTasks(info) ? Color.yellow : Color.white; + + if (Workhorse.IsThisRole(id)) NonCompleteColor = Workhorse.RoleColor; + + Color NormalColor = taskState.IsTaskFinished ? TaskCompleteColor : NonCompleteColor; + + if (Main.PlayerStates.TryGetValue(id, out PlayerState ps)) { - CustomRoles.Crewpostor => Color.red, - CustomRoles.Cherokious => GetRoleColor(CustomRoles.Cherokious), - _ => NormalColor - }; + NormalColor = ps.MainRole switch + { + CustomRoles.Crewpostor => Color.red, + CustomRoles.Cherokious => GetRoleColor(CustomRoles.Cherokious), + _ => NormalColor + }; + } + + Color TextColor = NormalColor; + var Completed = $"{taskState.CompletedTasksCount}"; + TaskCount = ColorString(TextColor, $" ({Completed}/{taskState.AllTasksCount})"); + } + else + TaskCount = string.Empty; + + var summary = $"{ColorString(Main.PlayerColors[id], name)} - {GetDisplayRoleName(id, true)}{TaskCount}{GetKillCountText(id)} ({GetVitalText(id, true)})"; + + switch (Options.CurrentGameMode) + { + case CustomGameMode.SoloKombat: + summary = TranslationController.Instance.currentLanguage.languageID is SupportedLangs.SChinese or SupportedLangs.TChinese ? $"{GetProgressText(id)}\t{ColorString(Main.PlayerColors[id], name)}" : $"{ColorString(Main.PlayerColors[id], name)}{GetProgressText(id)}"; + if (GetProgressText(id).Trim() == string.Empty) return string.Empty; + + break; + case CustomGameMode.FFA: + summary = $"{ColorString(Main.PlayerColors[id], name)} {GetKillCountText(id, true)}"; + break; + case CustomGameMode.Speedrun: + case CustomGameMode.MoveAndStop: + summary = $"{ColorString(Main.PlayerColors[id], name)} -{TaskCount.Replace("(", string.Empty).Replace(")", string.Empty)} ({GetVitalText(id, true)})"; + break; + case CustomGameMode.HotPotato: + int time = HotPotato.GetSurvivalTime(id); + summary = $"{ColorString(Main.PlayerColors[id], name)} - <#e8cd46>{GetString("SurvivedTimePrefix")}: <#ffffff>{(time == 0 ? $"{GetString("SurvivedUntilTheEnd")}" : $"{time}s")} ({GetVitalText(id, true)})"; + break; + case CustomGameMode.NaturalDisasters: + int time2 = NaturalDisasters.SurvivalTime(id); + summary = $"{ColorString(Main.PlayerColors[id], name)} - <#e8cd46>{GetString("SurvivedTimePrefix")}: <#ffffff>{(time2 == 0 ? $"{GetString("SurvivedUntilTheEnd")}" : $"{time2}s")} ({GetVitalText(id, true)})"; + break; + case CustomGameMode.RoomRush: + int time3 = RoomRush.GetSurvivalTime(id); + summary = $"{ColorString(Main.PlayerColors[id], name)} - <#e8cd46>{GetString("SurvivedTimePrefix")}: <#ffffff>{(time3 == 0 ? $"{GetString("SurvivedUntilTheEnd")}" : $"{time3}s")} ({GetVitalText(id, true)})"; + break; + case CustomGameMode.CaptureTheFlag: + summary = $"{ColorString(Main.PlayerColors[id], name)}: {CTFManager.GetStatistics(id)}"; + if (CTFManager.IsDeathPossible) summary += $" ({GetVitalText(id, true)})"; + break; + case CustomGameMode.AllInOne: + string survivalTimeText = !Main.PlayerStates[id].IsDead ? string.Empty : $" ({GetString("SurvivedTimePrefix")}: <#f542ad>{RoomRush.GetSurvivalTime(id)}s)"; + summary = $"{ColorString(Main.PlayerColors[id], name)} -{TaskCount}{GetKillCountText(id, true)} ({GetVitalText(id, true)}){survivalTimeText}"; + break; } - Color TextColor = NormalColor; - var Completed = $"{taskState.CompletedTasksCount}"; - TaskCount = ColorString(TextColor, $" ({Completed}/{taskState.AllTasksCount})"); + return check && GetDisplayRoleName(id, true).RemoveHtmlTags().Contains("INVALID:NotAssigned") + ? "INVALID" + : disableColor + ? summary.RemoveHtmlTags() + : summary; } - else - TaskCount = string.Empty; - - var summary = $"{ColorString(Main.PlayerColors[id], name)} - {GetDisplayRoleName(id, true)}{TaskCount}{GetKillCountText(id)} ({GetVitalText(id, true)})"; - - switch (Options.CurrentGameMode) + catch (Exception e) { - case CustomGameMode.SoloKombat: - summary = TranslationController.Instance.currentLanguage.languageID is SupportedLangs.SChinese or SupportedLangs.TChinese ? $"{GetProgressText(id)}\t{ColorString(Main.PlayerColors[id], name)}" : $"{ColorString(Main.PlayerColors[id], name)}{GetProgressText(id)}"; - if (GetProgressText(id).Trim() == string.Empty) return string.Empty; - - break; - case CustomGameMode.FFA: - summary = $"{ColorString(Main.PlayerColors[id], name)} {GetKillCountText(id, true)}"; - break; - case CustomGameMode.Speedrun: - case CustomGameMode.MoveAndStop: - summary = $"{ColorString(Main.PlayerColors[id], name)} -{TaskCount.Replace("(", string.Empty).Replace(")", string.Empty)} ({GetVitalText(id, true)})"; - break; - case CustomGameMode.HotPotato: - int time = HotPotato.GetSurvivalTime(id); - summary = $"{ColorString(Main.PlayerColors[id], name)} - <#e8cd46>{GetString("SurvivedTimePrefix")}: <#ffffff>{(time == 0 ? $"{GetString("SurvivedUntilTheEnd")}" : $"{time}s")} ({GetVitalText(id, true)})"; - break; - case CustomGameMode.NaturalDisasters: - int time2 = NaturalDisasters.SurvivalTime(id); - summary = $"{ColorString(Main.PlayerColors[id], name)} - <#e8cd46>{GetString("SurvivedTimePrefix")}: <#ffffff>{(time2 == 0 ? $"{GetString("SurvivedUntilTheEnd")}" : $"{time2}s")} ({GetVitalText(id, true)})"; - break; - case CustomGameMode.RoomRush: - int time3 = RoomRush.GetSurvivalTime(id); - summary = $"{ColorString(Main.PlayerColors[id], name)} - <#e8cd46>{GetString("SurvivedTimePrefix")}: <#ffffff>{(time3 == 0 ? $"{GetString("SurvivedUntilTheEnd")}" : $"{time3}s")} ({GetVitalText(id, true)})"; - break; - case CustomGameMode.CaptureTheFlag: - summary = $"{ColorString(Main.PlayerColors[id], name)}: {CTFManager.GetStatistics(id)}"; - if (CTFManager.IsDeathPossible) summary += $" ({GetVitalText(id, true)})"; - break; - case CustomGameMode.AllInOne: - string survivalTimeText = !Main.PlayerStates[id].IsDead ? string.Empty : $" ({GetString("SurvivedTimePrefix")}: <#f542ad>{RoomRush.GetSurvivalTime(id)}s)"; - summary = $"{ColorString(Main.PlayerColors[id], name)} -{TaskCount}{GetKillCountText(id, true)} ({GetVitalText(id, true)}){survivalTimeText}"; - break; + ThrowException(e); + return $"{id.ColoredPlayerName()} - ERROR"; } - - return check && GetDisplayRoleName(id, true).RemoveHtmlTags().Contains("INVALID:NotAssigned") - ? "INVALID" - : disableColor - ? summary.RemoveHtmlTags() - : summary; } public static string GetRemainingKillers(bool notify = false, bool president = false) diff --git a/Patches/ClientPatch.cs b/Patches/ClientPatch.cs index 2849fe7ed..12d7e9d06 100644 --- a/Patches/ClientPatch.cs +++ b/Patches/ClientPatch.cs @@ -1,355 +1,443 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using EHR.Modules; using HarmonyLib; using Hazel; -using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem.Collections.Generic; using InnerNet; using TMPro; using UnityEngine; using static EHR.Translator; -namespace EHR +namespace EHR; + +[HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.MakePublic))] +internal static class MakePublicPatch { - [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.MakePublic))] - internal static class MakePublicPatch + public static bool Prefix() { - public static bool Prefix() + if (ModUpdater.IsBroken || (ModUpdater.HasUpdate && ModUpdater.ForceUpdate) || !VersionChecker.IsSupported) { - if (ModUpdater.IsBroken || (ModUpdater.HasUpdate && ModUpdater.ForceUpdate) || !VersionChecker.IsSupported) - { - var message = string.Empty; - if (!VersionChecker.IsSupported) message = GetString("UnsupportedVersion"); + var message = string.Empty; + if (!VersionChecker.IsSupported) message = GetString("UnsupportedVersion"); - if (ModUpdater.IsBroken) message = GetString("ModBrokenMessage"); + if (ModUpdater.IsBroken) message = GetString("ModBrokenMessage"); - if (ModUpdater.HasUpdate) message = GetString("CanNotJoinPublicRoomNoLatest"); + if (ModUpdater.HasUpdate) message = GetString("CanNotJoinPublicRoomNoLatest"); - Logger.Info(message, "MakePublicPatch"); - Logger.SendInGame(message); - return false; - } - - return true; + Logger.Info(message, "MakePublicPatch"); + Logger.SendInGame(message); + return false; } - } - [HarmonyPatch(typeof(MMOnlineManager), nameof(MMOnlineManager.Start))] + return true; + } +} + +[HarmonyPatch(typeof(MMOnlineManager), nameof(MMOnlineManager.Start))] // ReSharper disable once InconsistentNaming - internal static class MMOnlineManagerStartPatch +internal static class MMOnlineManagerStartPatch +{ + public static void Postfix() { - public static void Postfix() + if (!((ModUpdater.HasUpdate && ModUpdater.ForceUpdate) || ModUpdater.IsBroken)) return; + + GameObject obj = GameObject.Find("FindGameButton"); + + if (obj) { - if (!((ModUpdater.HasUpdate && ModUpdater.ForceUpdate) || ModUpdater.IsBroken)) return; + obj.SetActive(false); + TextMeshPro textObj = Object.Instantiate(obj.transform.FindChild("Text_TMP").GetComponent()); + textObj.transform.position = new(1f, -0.3f, 0); + textObj.name = "CanNotJoinPublic"; - GameObject obj = GameObject.Find("FindGameButton"); + string message = ModUpdater.IsBroken + ? $"{Utils.ColorString(Color.red, GetString("ModBrokenMessage"))}" + : $"{Utils.ColorString(Color.red, GetString("CanNotJoinPublicRoomNoLatest"))}"; - if (obj) - { - obj.SetActive(false); - TextMeshPro textObj = Object.Instantiate(obj.transform.FindChild("Text_TMP").GetComponent()); - textObj.transform.position = new(1f, -0.3f, 0); - textObj.name = "CanNotJoinPublic"; - - string message = ModUpdater.IsBroken - ? $"{Utils.ColorString(Color.red, GetString("ModBrokenMessage"))}" - : $"{Utils.ColorString(Color.red, GetString("CanNotJoinPublicRoomNoLatest"))}"; - - LateTask.New(() => { textObj.text = message; }, 0.01f, "CanNotJoinPublic"); - } + LateTask.New(() => { textObj.text = message; }, 0.01f, "CanNotJoinPublic"); } } +} - [HarmonyPatch(typeof(SplashManager), nameof(SplashManager.Update))] - internal static class SplashLogoAnimatorPatch +[HarmonyPatch(typeof(SplashManager), nameof(SplashManager.Update))] +internal static class SplashLogoAnimatorPatch +{ + public static void Prefix(SplashManager __instance) { - public static void Prefix(SplashManager __instance) - { - __instance.sceneChanger.AllowFinishLoadingScene(); - __instance.startedSceneLoad = true; - } + __instance.sceneChanger.AllowFinishLoadingScene(); + __instance.startedSceneLoad = true; } +} - [HarmonyPatch(typeof(EOSManager), nameof(EOSManager.IsAllowedOnline))] - internal static class RunLoginPatch +[HarmonyPatch(typeof(EOSManager), nameof(EOSManager.IsAllowedOnline))] +internal static class RunLoginPatch +{ + public const int ClickCount = 0; + + public static void Prefix(ref bool canOnline) { - public const int ClickCount = 0; - - public static void Prefix(ref bool canOnline) - { - if (DebugModeManager.AmDebugger) canOnline = true; - } + if (DebugModeManager.AmDebugger) canOnline = true; } +} - [HarmonyPatch(typeof(BanMenu), nameof(BanMenu.SetVisible))] - internal static class BanMenuSetVisiblePatch +[HarmonyPatch(typeof(BanMenu), nameof(BanMenu.SetVisible))] +internal static class BanMenuSetVisiblePatch +{ + public static bool Prefix(BanMenu __instance, bool show) { - public static bool Prefix(BanMenu __instance, bool show) - { - if (!AmongUsClient.Instance.AmHost) return true; + if (!AmongUsClient.Instance.AmHost) return true; - show &= PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data != null; - __instance.BanButton.gameObject.SetActive(AmongUsClient.Instance.CanBan()); - __instance.KickButton.gameObject.SetActive(AmongUsClient.Instance.CanKick()); - __instance.MenuButton.gameObject.SetActive(show); + show &= PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data != null; + __instance.BanButton.gameObject.SetActive(AmongUsClient.Instance.CanBan()); + __instance.KickButton.gameObject.SetActive(AmongUsClient.Instance.CanKick()); + __instance.MenuButton.gameObject.SetActive(show); + return false; + } +} + +[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.CanBan))] +internal static class InnerNetClientCanBanPatch +{ + public static bool Prefix(InnerNetClient __instance, ref bool __result) + { + __result = __instance.AmHost; + return false; + } +} + +[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.KickPlayer))] +internal static class KickPlayerPatch +{ + public static bool Prefix( /*InnerNetClient __instance,*/ int clientId, bool ban) + { + if (!AmongUsClient.Instance.AmHost && !OnGameJoinedPatch.JoiningGame) return true; + + if (AmongUsClient.Instance.ClientId == clientId) + { + Logger.SendInGame($"Game Attempting to {(ban ? "Ban" : "Kick")} Host, Blocked the attempt."); + Logger.Info("Game attempted to kick/ban host....", "KickPlayerPatch"); return false; } + + if (ban) BanManager.AddBanPlayer(AmongUsClient.Instance.GetRecentClient(clientId)); + + return true; } +} - [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.CanBan))] - internal static class InnerNetClientCanBanPatch +[HarmonyPatch(typeof(ResolutionManager), nameof(ResolutionManager.SetResolution))] +internal static class SetResolutionManager +{ + public static void Postfix() { - public static bool Prefix(InnerNetClient __instance, ref bool __result) - { - __result = __instance.AmHost; - return false; - } + if (MainMenuManagerPatch.UpdateButton != null) MainMenuManagerPatch.UpdateButton.transform.localPosition = MainMenuManagerPatch.Template.transform.localPosition + new Vector3(0.25f, 0.75f); } +} - [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.KickPlayer))] - internal static class KickPlayerPatch +[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.SendAllStreamedObjects))] +internal static class InnerNetObjectSerializePatch +{ + public static void Prefix() { - public static bool Prefix( /*InnerNetClient __instance,*/ int clientId, bool ban) - { - if (!AmongUsClient.Instance.AmHost && !OnGameJoinedPatch.JoiningGame) return true; + if (AmongUsClient.Instance.AmHost) Main.Instance.StartCoroutine(GameOptionsSender.SendAllGameOptionsAsync()); + } +} - if (AmongUsClient.Instance.ClientId == clientId) +[HarmonyPatch(typeof(InnerNetClient))] +public class InnerNetClientPatch +{ + private static byte Timer; + + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.SendInitialData))] + [HarmonyPrefix] + public static bool SendInitialDataPrefix(InnerNetClient __instance, int clientId) + { + if (!Constants.IsVersionModded() || __instance.NetworkMode != NetworkModes.OnlineGame) return true; + MessageWriter messageWriter = MessageWriter.Get(SendOption.Reliable); + messageWriter.StartMessage(6); + messageWriter.Write(__instance.GameId); + messageWriter.WritePacked(clientId); + List obj = __instance.allObjects; + + lock (obj) + { + System.Collections.Generic.HashSet hashSet = []; + + for (int i = 0; i < __instance.allObjects.Count; i++) { - Logger.SendInGame($"Game Attempting to {(ban ? "Ban" : "Kick")} Host, Blocked the attempt."); - Logger.Info("Game attempted to kick/ban host....", "KickPlayerPatch"); - return false; + InnerNetObject innerNetObject = __instance.allObjects[i]; // False error + + if (innerNetObject && (innerNetObject.OwnerId != -4 || __instance.AmModdedHost) && hashSet.Add(innerNetObject.gameObject)) + { + GameManager gameManager = innerNetObject as GameManager; + + if (gameManager != null) + { + __instance.SendGameManager(clientId, gameManager); + } + else + { + if (innerNetObject is not NetworkedPlayerInfo) + __instance.WriteSpawnMessage(innerNetObject, innerNetObject.OwnerId, innerNetObject.SpawnFlags, messageWriter); + } + } } - if (ban) BanManager.AddBanPlayer(AmongUsClient.Instance.GetRecentClient(clientId)); - - return true; + messageWriter.EndMessage(); + __instance.SendOrDisconnect(messageWriter); + messageWriter.Recycle(); } + + DelaySpawnPlayerInfo(__instance, clientId); + return false; } - [HarmonyPatch(typeof(ResolutionManager), nameof(ResolutionManager.SetResolution))] - internal static class SetResolutionManager + private static void DelaySpawnPlayerInfo(InnerNetClient __instance, int clientId) { - public static void Postfix() + var players = GameData.Instance.AllPlayers.ToArray().ToList(); + + while (players.Count > 0) { - if (MainMenuManagerPatch.UpdateButton != null) MainMenuManagerPatch.UpdateButton.transform.localPosition = MainMenuManagerPatch.Template.transform.localPosition + new Vector3(0.25f, 0.75f); - } - } + var batch = players.Take(5).ToList(); - [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.SendAllStreamedObjects))] - internal static class InnerNetObjectSerializePatch - { - public static void Prefix() - { - if (AmongUsClient.Instance.AmHost) Main.Instance.StartCoroutine(GameOptionsSender.SendAllGameOptionsAsync()); - } - } - - internal static class InnerNetClientPatch - { - private static byte Timer; - - [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.SendInitialData))] - [HarmonyPrefix] - public static bool SendInitialDataPrefix(InnerNetClient __instance, int clientId) - { - if (!Constants.IsVersionModded() || GameStates.IsInGame || __instance.NetworkMode != NetworkModes.OnlineGame) return true; - - // We make sure other stuff like PlayerControl and LobbyBehavior is spawned properly - // Then we spawn the networked data for new clients MessageWriter messageWriter = MessageWriter.Get(SendOption.Reliable); messageWriter.StartMessage(6); messageWriter.Write(__instance.GameId); messageWriter.WritePacked(clientId); - List obj = __instance.allObjects; - lock (obj) + foreach (var player in batch) { - System.Collections.Generic.HashSet hashSet = []; + if (messageWriter.Length > 500) break; - for (var i = 0; i < __instance.allObjects.Count; i++) + if (player != null && player.ClientId != clientId && !player.Disconnected) { - InnerNetObject innerNetObject = __instance.allObjects[i]; // False error - - if (innerNetObject && (innerNetObject.OwnerId != -4 || __instance.AmModdedHost) && hashSet.Add(innerNetObject.gameObject)) - { - var gameManager = innerNetObject as GameManager; - - if (gameManager != null) - __instance.SendGameManager(clientId, gameManager); - else - { - if (innerNetObject is not NetworkedPlayerInfo) __instance.WriteSpawnMessage(innerNetObject, innerNetObject.OwnerId, innerNetObject.SpawnFlags, messageWriter); - } - } + __instance.WriteSpawnMessage(player, player.OwnerId, player.SpawnFlags, messageWriter); } - messageWriter.EndMessage(); - __instance.SendOrDisconnect(messageWriter); - messageWriter.Recycle(); + players.Remove(player); } - DelaySpawnPlayerInfo(__instance, clientId); - return false; - } - - private static void DelaySpawnPlayerInfo(InnerNetClient __instance, int clientId) - { - Il2CppArrayBase players = GameData.Instance.AllPlayers.ToArray(); - - // We send 5 players at a time to prevent too large packets - foreach (NetworkedPlayerInfo[] batch in players.Chunk(5)) - { - MessageWriter messageWriter = MessageWriter.Get(SendOption.Reliable); - messageWriter.StartMessage(6); - messageWriter.Write(__instance.GameId); - messageWriter.WritePacked(clientId); - - batch.DoIf(p => p != null && p.ClientId != clientId && !p.Disconnected, p => __instance.WriteSpawnMessage(p, p.OwnerId, p.SpawnFlags, messageWriter)); - - messageWriter.EndMessage(); - __instance.SendOrDisconnect(messageWriter); - messageWriter.Recycle(); - } - } - - [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.SendAllStreamedObjects))] - [HarmonyPrefix] - public static bool SendAllStreamedObjectsPrefix(InnerNetClient __instance, ref bool __result) - { - if (!Constants.IsVersionModded() || __instance.NetworkMode != NetworkModes.OnlineGame) return true; - - // Bypass all NetworkedData here. - __result = false; - List obj = __instance.allObjects; - - lock (obj) - { - for (var i = 0; i < __instance.allObjects.Count; i++) - { - InnerNetObject innerNetObject = __instance.allObjects[i]; // False error - - if (innerNetObject && innerNetObject is not NetworkedPlayerInfo && innerNetObject.IsDirty && (innerNetObject.AmOwner || (innerNetObject.OwnerId == -2 && __instance.AmHost))) - { - MessageWriter messageWriter = __instance.Streams[(int)innerNetObject.sendMode]; - messageWriter.StartMessage(1); - messageWriter.WritePacked(innerNetObject.NetId); - - try - { - if (innerNetObject.Serialize(messageWriter, false)) - messageWriter.EndMessage(); - else - messageWriter.CancelMessage(); - - if (innerNetObject.Chunked && innerNetObject.IsDirty) __result = true; - } - catch (Exception ex) - { - Logger.Exception(ex, "SendAllStreamedObjectsPrefix"); - messageWriter.CancelMessage(); - } - } - } - } - - for (var j = 0; j < __instance.Streams.Length; j++) - { - MessageWriter messageWriter2 = __instance.Streams[j]; - - if (messageWriter2.HasBytes(7)) - { - messageWriter2.EndMessage(); - __instance.SendOrDisconnect(messageWriter2); - messageWriter2.Clear((SendOption)j); - messageWriter2.StartMessage(5); - messageWriter2.Write(__instance.GameId); - } - } - - return false; - } - - [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.FixedUpdate))] - [HarmonyPostfix] - public static void FixedUpdatePostfix(InnerNetClient __instance) - { - if (!Constants.IsVersionModded() || GameStates.IsInGame || !__instance.AmHost || __instance.Streams == null || __instance.NetworkMode != NetworkModes.OnlineGame) return; - - if (Timer == 0) - { - Timer = 1; - return; - } - - NetworkedPlayerInfo player = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(x => x.IsDirty); - - if (player != null) - { - Timer = 0; - MessageWriter messageWriter = MessageWriter.Get(SendOption.Reliable); - messageWriter.StartMessage(5); - messageWriter.Write(__instance.GameId); - messageWriter.StartMessage(1); - messageWriter.WritePacked(player.NetId); - - try - { - if (player.Serialize(messageWriter, false)) - messageWriter.EndMessage(); - else - { - messageWriter.CancelMessage(); - player.ClearDirtyBits(); - return; - } - - messageWriter.EndMessage(); - __instance.SendOrDisconnect(messageWriter); - messageWriter.Recycle(); - } - catch (Exception ex) - { - Logger.Exception(ex, "FixedUpdatePostfix"); - messageWriter.CancelMessage(); - player.ClearDirtyBits(); - } - } + messageWriter.EndMessage(); + __instance.SendOrDisconnect(messageWriter); + messageWriter.Recycle(); } } - [HarmonyPatch] - internal static class AuthTimeoutPatch + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.SendAllStreamedObjects))] + [HarmonyPrefix] + public static bool SendAllStreamedObjectsPrefix(InnerNetClient __instance, ref bool __result) { - [HarmonyPatch(typeof(AuthManager._CoConnect_d__4), nameof(AuthManager._CoConnect_d__4.MoveNext))] - [HarmonyPatch(typeof(AuthManager._CoWaitForNonce_d__6), nameof(AuthManager._CoWaitForNonce_d__6.MoveNext))] - [HarmonyPrefix] - // From Reactor.gg - // https://github.com/NuclearPowered/Reactor/blob/master/Reactor/Patches/Miscellaneous/CustomServersPatch.cs - public static bool CoWaitforNoncePrefix(ref bool __result) - { - if (GameStates.IsVanillaServer) return true; + if (!Constants.IsVersionModded() || __instance.NetworkMode != NetworkModes.OnlineGame) return true; + __result = false; + List obj = __instance.allObjects; - __result = false; - return false; + lock (obj) + { + for (int i = 0; i < __instance.allObjects.Count; i++) + { + InnerNetObject innerNetObject = __instance.allObjects[i]; // False error + + if (innerNetObject && innerNetObject is not NetworkedPlayerInfo && innerNetObject.IsDirty && (innerNetObject.AmOwner || (innerNetObject.OwnerId == -2 && __instance.AmHost))) + { + MessageWriter messageWriter = __instance.Streams[(int)innerNetObject.sendMode]; + messageWriter.StartMessage(1); + messageWriter.WritePacked(innerNetObject.NetId); + + try + { + if (innerNetObject.Serialize(messageWriter, false)) + { + messageWriter.EndMessage(); + } + else + { + messageWriter.CancelMessage(); + } + + if (innerNetObject.Chunked && innerNetObject.IsDirty) + { + __result = true; + } + } + catch (Exception ex) + { + Logger.Exception(ex, "SendAllStreamedObjectsPrefix"); + messageWriter.CancelMessage(); + } + } + } } - // If you don't patch this, you still need to wait for 5 s. - // I have no idea why this is happening - [HarmonyPatch(typeof(AmongUsClient._CoJoinOnlinePublicGame_d__1), nameof(AmongUsClient._CoJoinOnlinePublicGame_d__1.MoveNext))] - [HarmonyPrefix] - public static void EnableUdpMatchmakingPrefix(AmongUsClient._CoJoinOnlinePublicGame_d__1 __instance) + for (int j = 0; j < __instance.Streams.Length; j++) { - // Skip to state 1, which just calls CoJoinOnlineGameDirect - if (__instance.__1__state == 0 && !ServerManager.Instance.IsHttp) - { - __instance.__1__state = 1; + MessageWriter messageWriter2 = __instance.Streams[j]; - __instance.__8__1 = new() - { - matchmakerToken = string.Empty - }; + if (messageWriter2.HasBytes(7)) + { + messageWriter2.EndMessage(); + __instance.SendOrDisconnect(messageWriter2); + messageWriter2.Clear((SendOption)j); + messageWriter2.StartMessage(5); + messageWriter2.Write(__instance.GameId); } } + + return false; + } + + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.Spawn))] + [HarmonyPostfix] + public static void Spawn_Postfix(InnerNetClient __instance, InnerNetObject netObjParent, int ownerId = -2, SpawnFlags flags = SpawnFlags.None) + { + if (!Constants.IsVersionModded() || __instance.NetworkMode != NetworkModes.OnlineGame) return; + + if (__instance.AmHost) + { + switch (netObjParent) + { + case NetworkedPlayerInfo playerinfo: + LateTask.New(() => + { + if (playerinfo != null && AmongUsClient.Instance.AmConnected) + { + var client = AmongUsClient.Instance.GetClient(playerinfo.ClientId); + + if (client != null && !client.IsDisconnected()) + { + if (playerinfo.IsIncomplete) + { + Logger.Info($"Disconnecting Client [{client.Id}]{client.PlayerName} {client.FriendCode} for playerinfo timeout", "DelayedNetworkedData"); + AmongUsClient.Instance.SendLateRejection(client.Id, DisconnectReasons.ClientTimeout); + __instance.OnPlayerLeft(client, DisconnectReasons.ClientTimeout); + } + } + } + }, 5f, "PlayerInfo Green Bean Kick", false); + + break; + case PlayerControl player: + LateTask.New(() => + { + if (player != null && !player.notRealPlayer && !player.isDummy && AmongUsClient.Instance.AmConnected) + { + var client = AmongUsClient.Instance.GetClient(player.OwnerId); + + if (client != null && !client.IsDisconnected()) + { + if (player.Data == null || player.Data.IsIncomplete) + { + Logger.Info($"Disconnecting Client [{client.Id}]{client.PlayerName} {client.FriendCode} for playercontrol timeout", "DelayedNetworkedData"); + AmongUsClient.Instance.SendLateRejection(client.Id, DisconnectReasons.ClientTimeout); + __instance.OnPlayerLeft(client, DisconnectReasons.ClientTimeout); + } + } + } + }, 5.5f, "PlayerControl Green Bean Kick", false); + + break; + } + } + + if (!__instance.AmHost) + { + Debug.LogError("Tried to spawn while not host:" + (netObjParent?.ToString())); + } + } + + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.FixedUpdate))] + [HarmonyPostfix] + public static void FixedUpdatePostfix(InnerNetClient __instance) + { + if (!Constants.IsVersionModded() || __instance.NetworkMode != NetworkModes.OnlineGame) return; + if (!__instance.AmHost || __instance.Streams == null) return; + + if (Timer == 0) + { + Timer = 1; + return; + } + + var player = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(x => x.IsDirty); + + if (player != null) + { + Timer = 0; + MessageWriter messageWriter = MessageWriter.Get(SendOption.Reliable); + messageWriter.StartMessage(5); + messageWriter.Write(__instance.GameId); + messageWriter.StartMessage(1); + messageWriter.WritePacked(player.NetId); + + try + { + if (player.Serialize(messageWriter, false)) + { + messageWriter.EndMessage(); + } + else + { + messageWriter.CancelMessage(); + player.ClearDirtyBits(); + return; + } + + messageWriter.EndMessage(); + __instance.SendOrDisconnect(messageWriter); + messageWriter.Recycle(); + } + catch (Exception ex) + { + Logger.Exception(ex, "FixedUpdatePostfix"); + messageWriter.CancelMessage(); + player.ClearDirtyBits(); + } + } + } +} + +[HarmonyPatch(typeof(GameData), nameof(GameData.DirtyAllData))] +internal class DirtyAllDataPatch +{ + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static bool Prefix() + { + return false; + } +} + +[HarmonyPatch] +internal static class AuthTimeoutPatch +{ + [HarmonyPatch(typeof(AuthManager._CoConnect_d__4), nameof(AuthManager._CoConnect_d__4.MoveNext))] + [HarmonyPatch(typeof(AuthManager._CoWaitForNonce_d__6), nameof(AuthManager._CoWaitForNonce_d__6.MoveNext))] + [HarmonyPrefix] + // From Reactor.gg + // https://github.com/NuclearPowered/Reactor/blob/master/Reactor/Patches/Miscellaneous/CustomServersPatch.cs + public static bool CoWaitforNoncePrefix(ref bool __result) + { + if (GameStates.IsVanillaServer) return true; + + __result = false; + return false; + } + + // If you don't patch this, you still need to wait for 5 s. + // I have no idea why this is happening + [HarmonyPatch(typeof(AmongUsClient._CoJoinOnlinePublicGame_d__1), nameof(AmongUsClient._CoJoinOnlinePublicGame_d__1.MoveNext))] + [HarmonyPrefix] + public static void EnableUdpMatchmakingPrefix(AmongUsClient._CoJoinOnlinePublicGame_d__1 __instance) + { + // Skip to state 1, which just calls CoJoinOnlineGameDirect + if (__instance.__1__state == 0 && !ServerManager.Instance.IsHttp) + { + __instance.__1__state = 1; + + __instance.__8__1 = new() + { + matchmakerToken = string.Empty + }; + } } } \ No newline at end of file diff --git a/Patches/Crowded.cs b/Patches/Crowded.cs index 35ff62843..edb484cc7 100644 --- a/Patches/Crowded.cs +++ b/Patches/Crowded.cs @@ -1,33 +1,506 @@ -using AmongUs.GameOptions; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; +using Il2CppInterop.Runtime.Attributes; +using TMPro; +using UnityEngine; + +namespace EHR.Patches; // https://github.com/CrowdedMods/CrowdedMod/blob/master/src/CrowdedMod -namespace EHR.Patches -{ - internal static class Crowded - { - public const int MaxImpostors = 127 / 2; +// Niko adjusted mono behavior patches to fit into non-reactor mods - [HarmonyPatch(typeof(GameOptionsData), nameof(GameOptionsData.AreInvalid))] - public static class InvalidOptionsPatches +internal static class Crowded +{ + private static CreateOptionsPicker Instance; + public static readonly int MaxImpostors = GameOptionsManager.Instance.currentHostOptions.MaxPlayers / 2; + private static int MaxPlayers => GameStates.IsVanillaServer ? 15 : 127; + + [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.Awake))] + public static class CreateOptionsPickerAwake + { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static void Prefix(CreateOptionsPicker __instance) { - public static bool Prefix(GameOptionsData __instance, [HarmonyArgument(0)] int maxExpectedPlayers) + Instance = __instance; + + if (GameStates.IsVanillaServer) { - return __instance.MaxPlayers > maxExpectedPlayers || - __instance.NumImpostors < 1 || - __instance.NumImpostors + 1 > maxExpectedPlayers / 2 || - __instance.KillDistance is < 0 or > 2 || - __instance.PlayerSpeedMod is <= 0f or > 3f; + if (GameOptionsManager.Instance.GameHostOptions != null) + { + if (GameOptionsManager.Instance.GameHostOptions.MaxPlayers > 15) + { + GameOptionsManager.Instance.GameHostOptions.SetInt(Int32OptionNames.MaxPlayers, 15); + } + } } } - [HarmonyPatch(typeof(SecurityLogger), nameof(SecurityLogger.Awake))] - public static class SecurityLoggerPatch + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static void Postfix(CreateOptionsPicker __instance) { - public static void Postfix(ref SecurityLogger __instance) + if (__instance.mode != SettingsMode.Host) return; + { - __instance.Timers = new float[127]; + var firstButtonRenderer = __instance.MaxPlayerButtons[0]; + firstButtonRenderer.GetComponentInChildren().text = "-"; + firstButtonRenderer.enabled = false; + var firstButtonButton = firstButtonRenderer.GetComponent(); + firstButtonButton.OnClick.RemoveAllListeners(); + + firstButtonButton.OnClick.AddListener((Action)(() => + { + for (var i = 1; i < 11; i++) + { + var playerButton = __instance.MaxPlayerButtons[i]; + var tmp = playerButton.GetComponentInChildren(); + var newValue = Mathf.Max(byte.Parse(tmp.text) - 10, byte.Parse(playerButton.name) - 2); + tmp.text = newValue.ToString(); + } + + __instance.UpdateMaxPlayersButtons(__instance.GetTargetOptions()); + })); + + Object.Destroy(firstButtonRenderer); + var lastButtonRenderer = __instance.MaxPlayerButtons[^1]; + lastButtonRenderer.GetComponentInChildren().text = "+"; // False error + lastButtonRenderer.enabled = false; // False error + var lastButtonButton = lastButtonRenderer.GetComponent(); // False error + lastButtonButton.OnClick.RemoveAllListeners(); + + lastButtonButton.OnClick.AddListener((Action)(() => + { + for (var i = 1; i < 11; i++) + { + var playerButton = __instance.MaxPlayerButtons[i]; + var tmp = playerButton.GetComponentInChildren(); + + var newValue = Mathf.Min(byte.Parse(tmp.text) + 10, + MaxPlayers - 14 + byte.Parse(playerButton.name)); + + tmp.text = newValue.ToString(); + } + + __instance.UpdateMaxPlayersButtons(__instance.GetTargetOptions()); + })); + + Object.Destroy(lastButtonRenderer); // False error + + for (var i = 1; i < 11; i++) + { + var playerButton = __instance.MaxPlayerButtons[i].GetComponent(); + var text = playerButton.GetComponentInChildren(); + playerButton.OnClick.RemoveAllListeners(); + + playerButton.OnClick.AddListener((Action)(() => + { + var maxPlayers = byte.Parse(text.text); + var maxImp = Mathf.Min(__instance.GetTargetOptions().NumImpostors, maxPlayers / 2); + __instance.GetTargetOptions().SetInt(Int32OptionNames.NumImpostors, maxImp); + __instance.ImpostorButtons[1].TextMesh.text = maxImp.ToString(); + __instance.SetMaxPlayersButtons(maxPlayers); + })); + } + + foreach (var button in __instance.MaxPlayerButtons) + { + button.enabled = button.GetComponentInChildren().text == __instance.GetTargetOptions().MaxPlayers.ToString(); + } + } + + { + var secondButton = __instance.ImpostorButtons[1]; + secondButton.SpriteRenderer.enabled = false; + Object.Destroy(secondButton.transform.FindChild("ConsoleHighlight").gameObject); + Object.Destroy(secondButton.PassiveButton); + Object.Destroy(secondButton.BoxCollider); + var secondButtonText = secondButton.TextMesh; + secondButtonText.text = __instance.GetTargetOptions().NumImpostors.ToString(); + var firstButton = __instance.ImpostorButtons[0]; + firstButton.SpriteRenderer.enabled = false; + firstButton.TextMesh.text = "-"; + var firstPassiveButton = firstButton.PassiveButton; + firstPassiveButton.OnClick.RemoveAllListeners(); + + firstPassiveButton.OnClick.AddListener((Action)(() => + { + var newVal = Mathf.Clamp( + byte.Parse(secondButtonText.text) - 1, + 1, + __instance.GetTargetOptions().MaxPlayers / 2 + ); + + __instance.SetImpostorButtons(newVal); + secondButtonText.text = newVal.ToString(); + })); + + var thirdButton = __instance.ImpostorButtons[2]; + thirdButton.SpriteRenderer.enabled = false; + thirdButton.TextMesh.text = "+"; + var thirdPassiveButton = thirdButton.PassiveButton; + thirdPassiveButton.OnClick.RemoveAllListeners(); + + thirdPassiveButton.OnClick.AddListener((Action)(() => + { + var newVal = Mathf.Clamp( + byte.Parse(secondButtonText.text) + 1, + 1, + __instance.GetTargetOptions().MaxPlayers / 2 + ); + + __instance.SetImpostorButtons(newVal); + secondButtonText.text = newVal.ToString(); + })); } } } + + [HarmonyPatch(typeof(ServerManager), nameof(ServerManager.SetRegion))] + public static class ServerManager_SetRegion + { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static void Postfix() + { + if (GameStates.IsVanillaServer) + { + if (GameOptionsManager.Instance.GameHostOptions != null && GameOptionsManager.Instance.GameHostOptions.MaxPlayers > 15) + GameOptionsManager.Instance.GameHostOptions.SetInt(Int32OptionNames.MaxPlayers, 15); + + if (Instance) + { + for (var i = 1; i < 11; i++) + { + var playerButton = Instance.MaxPlayerButtons[i]; + var tmp = playerButton.GetComponentInChildren(); + + var newValue = Mathf.Min(byte.Parse(tmp.text) + 10, + MaxPlayers - 14 + byte.Parse(playerButton.name)); + + tmp.text = newValue.ToString(); + } + + Instance.UpdateMaxPlayersButtons(Instance.GetTargetOptions()); + } + } + } + } + + [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.UpdateMaxPlayersButtons))] + public static class CreateOptionsPickerUpdateMaxPlayersButtons + { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static bool Prefix(CreateOptionsPicker __instance, [HarmonyArgument(0)] IGameOptions opts) + { + if (__instance.mode != SettingsMode.Host) return true; + + if (__instance.CrewArea) + { + __instance.CrewArea.SetCrewSize(opts.MaxPlayers, opts.NumImpostors); + } + + var selectedAsString = opts.MaxPlayers.ToString(); + + for (var i = 1; i < __instance.MaxPlayerButtons.Count - 1; i++) + { + __instance.MaxPlayerButtons[i].enabled = __instance.MaxPlayerButtons[i].GetComponentInChildren().text == selectedAsString; // False errors + } + + return false; + } + } + + [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.UpdateImpostorsButtons))] + public static class CreateOptionsPickerUpdateImpostorsButtons + { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static bool Prefix(CreateOptionsPicker __instance) + { + return __instance.mode != SettingsMode.Host; + } + } + + [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.SetImpostorButtons))] + public static class CreateOptionsPickerSetImpostorButtons + { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static bool Prefix(CreateOptionsPicker __instance, int numImpostors) + { + if (__instance.mode != SettingsMode.Host) return true; + IGameOptions targetOptions = __instance.GetTargetOptions(); + targetOptions.SetInt(Int32OptionNames.NumImpostors, numImpostors); + __instance.SetTargetOptions(targetOptions); + __instance.UpdateImpostorsButtons(numImpostors); + return false; + } + } + + [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.SetMaxPlayersButtons))] + public static class CreateOptionsPickerSetMaxPlayersButtons + { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static bool Prefix(CreateOptionsPicker __instance, int maxPlayers) + { + if (DestroyableSingleton.InstanceExists || __instance.mode != SettingsMode.Host) + { + return true; + } + + IGameOptions targetOptions = __instance.GetTargetOptions(); + targetOptions.SetInt(Int32OptionNames.MaxPlayers, maxPlayers); + __instance.SetTargetOptions(targetOptions); + __instance.UpdateMaxPlayersButtons(targetOptions); + return false; + } + } + + [HarmonyPatch(typeof(GameOptionsData), nameof(GameOptionsData.AreInvalid))] + public static class InvalidOptionsPatches + { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static bool Prefix(GameOptionsData __instance, [HarmonyArgument(0)] int maxExpectedPlayers) + { + return __instance.MaxPlayers > maxExpectedPlayers || + __instance.NumImpostors < 1 || + __instance.NumImpostors + 1 > maxExpectedPlayers / 2 || + __instance.KillDistance is < 0 or > 2 || + __instance.PlayerSpeedMod is <= 0f or > 3f; + } + } + + [HarmonyPatch(typeof(SecurityLogger), nameof(SecurityLogger.Awake))] + public static class SecurityLoggerPatch + { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static void Postfix(ref SecurityLogger __instance) + { + __instance.Timers = new float[127]; + } + } + + [HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.Update))] + public static class PlayerTabIsSelectedItemEquippedPatch + { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static void Postfix(PlayerTab __instance) + { + if (GameOptionsManager.Instance.CurrentGameOptions.MaxPlayers > 15) + { + __instance.currentColorIsEquipped = false; + } + } + } + + [HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.UpdateAvailableColors))] + public static class PlayerTabUpdateAvailableColorsPatch + { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public static bool Prefix(PlayerTab __instance) + { + if (GameOptionsManager.Instance.CurrentGameOptions.MaxPlayers <= 15) + { + return true; + } + + __instance.AvailableColors.Clear(); + + for (var i = 0; i < Palette.PlayerColors.Count; i++) + { + if (!PlayerControl.LocalPlayer || PlayerControl.LocalPlayer.CurrentOutfit.ColorId != i) + { + __instance.AvailableColors.Add(i); + } + } + + return false; + } + } +} + +public class AbstractPagingBehaviour(IntPtr ptr) : MonoBehaviour(ptr) +{ + protected const string PageIndexGameObjectName = "CrowdedMod_PageIndex"; + + // ReSharper disable once ReplaceWithFieldKeyword + private int _page; + + protected static int MaxPerPage => 15; + + public virtual int PageIndex + { + get => _page; + set + { + _page = value; + OnPageChanged(); + } + } + + protected virtual int MaxPageIndex => throw new("MaxPageIndex must be overridden"); + public virtual void Start() => OnPageChanged(); + + public virtual void Update() + { + if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.LeftArrow) || Input.mouseScrollDelta.y > 0f) + Cycle(false); + else if (Input.GetKeyDown(KeyCode.DownArrow) || Input.GetKeyDown(KeyCode.RightArrow) || Input.mouseScrollDelta.y < 0f) + Cycle(true); + } + + public virtual void OnPageChanged() => throw new("OnPageChanged must be overridden"); + + /// + /// Loops around if you go over the limits.
+ /// Attempting to go up a page while on the first page will take you to the last page and vice versa. + ///
+ public virtual void Cycle(bool increment) + { + var change = increment ? 1 : -1; + PageIndex = Mathf.Clamp(PageIndex + change, 0, MaxPageIndex); + } +} + +public class MeetingHudPagingBehaviour(IntPtr ptr) : AbstractPagingBehaviour(ptr) +{ + private readonly MeetingHud meetingHud = null!; + [HideFromIl2Cpp] private IEnumerable Targets => meetingHud.playerStates.OrderBy(p => p.AmDead); + + protected override int MaxPageIndex => (Targets.Count() - 1) / MaxPerPage; + public override void Start() => OnPageChanged(); + + public override void Update() + { + base.Update(); + // Sometimes the timer text is spammed with the page counter for some eccentric reason, so this is just a Band-Aid fix for it + if (meetingHud.state is MeetingHud.VoteStates.Animating or MeetingHud.VoteStates.Proceeding || meetingHud.TimerText.text.Contains($" ({PageIndex + 1}/{MaxPageIndex + 1})")) return; // TimerText does not update there + meetingHud.TimerText.text += $" ({PageIndex + 1}/{MaxPageIndex + 1})"; + } + + public override void OnPageChanged() + { + var i = 0; + + foreach (var button in Targets) + { + if (i >= PageIndex * MaxPerPage && i < (PageIndex + 1) * MaxPerPage) + { + button.gameObject.SetActive(true); + var relativeIndex = i % MaxPerPage; + var row = relativeIndex / 3; + var col = relativeIndex % 3; + var buttonTransform = button.transform; + + buttonTransform.localPosition = meetingHud.VoteOrigin + + new Vector3( + meetingHud.VoteButtonOffsets.x * col, + meetingHud.VoteButtonOffsets.y * row, + buttonTransform.localPosition.z + ); + } + else button.gameObject.SetActive(false); + + i++; + } + } +} + +public class ShapeShifterPagingBehaviour(IntPtr ptr) : AbstractPagingBehaviour(ptr) +{ + public ShapeshifterMinigame shapeshifterMinigame = null!; + private TextMeshPro PageText = null!; + [HideFromIl2Cpp] private IEnumerable Targets => shapeshifterMinigame.potentialVictims.ToArray(); + + protected override int MaxPageIndex => (Targets.Count() - 1) / MaxPerPage; + + public override void Start() + { + PageText = Instantiate(HudManager.Instance.KillButton.cooldownTimerText, shapeshifterMinigame.transform); + PageText.name = PageIndexGameObjectName; + PageText.enableWordWrapping = false; + PageText.gameObject.SetActive(true); + PageText.transform.localPosition = new(4.1f, -2.36f, -1f); + PageText.transform.localScale *= 0.5f; + OnPageChanged(); + } + + public override void OnPageChanged() + { + PageText.text = $"({PageIndex + 1}/{MaxPageIndex + 1})"; + var i = 0; + + foreach (var panel in Targets) + { + if (i >= PageIndex * MaxPerPage && i < (PageIndex + 1) * MaxPerPage) + { + panel.gameObject.SetActive(true); + var relativeIndex = i % MaxPerPage; + var row = relativeIndex / 3; + var col = relativeIndex % 3; + var buttonTransform = panel.transform; + + buttonTransform.localPosition = + new( + shapeshifterMinigame.XStart + shapeshifterMinigame.XOffset * col, + shapeshifterMinigame.YStart + shapeshifterMinigame.YOffset * row, + buttonTransform.localPosition.z + ); + } + else panel.gameObject.SetActive(false); + + i++; + } + } +} + +public class VitalsPagingBehaviour(IntPtr ptr) : AbstractPagingBehaviour(ptr) +{ + public VitalsMinigame vitalsMinigame = null!; + private TextMeshPro PageText = null!; + [HideFromIl2Cpp] private IEnumerable Targets => vitalsMinigame.vitals.ToArray(); + + protected override int MaxPageIndex => (Targets.Count() - 1) / MaxPerPage; + + public override void Start() + { + PageText = Instantiate(HudManager.Instance.KillButton.cooldownTimerText, vitalsMinigame.transform); + PageText.name = PageIndexGameObjectName; + PageText.enableWordWrapping = false; + PageText.gameObject.SetActive(true); + PageText.transform.localPosition = new(2.7f, -2f, -1f); + PageText.transform.localScale *= 0.5f; + OnPageChanged(); + } + + public override void OnPageChanged() + { + if (PlayerTask.PlayerHasTaskOfType(PlayerControl.LocalPlayer)) + return; + + PageText.text = $"({PageIndex + 1}/{MaxPageIndex + 1})"; + var i = 0; + + foreach (var panel in Targets) + { + if (i >= PageIndex * MaxPerPage && i < (PageIndex + 1) * MaxPerPage) + { + panel.gameObject.SetActive(true); + var relativeIndex = i % MaxPerPage; + var row = relativeIndex / 3; + var col = relativeIndex % 3; + var panelTransform = panel.transform; + + panelTransform.localPosition = + new( + vitalsMinigame.XStart + vitalsMinigame.XOffset * col, + vitalsMinigame.YStart + vitalsMinigame.YOffset * row, + panelTransform.localPosition.z + ); + } + else panel.gameObject.SetActive(false); + + i++; + } + } } \ No newline at end of file diff --git a/Patches/GameOptionsMenuPatch.cs b/Patches/GameOptionsMenuPatch.cs index 84145bd7c..c247f1bc0 100644 --- a/Patches/GameOptionsMenuPatch.cs +++ b/Patches/GameOptionsMenuPatch.cs @@ -511,10 +511,9 @@ namespace EHR __instance.Value = (float)Math.Round(__instance.Value, 2); break; case StringNames.GameNumImpostors: - __instance.ValidRange = new(1, Crowded.MaxImpostors); + __instance.ValidRange = new(0, Crowded.MaxImpostors); __instance.Value = (float)Math.Round(__instance.Value, 2); if (DebugModeManager.AmDebugger) __instance.ValidRange.min = 0; - break; } diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index 25a934c8c..a97fd4c8d 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -506,10 +506,7 @@ namespace EHR CustomRoles.NiceGuesser => PlayerControl.LocalPlayer.KillSfx, - CustomRoles.Swooper or - CustomRoles.Wraith or - CustomRoles.Chameleon or - CustomRoles.Drainer + CustomRoles.Drainer => PlayerControl.LocalPlayer.MyPhysics.ImpostorDiscoveredSound, CustomRoles.Addict or @@ -534,10 +531,15 @@ namespace EHR CustomRoles.Noisemaker or CustomRoles.NoisemakerEHR + or CustomRoles.DarkHide + or CustomRoles.Specter => GetIntroSound(RoleTypes.Noisemaker), CustomRoles.Phantom or CustomRoles.PhantomEHR + or CustomRoles.Swooper + or CustomRoles.Wraith + or CustomRoles.Chameleon => GetIntroSound(RoleTypes.Phantom), CustomRoles.Shapeshifter diff --git a/Patches/PlayerJoinAndLeftPatch.cs b/Patches/PlayerJoinAndLeftPatch.cs index b5dcc22c3..8ccfba959 100644 --- a/Patches/PlayerJoinAndLeftPatch.cs +++ b/Patches/PlayerJoinAndLeftPatch.cs @@ -109,7 +109,7 @@ namespace EHR [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerJoined))] internal static class OnPlayerJoinedPatch { - private static bool IsDisconnected(this ClientData client) + public static bool IsDisconnected(this ClientData client) { var __instance = AmongUsClient.Instance; diff --git a/README.md b/README.md index 9b04e9c24..bbe77d60e 100644 --- a/README.md +++ b/README.md @@ -379,6 +379,11 @@ Source language: English > - Reference: Custom Region Installer (adding modded regions) > +### :star: [Crowded](https://github.com/CrowdedMods/CrowdedMod) + +> +> - Crowded is integrated into EHR + ### Endless Host Roles (EHR), with Endless Possibilities, brought to you by the Endless Network. ##### Developers: diff --git a/Roles/Crewmate/Addict.cs b/Roles/Crewmate/Addict.cs index c604fa650..a87b8812d 100644 --- a/Roles/Crewmate/Addict.cs +++ b/Roles/Crewmate/Addict.cs @@ -147,5 +147,10 @@ namespace EHR.Crewmate { hud.AbilityButton?.OverrideText(Translator.GetString("AddictVentButtonText")); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Alchemist.cs b/Roles/Crewmate/Alchemist.cs index 3affa27fe..5ceb2ca2c 100644 --- a/Roles/Crewmate/Alchemist.cs +++ b/Roles/Crewmate/Alchemist.cs @@ -459,5 +459,10 @@ namespace EHR.Crewmate else hud.AbilityButton?.OverrideText(GetString("AlchemistVentButtonText")); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Altruist.cs b/Roles/Crewmate/Altruist.cs index 420dbdf67..5728a934d 100644 --- a/Roles/Crewmate/Altruist.cs +++ b/Roles/Crewmate/Altruist.cs @@ -160,5 +160,10 @@ namespace EHR.Crewmate if (ReviveStartTS != 0) return string.Format(Translator.GetString("AltruistSuffixRevive"), ReviveTime.GetInt() - (Utils.TimeStamp - ReviveStartTS)); return string.Format(Translator.GetString("AltruistSuffix"), Translator.GetString(RevivingMode ? "AltruistReviveMode" : "AltruistReportMode")); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/CameraMan.cs b/Roles/Crewmate/CameraMan.cs index 0b6e057f7..fe901fd3f 100644 --- a/Roles/Crewmate/CameraMan.cs +++ b/Roles/Crewmate/CameraMan.cs @@ -121,5 +121,10 @@ namespace EHR.Crewmate { IsTeleported = false; } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Catcher.cs b/Roles/Crewmate/Catcher.cs index 8dd7a9720..262e3d421 100644 --- a/Roles/Crewmate/Catcher.cs +++ b/Roles/Crewmate/Catcher.cs @@ -180,5 +180,10 @@ namespace EHR.Crewmate return string.Format(Translator.GetString("Catcher.Suffix"), TrapPlaceDelay.GetInt() - (Utils.TimeStamp - DelayStartTS)); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Clairvoyant.cs b/Roles/Crewmate/Clairvoyant.cs index 52bc676d5..55acb3636 100644 --- a/Roles/Crewmate/Clairvoyant.cs +++ b/Roles/Crewmate/Clairvoyant.cs @@ -58,5 +58,10 @@ namespace EHR.Crewmate { UseAbility(pc); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Convener.cs b/Roles/Crewmate/Convener.cs index ea03fa051..b96d59abb 100644 --- a/Roles/Crewmate/Convener.cs +++ b/Roles/Crewmate/Convener.cs @@ -74,5 +74,10 @@ namespace EHR.Crewmate pc.RpcRemoveAbilityUse(); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Dad.cs b/Roles/Crewmate/Dad.cs index 5f3b1fae3..ea96389f1 100644 --- a/Roles/Crewmate/Dad.cs +++ b/Roles/Crewmate/Dad.cs @@ -459,5 +459,10 @@ namespace EHR.Crewmate return sb.ToString(); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Doormaster.cs b/Roles/Crewmate/Doormaster.cs index 94e9db859..bdd8be3d4 100644 --- a/Roles/Crewmate/Doormaster.cs +++ b/Roles/Crewmate/Doormaster.cs @@ -80,5 +80,10 @@ namespace EHR.Crewmate else pc.Notify(Translator.GetString("OutOfAbilityUsesDoMoreTasks")); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/DovesOfNeace.cs b/Roles/Crewmate/DovesOfNeace.cs index 3efa89f47..5a01717b0 100644 --- a/Roles/Crewmate/DovesOfNeace.cs +++ b/Roles/Crewmate/DovesOfNeace.cs @@ -96,5 +96,10 @@ namespace EHR.Crewmate pc.RPCPlayCustomSound("Dove"); pc.Notify(string.Format(Translator.GetString("DovesOfNeaceOnGuard"), pc.GetAbilityUseLimit())); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Drainer.cs b/Roles/Crewmate/Drainer.cs index 3ce21533e..33461a16d 100644 --- a/Roles/Crewmate/Drainer.cs +++ b/Roles/Crewmate/Drainer.cs @@ -113,5 +113,10 @@ namespace EHR.Crewmate PlayersInVents.Clear(); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Druid.cs b/Roles/Crewmate/Druid.cs index cbc073fc0..d2a332939 100644 --- a/Roles/Crewmate/Druid.cs +++ b/Roles/Crewmate/Druid.cs @@ -213,5 +213,10 @@ namespace EHR.Crewmate return sb.ToString(); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Grenadier.cs b/Roles/Crewmate/Grenadier.cs index a996843d9..b8e1c223b 100644 --- a/Roles/Crewmate/Grenadier.cs +++ b/Roles/Crewmate/Grenadier.cs @@ -144,5 +144,10 @@ namespace EHR.Crewmate else pc.Notify(Translator.GetString("OutOfAbilityUsesDoMoreTasks")); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Lighter.cs b/Roles/Crewmate/Lighter.cs index 10d9546eb..266742ebb 100644 --- a/Roles/Crewmate/Lighter.cs +++ b/Roles/Crewmate/Lighter.cs @@ -140,5 +140,10 @@ namespace EHR.Crewmate player.MarkDirtySettings(); } } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Mayor.cs b/Roles/Crewmate/Mayor.cs index 1332e5df0..83227affb 100644 --- a/Roles/Crewmate/Mayor.cs +++ b/Roles/Crewmate/Mayor.cs @@ -95,5 +95,10 @@ namespace EHR.Crewmate OverrideTasksData.Create(9516, TabGroup.CrewmateRoles, CustomRoles.Mayor); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Mole.cs b/Roles/Crewmate/Mole.cs index fa9d9fa74..d1fd1bc41 100644 --- a/Roles/Crewmate/Mole.cs +++ b/Roles/Crewmate/Mole.cs @@ -50,5 +50,10 @@ namespace EHR.Crewmate { pc.TPToRandomVent(); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/NiceHacker.cs b/Roles/Crewmate/NiceHacker.cs index f7346de57..02cd9e981 100644 --- a/Roles/Crewmate/NiceHacker.cs +++ b/Roles/Crewmate/NiceHacker.cs @@ -208,5 +208,10 @@ namespace EHR.Crewmate return sb.ToString(); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Oxyman.cs b/Roles/Crewmate/Oxyman.cs index d21155751..b699e4299 100644 --- a/Roles/Crewmate/Oxyman.cs +++ b/Roles/Crewmate/Oxyman.cs @@ -187,6 +187,11 @@ namespace EHR.Crewmate return $"<#ff0000>O2: {Utils.ColorString(GetLevelColor(), OxygenLevel.ToString())}%"; } + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } + private enum Level { Blind, diff --git a/Roles/Crewmate/Paranoia.cs b/Roles/Crewmate/Paranoia.cs index a5d5eadad..6978bfba7 100644 --- a/Roles/Crewmate/Paranoia.cs +++ b/Roles/Crewmate/Paranoia.cs @@ -76,5 +76,10 @@ namespace EHR.Crewmate pc.NoCheckStartMeeting(pc.Data); } } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Perceiver.cs b/Roles/Crewmate/Perceiver.cs index f43fdadb3..118312ca1 100644 --- a/Roles/Crewmate/Perceiver.cs +++ b/Roles/Crewmate/Perceiver.cs @@ -77,5 +77,10 @@ namespace EHR.Crewmate pc.RpcRemoveAbilityUse(); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Rhapsode.cs b/Roles/Crewmate/Rhapsode.cs index 5fc2fa4c1..643173ba7 100644 --- a/Roles/Crewmate/Rhapsode.cs +++ b/Roles/Crewmate/Rhapsode.cs @@ -131,5 +131,10 @@ namespace EHR.Crewmate return $"\u25b6 ({AbilityDuration.GetInt() - (Utils.TimeStamp - ActivateTimeStamp)}s)"; } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/SecurityGuard.cs b/Roles/Crewmate/SecurityGuard.cs index 904bc1108..57ba4523a 100644 --- a/Roles/Crewmate/SecurityGuard.cs +++ b/Roles/Crewmate/SecurityGuard.cs @@ -109,5 +109,10 @@ namespace EHR.Crewmate player.Notify(Translator.GetString("SecurityGuardSkillStop")); } } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Sentinel.cs b/Roles/Crewmate/Sentinel.cs index 4b987f411..ce8a1eb6f 100644 --- a/Roles/Crewmate/Sentinel.cs +++ b/Roles/Crewmate/Sentinel.cs @@ -201,5 +201,10 @@ namespace EHR.Crewmate state.FinishPatrolling(); }, 0.1f, "SentinelFinishPatrol"); } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/Soothsayer.cs b/Roles/Crewmate/Soothsayer.cs index 8f31bc346..0bcaf9380 100644 --- a/Roles/Crewmate/Soothsayer.cs +++ b/Roles/Crewmate/Soothsayer.cs @@ -49,7 +49,7 @@ namespace EHR.Crewmate public static void OnAnyoneDeath(PlayerControl killer, PlayerControl target) { - Instances.DoIf(x => x.Target == target.PlayerId, _ => Main.AllAlivePlayerControls.Do(p => p.Notify(string.Format(Translator.GetString("SoothsayerDiedNotify"), Utils.ColorString(Main.PlayerColors.GetValueOrDefault(killer.PlayerId), killer.GetRealName())), 10f))); + Instances.DoIf(x => x.Target == killer.PlayerId, _ => Main.AllAlivePlayerControls.Do(p => p.Notify(string.Format(Translator.GetString("SoothsayerDiedNotify"), Utils.ColorString(Main.PlayerColors.GetValueOrDefault(killer.PlayerId), killer.GetRealName())), 10f))); } } } \ No newline at end of file diff --git a/Roles/Crewmate/Telekinetic.cs b/Roles/Crewmate/Telekinetic.cs index 585022623..9ad60b9a6 100644 --- a/Roles/Crewmate/Telekinetic.cs +++ b/Roles/Crewmate/Telekinetic.cs @@ -211,6 +211,11 @@ namespace EHR.Crewmate return $"<#ffffff>{Timer}{base.GetProgressText(playerId, comms)}"; } + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } + private enum Mode { TeleportTarget, diff --git a/Roles/Crewmate/Tether.cs b/Roles/Crewmate/Tether.cs index b58480ef0..67231b154 100644 --- a/Roles/Crewmate/Tether.cs +++ b/Roles/Crewmate/Tether.cs @@ -145,5 +145,10 @@ namespace EHR.Crewmate { return Target != byte.MaxValue && seer.PlayerId == target.PlayerId && seer.PlayerId == TetherId ? $"Target: {Utils.GetPlayerById(Target).GetRealName()}" : string.Empty; } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/TimeMaster.cs b/Roles/Crewmate/TimeMaster.cs index 7d7c065bc..5688a229f 100644 --- a/Roles/Crewmate/TimeMaster.cs +++ b/Roles/Crewmate/TimeMaster.cs @@ -141,5 +141,10 @@ namespace EHR.Crewmate player.Notify(Translator.GetString("TimeMasterSkillStop"), (int)player.GetAbilityUseLimit()); } } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Crewmate/ToiletMaster.cs b/Roles/Crewmate/ToiletMaster.cs index a2edfbb3f..7d0a2cd31 100644 --- a/Roles/Crewmate/ToiletMaster.cs +++ b/Roles/Crewmate/ToiletMaster.cs @@ -304,6 +304,11 @@ namespace EHR.Crewmate return true; } + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } + private enum ToiletVisibilityOptions { Instant, diff --git a/Roles/Crewmate/Veteran.cs b/Roles/Crewmate/Veteran.cs index 68a3e371e..35fd87f07 100644 --- a/Roles/Crewmate/Veteran.cs +++ b/Roles/Crewmate/Veteran.cs @@ -139,5 +139,10 @@ namespace EHR.Crewmate player.Notify(string.Format(Translator.GetString("VeteranOffGuard"), (int)player.GetAbilityUseLimit())); } } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Impostor/AntiAdminer.cs b/Roles/Impostor/AntiAdminer.cs index 06097c8a2..f643c2a40 100644 --- a/Roles/Impostor/AntiAdminer.cs +++ b/Roles/Impostor/AntiAdminer.cs @@ -315,5 +315,10 @@ namespace EHR.Impostor return aa == null ? string.Empty : $"<#ffff00>\u26a0 {Delay.GetInt() - (Utils.TimeStamp - aa.ExtraAbilityStartTimeStamp):N0}"; } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return !IsMonitor || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/Impostor/Swooper.cs b/Roles/Impostor/Swooper.cs index 067a16be5..b84abb103 100644 --- a/Roles/Impostor/Swooper.cs +++ b/Roles/Impostor/Swooper.cs @@ -291,5 +291,10 @@ namespace EHR.Impostor target.RpcCheckAndMurder(target); return false; } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return UsedRole != CustomRoles.Chameleon || pc.GetClosestVent()?.Id == ventId; + } } } \ No newline at end of file diff --git a/Roles/RoleBase.cs b/Roles/RoleBase.cs index f7aba2960..b88892f00 100644 --- a/Roles/RoleBase.cs +++ b/Roles/RoleBase.cs @@ -167,6 +167,17 @@ namespace EHR return seerRole.IsNK() && seerRole == target.GetCustomRole() && seer.GetTeam() == target.GetTeam(); } + protected bool IsThisRole(PlayerControl pc) + { + return pc.GetCustomRole() == Enum.Parse(GetType().Name, true); + } + + protected bool IsThisRole(byte id) + { + var role = Main.PlayerStates.TryGetValue(id, out var state) ? state.MainRole : CustomRoles.NotAssigned; + return role == Enum.Parse(GetType().Name, true); + } + // Option setup simplifier protected OptionSetupHandler StartSetup(int id, bool single = false) { @@ -179,6 +190,7 @@ namespace EHR if (single) Options.SetupSingleRoleOptions(id++, tab, role, hideMaxSetting: true); else Options.SetupRoleOptions(id++, tab, role); + return new(id, tab, role); } } diff --git a/main.cs b/main.cs index 04337ad49..41b26ba3f 100644 --- a/main.cs +++ b/main.cs @@ -12,6 +12,7 @@ using BepInEx.Unity.IL2CPP.Utils.Collections; using EHR; using EHR.Modules; using EHR.Neutral; +using EHR.Patches; using HarmonyLib; using Il2CppInterop.Runtime.Injection; using UnityEngine; @@ -25,6 +26,8 @@ namespace EHR [BepInPlugin(PluginGuid, "EHR", PluginVersion)] [BepInIncompatibility("jp.ykundesu.supernewroles")] [BepInIncompatibility("MalumMenu")] + [BepInIncompatibility("com.ten.thebetterroles")] + [BepInIncompatibility("xyz.crowdedmods.crowdedmod")] [BepInProcess("Among Us.exe")] public class Main : BasePlugin { @@ -709,6 +712,13 @@ namespace EHR handler.Info($"{nameof(ThisAssembly.Git.Tag)}: {ThisAssembly.Git.Tag}"); ClassInjector.RegisterTypeInIl2Cpp(); + ClassInjector.RegisterTypeInIl2Cpp(); + ClassInjector.RegisterTypeInIl2Cpp(); + ClassInjector.RegisterTypeInIl2Cpp(); + + NormalGameOptionsV08.RecommendedImpostors = NormalGameOptionsV08.MaxImpostors = Enumerable.Repeat(127, 127).ToArray(); + NormalGameOptionsV08.MinPlayers = Enumerable.Repeat(4, 127).ToArray(); + HideNSeekGameOptionsV08.MinPlayers = Enumerable.Repeat(4, 127).ToArray(); CustomLogger.ClearLog(); LogMonitor.StartMonitoring();