From 49ee73740bda48e355e6f6f31871b41b7c48dd2c Mon Sep 17 00:00:00 2001 From: D1GQ Date: Sun, 1 Feb 2026 18:17:02 -0600 Subject: [PATCH] Add comments to patches --- src/Patches/Client/AnnouncementPanelPatch.cs | 16 ++++-- src/Patches/Client/ClientPatch.cs | 28 +++++++++- src/Patches/Client/DisconnectPenaltyPatch.cs | 5 +- src/Patches/Client/Discord/DiscordPatch.cs | 11 +++- src/Patches/Client/InnerNetClientPatch.cs | 18 +++++-- .../Client/Managers/MainMenuManagerPatch.cs | 19 ++++++- .../Client/Managers/ModManagerPatch.cs | 8 ++- .../Client/OptionsMenuBehaviourPatch.cs | 22 ++++++++ src/Patches/Client/PrivateLobbyPatch.cs | 32 +++++++++--- src/Patches/Client/SplashIntroPatch.cs | 21 +++++++- .../FindAGameManagerPatch.cs | 22 +++++++- .../FindGameMoreInfoPopupPatch.cs | 16 ++++++ .../Gameplay/Managers/HudManagerPatch.cs | 29 +++++++++-- .../Gameplay/Managers/RoleManagerPatch.cs | 51 +++++++++++++------ .../Gameplay/Player/CosmeticsLayerPatch.cs | 7 +-- .../Gameplay/Player/PlayerControlPatch.cs | 20 +++++++- .../Gameplay/Player/PlayerJoinAndLeftPatch.cs | 18 +++++-- .../Gameplay/Systems/VoteBanSystemPatch.cs | 22 ++++++-- .../Gameplay/UI/Chat/ChatCommandsPatch.cs | 37 ++++++++++++-- src/Patches/Gameplay/UI/Chat/ChatPatch.cs | 30 +++++++++-- 20 files changed, 370 insertions(+), 62 deletions(-) diff --git a/src/Patches/Client/AnnouncementPanelPatch.cs b/src/Patches/Client/AnnouncementPanelPatch.cs index e97eef3..e9ec75c 100644 --- a/src/Patches/Client/AnnouncementPanelPatch.cs +++ b/src/Patches/Client/AnnouncementPanelPatch.cs @@ -15,14 +15,18 @@ internal static class AnnouncementPanelPatch { [HarmonyPatch(typeof(PlayerAnnouncementData), nameof(PlayerAnnouncementData.SetAnnouncements))] [HarmonyPrefix] - private static bool PlayerAnnouncementData_SetModAnnouncements_Prefix(PlayerAnnouncementData __instance, ref Il2CppReferenceArray aRange) + private static void PlayerAnnouncementData_SetModAnnouncements_Prefix(PlayerAnnouncementData __instance, ref Il2CppReferenceArray aRange) { + // Load and process mod news from github ModNews.ProcessModNewsFiles(); + // Sort mod news by date, newest first (higher number = newer date) ModNews.AllModNews.Sort((a1, a2) => DateTime.Compare(DateTime.Parse(a2.Date), DateTime.Parse(a1.Date))); + // Convert all mod news to Announcement objects var finalAllNews = ModNews.AllModNews.Select(n => n.ToAnnouncement()).ToList(); + // Add original game announcements that aren't mod news foreach (var news in aRange) { if (!ModNews.AllModNews.Any(x => x.Number == news.Number)) @@ -31,37 +35,42 @@ internal static class AnnouncementPanelPatch } } + // Sort combined list by date (newest first) using proper date parsing finalAllNews.Sort((a1, a2) => DateTime.Compare( DateTime.Parse(a2.Date, null, DateTimeStyles.RoundtripKind), DateTime.Parse(a1.Date, null, DateTimeStyles.RoundtripKind) )); + // Convert List back to Il2CppReferenceArray for game compatibility aRange = new Il2CppReferenceArray(finalAllNews.Count); for (int i = 0; i < finalAllNews.Count; i++) { aRange[i] = finalAllNews[i]; } - - return true; } [HarmonyPatch(typeof(AnnouncementPanel), nameof(AnnouncementPanel.SetUp))] [HarmonyPostfix] private static void AnnouncementPanel_SetUpPanel_Postfix(AnnouncementPanel __instance, Announcement announcement) { + // Check if this is a mod announcement (mod news have numbers >= 100000) if (announcement.Number >= 100000) { + // Create a GameObject to display the mod icon/label var obj = new GameObject("ModLabel"); obj.transform.SetParent(__instance.transform); + // Position in top-left corner of announcement panel obj.transform.localPosition = new Vector3(-0.8f, 0.13f, 0.5f); obj.transform.localScale = new Vector3(0.9f, 0.9f, 0.9f); + // Add SpriteRenderer to display the icon var renderer = obj.AddComponent(); var modNews = ModNews.AllModNews.Find(a => a.Number == announcement.Number); if (modNews != null) { + // Load appropriate icon based on mod type switch (modNews.NewsType) { case NewsTypes.BAU: @@ -69,6 +78,7 @@ internal static class AnnouncementPanelPatch break; } + // Ensure icon stays within announcement panel bounds renderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; } } diff --git a/src/Patches/Client/ClientPatch.cs b/src/Patches/Client/ClientPatch.cs index acfe13b..98464a7 100644 --- a/src/Patches/Client/ClientPatch.cs +++ b/src/Patches/Client/ClientPatch.cs @@ -18,6 +18,7 @@ internal static class ClientPatch [HarmonyPostfix] private static void AccountTab_Awake_Postfix(AccountTab __instance) { + // Apply custom UI colors to the friends button __instance.signInStatusComponent.friendsButton.SetUIColors(); } @@ -25,24 +26,29 @@ internal static class ClientPatch [HarmonyPrefix] private static bool SignInStatusComponent_SetOnline_Prefix(SignInStatusComponent __instance) { + // Get supported Among Us versions for BAU var varSupportedVersions = BAUPlugin.SupportedAmongUsVersions; Version currentVersion = new(BAUPlugin.AppVersion); Version firstSupportedVersion = new(varSupportedVersions.First()); Version lastSupportedVersion = new(varSupportedVersions.Last()); + // Check if current Among Us version is higher than supported range if (currentVersion > firstSupportedVersion) { var verText = $"{varSupportedVersions.First()}"; + // Format version range if there are multiple supported versions if (firstSupportedVersion != lastSupportedVersion) { verText = $"{varSupportedVersions.Last()} - {varSupportedVersions.First()}"; } + // Show warning popup for newer Among Us version Utils.ShowPopUp($"-= Warning =-\n\n" + $"Better Among Us {BAUPlugin.GetVersionText()}\nsupports Among Us {verText},\n" + $"Among Us {BAUPlugin.AppVersion} is above the supported versions!\n" + $"You may encounter minor to game breaking bugs."); } + // Check if current Among Us version is lower than supported range else if (currentVersion < lastSupportedVersion) { var verText = $"{varSupportedVersions.First()}"; @@ -51,6 +57,7 @@ internal static class ClientPatch verText = $"{varSupportedVersions.Last()} - {varSupportedVersions.First()}"; } + // Show warning popup for older Among Us version Utils.ShowPopUp($"-= Warning =-\n\n" + $"Better Among Us {BAUPlugin.GetVersionText()}\nsupports Among Us {verText},\n" + $"Among Us {BAUPlugin.AppVersion} is below the supported versions!\n" + @@ -64,6 +71,7 @@ internal static class ClientPatch [HarmonyPostfix] private static void AmongUsClient_ExitGame_Postfix([HarmonyArgument(0)] DisconnectReasons reason) { + // Hide custom loading bar when exiting game CustomLoadingBarManager.ToggleLoadingBar(false); Logger_.Log($"Client has left game for: {Enum.GetName(reason)}", "AmongUsClientPatch"); } @@ -72,11 +80,13 @@ internal static class ClientPatch [HarmonyPrefix] private static void AmongUsClient_OnGameEnd_Prefix() { + // Preserve all player GameObjects during scene transitions foreach (var data in GameData.Instance.AllPlayers) { UnityEngine.Object.DontDestroyOnLoad(data.gameObject); } + // Move player GameObjects to active scene after a short delay LateTask.Schedule(() => { foreach (var data in GameData.Instance.AllPlayers) @@ -90,17 +100,22 @@ internal static class ClientPatch [HarmonyPostfix] private static void AmongUsClient_CoStartGame_Postfix(AmongUsClient __instance) { + // Clear in-game chat if chat feature is enabled if (BAUPlugin.ChatInGameplay.Value) { ChatPatch.ClearChat(); } + + // Start custom loading sequence __instance.StartCoroutine(CoLoading()); } private static IEnumerator CoLoading() { + // Show custom loading bar CustomLoadingBarManager.ToggleLoadingBar(true); + // Run different loading logic for host vs client if (GameState.IsHost) { yield return CoLoadingHost(); @@ -110,6 +125,7 @@ internal static class ClientPatch yield return CoLoadingClient(); } + // Mark loading as complete and hide bar after delay CustomLoadingBarManager.SetLoadingPercent(100f, "Complete"); yield return new WaitForSeconds(0.25f); CustomLoadingBarManager.ToggleLoadingBar(false); @@ -120,8 +136,10 @@ internal static class ClientPatch var client = AmongUsClient.Instance.GetClient(AmongUsClient.Instance.ClientId); var clients = AmongUsClient.Instance.allClients; + // Continue loading while there are unassigned roles while (BAUPlugin.AllPlayerControls.Count > 0 && BAUPlugin.AllPlayerControls.Any(pc => !pc.roleAssigned)) { + // Early exit if game ended during loading if (!GameState.IsInGame) { CustomLoadingBarManager.ToggleLoadingBar(false); @@ -131,6 +149,7 @@ internal static class ClientPatch string loadingText = "Initializing Game"; float progress = 0f; + // Progress through different loading stages if (AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started) { loadingText = "Starting Game Session"; @@ -150,6 +169,7 @@ internal static class ClientPatch } else if (BAUPlugin.AllPlayerControls.Any(player => !player.roleAssigned)) { + // Calculate role assignment progress int totalPlayers = BAUPlugin.AllPlayerControls.Count; int assignedPlayers = BAUPlugin.AllPlayerControls.Count(pc => pc.roleAssigned); float assignmentProgress = (float)assignedPlayers / Mathf.Max(1, totalPlayers); @@ -159,6 +179,7 @@ internal static class ClientPatch } else if (!client.IsReady) { + // Wait for other clients to be ready int readyClients = clients.CountIl2Cpp(c => c?.Character != null && c.IsReady); int totalClients = clients.CountIl2Cpp(c => c?.Character != null); @@ -166,6 +187,7 @@ internal static class ClientPatch progress = 0.8f + 0.2f * readyClients / Mathf.Max(1, totalClients); } + // Update loading bar with current progress int percent = Mathf.RoundToInt(progress * 100f); CustomLoadingBarManager.SetLoadingPercent(percent, loadingText); @@ -178,9 +200,10 @@ internal static class ClientPatch var client = AmongUsClient.Instance.GetClient(AmongUsClient.Instance.ClientId); var clients = AmongUsClient.Instance.allClients; + // Client loading logic (similar to host but with some differences) while (BAUPlugin.AllPlayerControls.Count > 0 && BAUPlugin.AllPlayerControls.Any(pc => !pc.roleAssigned)) { - + // Switch to host logic if client becomes host mid-loading if (GameState.IsHost) { yield return CoLoadingHost(); @@ -220,6 +243,7 @@ internal static class ClientPatch } else { + // Wait for other players (including host) to be ready int readyClients = clients.CountIl2Cpp(c => c?.Character != null && c.IsReady); int totalClients = clients.CountIl2Cpp(c => c?.Character != null); @@ -233,4 +257,4 @@ internal static class ClientPatch yield return null; } } -} +} \ No newline at end of file diff --git a/src/Patches/Client/DisconnectPenaltyPatch.cs b/src/Patches/Client/DisconnectPenaltyPatch.cs index df41ea5..87c8aef 100644 --- a/src/Patches/Client/DisconnectPenaltyPatch.cs +++ b/src/Patches/Client/DisconnectPenaltyPatch.cs @@ -10,9 +10,12 @@ internal static class DisconnectPenaltyPatch [HarmonyPrefix] private static bool PlayerBanData_IsBanned_Prefix(PlayerBanData __instance, ref bool __result) { + // Reset ban points to zero to prevent disconnect penalties __instance.BanPoints = 0f; __instance.banPoints = 0f; + + // Always return false (not banned) regardless of actual ban status __result = false; return false; } -} +} \ No newline at end of file diff --git a/src/Patches/Client/Discord/DiscordPatch.cs b/src/Patches/Client/Discord/DiscordPatch.cs index 5eaa8ee..877f872 100644 --- a/src/Patches/Client/Discord/DiscordPatch.cs +++ b/src/Patches/Client/Discord/DiscordPatch.cs @@ -16,21 +16,26 @@ internal static class DiscordPatch [HarmonyPrefix] private static void ActivityManager_UpdateActivity_Prefix(Activity activity) { + // Skip Discord Rich Presence if other mods have disabled it via BAUModdedSupportFlags if (BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_DiscordRP)) return; if (activity == null) return; string details = $"BAU {BAUPlugin.GetVersionText()}"; activity.Details = details; + // Skip lobby info processing if Discord already shows "In Menus" if (activity.State == "In Menus") return; try { + // Only show lobby code/region if streamer mode is off if (!DataManager.Settings.Gameplay.StreamerMode) { UpdateRegionAndLobbyCode(); + // Build lobby info string only when both code and region exist if (!string.IsNullOrEmpty(lobbycode) && !string.IsNullOrEmpty(region)) { + // Show lobby code with region in parentheses if (GameState.IsNormalGame) details = $"BAU - {lobbycode} ({region})"; else if (GameState.IsHideNSeek) @@ -39,12 +44,14 @@ internal static class DiscordPatch } else { + // Streamer mode hides lobby code, only show mode info if (GameState.IsHideNSeek) details = $"BAU v{ModInfo.PLUGIN_VERSION} - Hide & Seek"; } } catch { + // Silent fail to prevent Discord crashes from affecting gameplay } activity.Details = details; @@ -52,12 +59,14 @@ internal static class DiscordPatch private static void UpdateRegionAndLobbyCode() { + // Only fetch lobby data when player is in a lobby if (GameState.IsLobby) { if (GameStartManager.Instance?.GameRoomNameCode != null) { lobbycode = GameStartManager.Instance.GameRoomNameCode.text; region = ServerManager.Instance.CurrentRegion.Name; + // Convert full region names to short codes region = region switch { "North America" => "NA", @@ -71,4 +80,4 @@ internal static class DiscordPatch } } } -} +} \ No newline at end of file diff --git a/src/Patches/Client/InnerNetClientPatch.cs b/src/Patches/Client/InnerNetClientPatch.cs index 2d93478..56d5901 100644 --- a/src/Patches/Client/InnerNetClientPatch.cs +++ b/src/Patches/Client/InnerNetClientPatch.cs @@ -16,8 +16,9 @@ internal static class InnerNetClientPatch [HarmonyPrefix] private static bool InnerNetClient_SendOrDisconnect_Prefix(InnerNetClient __instance, MessageWriter msg) { + // Route all outgoing messages through custom NetworkManager + // This allows BAU to intercept/modify network traffic NetworkManager.SendToServer(msg); - return false; } @@ -25,6 +26,8 @@ internal static class InnerNetClientPatch [HarmonyPrefix] private static bool InnerNetClient_HandleGameDataInner_Prefix([HarmonyArgument(0)] MessageReader oldReader) { + // Route all incoming game data through custom NetworkManager + // This allows BAU to process/modify incoming network messages NetworkManager.HandleGameData(oldReader); return false; } @@ -33,6 +36,7 @@ internal static class InnerNetClientPatch [HarmonyPrefix] private static bool InnerNetClient_CanBan_Prefix(ref bool __result) { + // Only allow hosts to ban players in BAU __result = GameState.IsHost; return false; } @@ -41,7 +45,11 @@ internal static class InnerNetClientPatch [HarmonyPrefix] private static bool InnerNetClient_CanKick_Prefix(ref bool __result) { - __result = GameState.IsHost || GameState.IsInGamePlay && (GameState.IsMeeting || GameState.IsExilling); + // Allow kicking under specific conditions: + // 1. Host can always kick + // 2. Non-hosts can only kick during meetings or when player is being voted out + __result = GameState.IsHost || (GameState.IsInGamePlay && (GameState.IsMeeting || GameState.IsExilling)); + return false; } @@ -49,10 +57,14 @@ internal static class InnerNetClientPatch [HarmonyPrefix] private static void InnerNetClient_KickPlayer_Prefix(ref int clientId, ref bool ban) { + // When banning a player, add them to BAU's custom ban list if enabled if (ban && BetterGameSettings.UseBanPlayerList.GetBool()) { + // Get player info from client ID NetworkedPlayerInfo info = Utils.PlayerFromClientId(clientId).Data; + + // Add player to ban list using both friend code and PUID BetterDataManager.AddToBanList(info.FriendCode, info.Puid); } } -} +} \ No newline at end of file diff --git a/src/Patches/Client/Managers/MainMenuManagerPatch.cs b/src/Patches/Client/Managers/MainMenuManagerPatch.cs index 38b85e3..4a5ecc3 100644 --- a/src/Patches/Client/Managers/MainMenuManagerPatch.cs +++ b/src/Patches/Client/Managers/MainMenuManagerPatch.cs @@ -15,42 +15,57 @@ internal static class MainMenuManagerPatch [HarmonyPostfix] private static void MainMenuManager_LateUpdate_Postfix(MainMenuManager __instance) { + // Create list of all main menu buttons to recolor List buttons = [__instance.playButton, __instance.inventoryButton, __instance.shopButton, __instance.playLocalButton, __instance.PlayOnlineButton, __instance.backButtonOnline, __instance.newsButton, __instance.myAccountButton, __instance.settingsButton, __instance.howToPlayButton, __instance.freePlayButton, __instance.accountCTAButton, __instance.accountStatsButton]; + + // Apply custom UI colors to each button's icon and background foreach (var button in buttons) { button.gameObject?.SetUIColors(sprite => { + // Only recolor white sprites to preserve original color variations return sprite.color == Color.white; }, "Icon", "Background"); } } - // Replace AU logo with BAU logo [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start))] [HarmonyPostfix] private static void MainMenuManager_Start_Postfix(MainMenuManager __instance) { + // Check if other mods haven't disabled the BAU logo replacement if (!BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_BAULogo)) { + // Find the original Among Us logo GameObject logo = GameObject.Find("LeftPanel/Sizer/LOGO-AU"); GameObject sizer = logo.transform.parent.gameObject; + + // Adjust logo position downward and move it forward in Z-axis sizer.transform.localPosition = new Vector3(sizer.transform.localPosition.x, sizer.transform.localPosition.y - 0.035f, sizer.transform.localPosition.z); sizer.transform.position = new Vector3(sizer.transform.position.x, sizer.transform.position.y, -0.5f); + + // Scale down the logo slightly logo.transform.localScale = new Vector3(0.003f, 0.0025f, 0f); + + // Replace Among Us sprite with Better Among Us logo logo.GetComponent().sprite = Utils.LoadSprite("BetterAmongUs.Resources.Images.BetterAmongUs-Logo.png", 1f); } + // Apply custom colors to main menu background __instance.transform.Find("MainUI/AspectScaler/BackgroundTexture")?.gameObject?.SetSpriteColors(sprite => ObjectHelper.AddColor(sprite)); + // Create a reusable button prefab if it doesn't exist yet if (ButtonPrefab == null) { + // Clone inventory button as template for custom UI elements ButtonPrefab = UnityEngine.Object.Instantiate(__instance.inventoryButton); ButtonPrefab.gameObject.SetActive(false); UnityEngine.Object.DontDestroyOnLoad(ButtonPrefab); } + // Notify UpdateManager that we're in the main menu UpdateManager.Instance?.OnMainMenu(); } -} +} \ No newline at end of file diff --git a/src/Patches/Client/Managers/ModManagerPatch.cs b/src/Patches/Client/Managers/ModManagerPatch.cs index 1f52c85..2cc9cfa 100644 --- a/src/Patches/Client/Managers/ModManagerPatch.cs +++ b/src/Patches/Client/Managers/ModManagerPatch.cs @@ -16,19 +16,24 @@ internal static class ModManagerPatch [HarmonyPostfix] private static void LateUpdate_Postfix(ModManager __instance) { + // Show the mod stamp only after the splash screen has fully loaded if (SplashIntroPatch.IsReallyDoneLoading) { __instance.ShowModStamp(); } + // Check if the mod stamp is currently visible in the UI if (__instance.ModStamp.gameObject.active == true) { + // Cache the SpriteRenderer component on first access if (modStamp == null) { modStamp = __instance.ModStamp.GetComponent(); } else { + // Replace the mod stamp sprite with BAU's custom version + // unless other mods have disabled custom mod stamps if (!BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_CustomModStamp)) { modStamp.sprite = Utils.LoadSprite("BetterAmongUs.Resources.Images.BetterAmongUs-Mod.png", 250f); @@ -36,8 +41,9 @@ internal static class ModManagerPatch } } + // Update various BAU systems each frame BetterAntiCheat.Update(); LateTask.UpdateAll(Time.deltaTime); BetterNotificationManager.Update(); } -} +} \ No newline at end of file diff --git a/src/Patches/Client/OptionsMenuBehaviourPatch.cs b/src/Patches/Client/OptionsMenuBehaviourPatch.cs index 596559a..f084c92 100644 --- a/src/Patches/Client/OptionsMenuBehaviourPatch.cs +++ b/src/Patches/Client/OptionsMenuBehaviourPatch.cs @@ -21,8 +21,13 @@ internal static class OptionsMenuBehaviourPatch [HarmonyPostfix] private static void Start_Postfix(OptionsMenuBehaviour __instance) { + // Create custom "Better Options" tab in settings menu BetterOptionsTab = CreateTabPage(__instance, Translator.GetString("BetterOption")); + + // Populate the tab with all BAU client options SetupAllClientOptions(__instance); + + // Reposition tabs to fit new Better Options tab UpdateTabPositions(__instance); } @@ -30,6 +35,7 @@ internal static class OptionsMenuBehaviourPatch { if (__instance.DisableMouseMovement == null) return; + // Clear previous client options to prevent duplicates ClientOptionItem.ClientOptions.Clear(); // Toggle options with config binding @@ -47,6 +53,7 @@ internal static class OptionsMenuBehaviourPatch // Button options (no toggle) ClientOptionItem.CreateButton(Translator.GetString("BetterOption.SaveData"), __instance, OpenSaveData, () => { + // Only allow opening save data in lobby/main menu, not during gameplay bool cannotOpen = GameState.IsInGame && !GameState.IsLobby; if (cannotOpen) { @@ -57,6 +64,7 @@ internal static class OptionsMenuBehaviourPatch ClientOptionItem.CreateButton(Translator.GetString("BetterOption.ToVanilla"), __instance, SwitchToVanilla, () => { + // Prevent switching to vanilla while in a game bool cannotSwitch = GameState.IsInGame; if (cannotSwitch) { @@ -68,6 +76,7 @@ internal static class OptionsMenuBehaviourPatch private static void SwitchToVanilla() { + // Clean up BAU mod components and return to vanilla Among Us ConsoleManager.DetachConsole(); BetterNotificationManager.BAUNotificationManagerObj?.DestroyObj(); Harmony.UnpatchAll(); @@ -77,6 +86,7 @@ internal static class OptionsMenuBehaviourPatch private static void SendBetterRpcAction() { + // Resend handshake secret to all other players when option is toggled if (!GameState.IsInGame) return; foreach (var player in BAUPlugin.AllPlayerControls) @@ -88,6 +98,7 @@ internal static class OptionsMenuBehaviourPatch private static void ClearNotifications() { + // Clear all active notifications when option is toggled BetterNotificationManager.NotifyQueue.Clear(); BetterNotificationManager.showTime = 0f; BetterNotificationManager.Notifying = false; @@ -95,6 +106,7 @@ internal static class OptionsMenuBehaviourPatch private static void ToggleLobbyTheme() { + // Play lobby theme music if re-enabled while in lobby if (GameState.IsLobby && !BAUPlugin.DisableLobbyTheme.Value) { SoundManager.instance.CrossFadeSound("MapTheme", LobbyBehaviour.Instance.MapTheme, 0.5f, 1.5f); @@ -103,11 +115,13 @@ internal static class OptionsMenuBehaviourPatch private static void UpdateFrameRate() { + // Toggle between 60 FPS (default) and 165 FPS Application.targetFrameRate = BAUPlugin.UnlockFPS.Value ? 165 : 60; } private static void OpenSaveData() { + // Open BAU save data folder in file explorer if (!File.Exists(BetterDataManager.dataPath)) return; Process.Start(new ProcessStartInfo @@ -120,6 +134,7 @@ internal static class OptionsMenuBehaviourPatch private static TabGroup CreateTabPage(OptionsMenuBehaviour __instance, string name) { + // Clone last tab as template for new Better Options tab var tabPrefab = __instance.Tabs[^1]; var tab = UnityEngine.Object.Instantiate(tabPrefab, tabPrefab.transform.parent); @@ -128,15 +143,18 @@ internal static class OptionsMenuBehaviourPatch tab.GetComponentInChildren(true)?.SetText(name); tab.gameObject.SetActive(true); + // Create content container for the new tab var content = new GameObject($"{name}Tab"); content.SetActive(false); content.transform.SetParent(tab.Content.transform.parent); content.transform.localScale = Vector3.one; tab.Content = content; + // Add new tab to the tabs array var tabs = new List(__instance.Tabs) { tab }; __instance.Tabs = tabs.ToArray(); + // Set up click handler for the tab button var index = __instance.Tabs.Length - 1; var button = tab.GetComponent(); button.OnClick = new(); @@ -151,10 +169,12 @@ internal static class OptionsMenuBehaviourPatch private static void UpdateTabPositions(OptionsMenuBehaviour __instance) { + // Position tabs based on game state (in-game vs main menu) Vector3 basePos = new(0f, !GameState.InGame ? 0 : 2.5f, -1f); const float buttonSpacing = 0.6f; const float buttonWidth = 1.0f; + // Count only active tabs int activeCount = 0; foreach (var tabButton in __instance.Tabs) { @@ -163,9 +183,11 @@ internal static class OptionsMenuBehaviourPatch if (activeCount == 0) return; + // Calculate total width needed for all active tabs float totalWidth = (activeCount - 1) * buttonSpacing + activeCount * buttonWidth; float startX = basePos.x - (totalWidth / 2f) + (buttonWidth / 2f); + // Position each active tab evenly spaced int activeIndex = 0; foreach (var tabButton in __instance.Tabs) { diff --git a/src/Patches/Client/PrivateLobbyPatch.cs b/src/Patches/Client/PrivateLobbyPatch.cs index 3798e5e..f7d7854 100644 --- a/src/Patches/Client/PrivateLobbyPatch.cs +++ b/src/Patches/Client/PrivateLobbyPatch.cs @@ -11,31 +11,38 @@ namespace BetterAmongUs.Patches.Client; internal static class PrivateLobbyPatch { private static GameObject? toggle; - private static List? buttons = []; + private static readonly List? buttons = []; private static TextMeshPro? toggleText; [HarmonyPatch(typeof(CreateGameOptions), nameof(CreateGameOptions.Show))] [HarmonyPostfix] private static void CreateGameOptions_Show_Postfix(CreateGameOptions __instance) { + // Check if other mods have disabled private lobby feature if (BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_PrivateLobby)) { BAUPlugin.PrivateOnlyLobby.Value = false; return; } + // Only create toggle UI once if (toggle != null) return; buttons.Clear(); + // Clone April Fools toggle as template for private lobby toggle toggle = UnityEngine.Object.Instantiate(__instance.AprilFoolsToggle, __instance.contentObjects.First().transform.parent); if (toggle != null) { toggle.name = "PrivateOnlyLobby"; + + // Get the ON and OFF buttons from the toggle buttons.Add(toggle.transform.Find("AprilOn").GetComponent()); buttons.Add(toggle.transform.Find("AprilOff").GetComponent()); if (buttons.Count < 2) return; toggle.gameObject.SetActive(true); + + // Set up ON button click handler var onButton = buttons[0]; if (onButton != null) { @@ -43,6 +50,8 @@ internal static class PrivateLobbyPatch onButton.OnClick = new(); onButton.OnClick.AddListener((Action)(() => TogglePrivateOnlyLobby(true))); } + + // Set up OFF button click handler var offButton = buttons[1]; if (offButton != null) { @@ -50,11 +59,14 @@ internal static class PrivateLobbyPatch offButton.OnClick = new(); offButton.OnClick.AddListener((Action)(() => TogglePrivateOnlyLobby(false))); } + + // Position toggle at top-right of create game screen var aspect = toggle.gameObject.AddComponent(); aspect.Alignment = AspectPosition.EdgeAlignments.Top; aspect.DistanceFromEdge = new Vector3(0.4f, 1.62f, 0); aspect.AdjustPosition(); + // Update toggle text label var text = toggle.transform.Find("BlackSquare/ModeText")?.GetComponent(); if (text != null) { @@ -64,6 +76,7 @@ internal static class PrivateLobbyPatch } } + // Initialize toggle state from saved configuration TogglePrivateOnlyLobby(BAUPlugin.PrivateOnlyLobby.Value); } @@ -71,11 +84,15 @@ internal static class PrivateLobbyPatch { if (buttons.Count < 2) return; + // Show both buttons first buttons[0].gameObject.SetActive(true); buttons[1].gameObject.SetActive(true); + + // Deselect both buttons buttons[0].SelectButton(false); buttons[1].SelectButton(false); + // Select appropriate button based on mode if (modeOn) { buttons[0].SelectButton(true); @@ -84,6 +101,8 @@ internal static class PrivateLobbyPatch { buttons[1].SelectButton(true); } + + // Save setting to configuration BAUPlugin.PrivateOnlyLobby.Value = modeOn; } @@ -91,24 +110,25 @@ internal static class PrivateLobbyPatch [HarmonyPostfix] private static void LobbyInfoPane_Update_Postfix(LobbyInfoPane __instance) { + // Enforce private lobby setting if enabled by host if (BAUPlugin.PrivateOnlyLobby.Value && !GameState.IsLocalGame && GameState.IsHost) { + // Ensure game stays private if (AmongUsClient.Instance.IsGamePublic) { AmongUsClient.Instance.ChangeGamePublic(false); } + // Disable and visually indicate that public lobby button is locked var button = __instance.HostPrivateButton.GetComponent(); if (button != null) { button.enabled = false; + // Change inactive sprite to cyan color to indicate locked state var sprite = __instance.HostPrivateButton.transform.Find("Inactive").GetComponent(); - if (sprite != null) - { - sprite.color = new(0.35f, 1, 1, 1); - } + sprite?.color = new(0.35f, 1, 1, 1); } } } -} +} \ No newline at end of file diff --git a/src/Patches/Client/SplashIntroPatch.cs b/src/Patches/Client/SplashIntroPatch.cs index 1dd7c55..3bd3034 100644 --- a/src/Patches/Client/SplashIntroPatch.cs +++ b/src/Patches/Client/SplashIntroPatch.cs @@ -16,9 +16,12 @@ internal static class SplashIntroPatch [HarmonyPrefix] private static void SplashManager_Start_Prefix(SplashManager __instance) { + // Reset all flags when splash screen starts Skip = false; BetterIntro = false; IsReallyDoneLoading = false; + + // Hide black overlay by moving it out of view __instance.logoAnimFinish.transform.Find("BlackOverlay").transform.SetLocalY(100f); } @@ -26,17 +29,20 @@ internal static class SplashIntroPatch [HarmonyPrefix] private static bool SplashManager_Update_Prefix(SplashManager __instance) { + // If skip is triggered, just check if loading is done if (Skip) { CheckIfDone(__instance); return false; } + // After 1.8 seconds in BAU intro, remove audio to prevent overlap if (Time.time - __instance.startTime > 1.8f && BetterIntro) { UnityEngine.Object.Destroy(__instance.logoAnimFinish.GetComponent()); } + // Allow mouse click to skip splash screen if (Input.GetKeyDown(KeyCode.Mouse0) || Input.GetKeyDown(KeyCode.Mouse1)) { if (CheckIfDone(__instance, true)) @@ -46,35 +52,48 @@ internal static class SplashIntroPatch } } + // When game data is loaded and minimum time has passed if (__instance.doneLoadingRefdata && !__instance.startedSceneLoad && Time.time - __instance.startTime > __instance.minimumSecondsBeforeSceneChange) { if (!BetterIntro) { + // Start BAU custom intro sequence __instance.startTime = Time.time; __instance.logoAnimFinish.gameObject.SetActive(false); __instance.logoAnimFinish.gameObject.SetActive(true); + + // Replace InnerSloth logo with BAU logo GameObject InnerLogo = __instance.logoAnimFinish.transform.Find("LogoRoot/ISLogo").gameObject; BetterLogo = UnityEngine.Object.Instantiate(InnerLogo, InnerLogo.transform.parent); InnerLogo.DestroyObj(); BetterLogo.name = "BetterLogo"; BetterLogo.GetComponent().sprite = Utils.LoadSprite("BetterAmongUs.Resources.Images.BetterAmongUs-Logo.png", 150f); + + // Show black overlay __instance.logoAnimFinish.transform.Find("BlackOverlay").transform.SetLocalY(0f); BetterIntro = true; return false; } + // Check if BAU intro has completed CheckIfDone(__instance); } + // Return false to prevent original Update from running (we handle everything) return false; } private static bool CheckIfDone(SplashManager __instance, bool isSkip = false) { - if (Time.time - __instance.startTime > 2f && BetterIntro || isSkip && BetterIntro) + // Allow transition if: + // 1. BAU intro played for 2 seconds OR + // 2. User skipped and BAU intro was shown + if ((Time.time - __instance.startTime > 2f && BetterIntro) || (isSkip && BetterIntro)) { IsReallyDoneLoading = true; + + // Allow scene transition to proceed __instance.sceneChanger.AllowFinishLoadingScene(); __instance.startedSceneLoad = true; __instance.loadingObject.SetActive(true); diff --git a/src/Patches/Gameplay/Managers/FindAGameManagerClass/FindAGameManagerPatch.cs b/src/Patches/Gameplay/Managers/FindAGameManagerClass/FindAGameManagerPatch.cs index 4cab60c..2ae75fd 100644 --- a/src/Patches/Gameplay/Managers/FindAGameManagerClass/FindAGameManagerPatch.cs +++ b/src/Patches/Gameplay/Managers/FindAGameManagerClass/FindAGameManagerPatch.cs @@ -15,22 +15,26 @@ internal static class FindAGameManagerPatch [HarmonyPrefix] private static void FindAGameManager_Start_Prefix(FindAGameManager __instance) { + // Apply custom UI colors to all Find Game menu buttons __instance.refreshButton.gameObject.SetUIColors(); __instance.BackButton.gameObject.SetUIColors(); __instance.clearFilterButton.gameObject.SetUIColors("Disabled"); __instance.serverButton.gameObject.SetUIColors("Inactive", "Disabled", "Background"); __instance.serverButton.activeTextColor = Color.cyan * 0.3f; + // Modify game container hover colors foreach (var con in __instance.gameContainers) { var roll = con.GetComponent(); roll.OverColor = (roll.OverColor * 0.6f) + (Color.green * 0.5f); } + // Create scrollable container for game listings var prefab = __instance.gameContainers[4]; var list = new GameObject("GameListScroller"); list.transform.SetParent(prefab.transform.parent); + // Set up scroller component for vertical scrolling Scroller = list.AddComponent(); Scroller.Inner = list.transform; Scroller.MouseMustBeOverToScroll = true; @@ -42,6 +46,7 @@ internal static class FindAGameManagerPatch Scroller.SetYBoundsMax(3.5f); Scroller.allowY = true; + // Move existing game containers into scrollable list foreach (var con in __instance.gameContainers) { con.transform.SetParent(list.transform); @@ -49,6 +54,7 @@ internal static class FindAGameManagerPatch con.transform.position = new Vector3(oldPos.x, oldPos.y, 25); } + // Add more game containers to support longer server lists var oldGameContainers = __instance.gameContainers.ToList(); for (int i = 0; i < 5; i++) @@ -61,6 +67,7 @@ internal static class FindAGameManagerPatch __instance.gameContainers = oldGameContainers.ToArray(); + // Create visual cutoff for scrolling content var cutOffTop = CreateBlackSquareSprite(); cutOffTop.transform.SetParent(list.transform.parent); cutOffTop.transform.localPosition = new Vector3(0, 3, 1); @@ -71,6 +78,7 @@ internal static class FindAGameManagerPatch [HarmonyPostfix] private static void FindAGameManager_RefreshList_Postfix(FindAGameManager __instance) { + // Reset scroll position when refreshing game list Scroller?.ScrollRelative(new(0f, -100f)); } @@ -78,12 +86,16 @@ internal static class FindAGameManagerPatch [HarmonyPostfix] private static void FindAGameManager_HandleList_Postfix(FindAGameManager __instance, HttpMatchmakerManager.FindGamesListFilteredResponse response) { + // Clear existing containers before populating with new data __instance.ResetContainers(); GameListing[] games = response.Games.ToArray(); + // Sort games: first by player count (highest first), then by host name games = [.. games.OrderByDescending(game => game.PlayerCount).ThenBy(game => game.TrueHostName)]; int gameNum = 0; int count = 0; + + // Populate visible containers with available games while (count < __instance.gameContainers.Length && count < games.Count()) { if (games[count].Options != null) @@ -96,11 +108,13 @@ internal static class FindAGameManagerPatch count++; } + // Add true host name and platform info to each game container foreach (var container in __instance.gameContainers) { Transform child = container.transform.Find("Container"); Transform tmproObject = child.Find("TrueHostName_TMP"); + // Use existing TMP component or create new one TMP_Text tmpro = tmproObject != null ? tmproObject.GetComponent() : CreateNewTextMeshPro(child); @@ -113,24 +127,30 @@ internal static class FindAGameManagerPatch private static TMP_Text CreateNewTextMeshPro(Transform parent) { + // Create new TextMeshPro component for displaying host info var tmproObject = new GameObject("TrueHostName_TMP").transform; tmproObject.SetParent(parent, true); + + // Position text in container var aspectPos = tmproObject.gameObject.AddComponent(); aspectPos.Alignment = AspectPosition.EdgeAlignments.Center; aspectPos.anchorPoint = new Vector2(0.2f, 0.5f); aspectPos.DistanceFromEdge = new Vector3(10.9f, -2.17f, -2f); aspectPos.AdjustPosition(); + return tmproObject.gameObject.AddComponent(); } private static string FormatGameInfoText(GameListing listing) { + // Format game info text with host name, platform, and game code var hostStr = !string.IsNullOrEmpty(listing.TrueHostName) ? listing.TrueHostName : listing.HostName; return @$"{hostStr}{Environment.NewLine}{Utils.GetPlatformName(listing.Platform)} ({GameCode.IntToGameName(listing.GameId)})"; } private static SpriteRenderer CreateBlackSquareSprite() { + // Create a black sprite to visually cut off scrolling content var square = new GameObject("CutOffTop"); var renderer = square.AddComponent(); Texture2D texture = new(100, 100); @@ -146,4 +166,4 @@ internal static class FindAGameManagerPatch square.transform.localScale = new Vector3(100, 100, 1); return renderer; } -} +} \ No newline at end of file diff --git a/src/Patches/Gameplay/Managers/FindAGameManagerClass/FindGameMoreInfoPopupPatch.cs b/src/Patches/Gameplay/Managers/FindAGameManagerClass/FindGameMoreInfoPopupPatch.cs index 131f57e..01aedfa 100644 --- a/src/Patches/Gameplay/Managers/FindAGameManagerClass/FindGameMoreInfoPopupPatch.cs +++ b/src/Patches/Gameplay/Managers/FindAGameManagerClass/FindGameMoreInfoPopupPatch.cs @@ -20,6 +20,7 @@ internal static class FindGameMoreInfoPopupPatch { _findGameMoreInfoPopup = __instance; + // Create custom settings info text box if it doesn't exist if (_textBox == null) { _textBox = UnityEngine.Object.Instantiate(AccountManager.Instance.genericInfoDisplayBox, __instance.transform); @@ -41,6 +42,7 @@ internal static class FindGameMoreInfoPopupPatch pos.AdjustPosition(); } + // Display all game settings when popup opens ShowAll(); } @@ -51,8 +53,10 @@ internal static class FindGameMoreInfoPopupPatch _sb.Clear(); var options = _findGameMoreInfoPopup.gameListing.Options; + // Display settings based on game mode if (options.GameMode is GameModes.Normal or GameModes.NormalFools) { + // Impostor settings FormatOption(StringNames.GameNumImpostors, $"{options.NumImpostors}"); if (options.TryGetFloat(FloatOptionNames.KillCooldown, out var value1)) { @@ -69,6 +73,7 @@ internal static class FindGameMoreInfoPopupPatch AddBreak(); + // General gameplay settings if (options.TryGetFloat(FloatOptionNames.PlayerSpeedMod, out var value4)) { FormatOption(StringNames.GamePlayerSpeed, $"{value4}x"); @@ -80,6 +85,7 @@ internal static class FindGameMoreInfoPopupPatch AddBreak(); + // Meeting settings if (options.TryGetInt(Int32OptionNames.NumEmergencyMeetings, out var value6)) { FormatOption(StringNames.GameNumMeetings, $"{value6}"); @@ -107,6 +113,7 @@ internal static class FindGameMoreInfoPopupPatch AddBreak(); + // Task settings if (options.TryGetInt(Int32OptionNames.TaskBarMode, out var value12)) { FormatOption(StringNames.GameTaskBarMode, GetTaskBarMode(value12)); @@ -130,6 +137,7 @@ internal static class FindGameMoreInfoPopupPatch } else if (options.GameMode is GameModes.HideNSeek or GameModes.SeekFools) { + // Hide & Seek specific settings (Hider/Crewmate) if (options.TryGetFloat(FloatOptionNames.PlayerSpeedMod, out var value1)) { FormatOption(StringNames.GamePlayerSpeed, $"{value1}x"); @@ -165,6 +173,7 @@ internal static class FindGameMoreInfoPopupPatch AddBreak(); + // Hide & Seek Seeker (Impostor) settings if (options.TryGetFloat(FloatOptionNames.ImpostorFlashlightSize, out var value9)) { FormatOption(StringNames.ImpostorFlashlightSize, $"{value9}x"); @@ -176,6 +185,7 @@ internal static class FindGameMoreInfoPopupPatch AddBreak(); + // Hide & Seek final phase settings if (options.TryGetFloat(FloatOptionNames.FinalEscapeTime, out var value11)) { FormatOption(StringNames.FinalEscapeTime, $"{value11}s"); @@ -199,6 +209,7 @@ internal static class FindGameMoreInfoPopupPatch AddBreak(); + // Hide & Seek task settings if (options.TryGetInt(Int32OptionNames.NumCommonTasks, out var value16)) { FormatOption(StringNames.GameCommonTasks, $"{value16}"); @@ -213,6 +224,7 @@ internal static class FindGameMoreInfoPopupPatch } } + // Update text box with formatted settings if (_textBox != null) { _textBox.SetText(_sb.ToString()); @@ -221,11 +233,13 @@ internal static class FindGameMoreInfoPopupPatch private static void FormatOption(StringNames optName, string value) { + // Format each setting line with gray label and yellow value _sb.AppendLine($"{Translator.GetString(optName)}<#989898>: <#CFCF00>{value}"); } private static void AddBreak() { + // Add separator line between settings groups _sb.AppendLine("<#999999>----------------------"); } @@ -233,6 +247,7 @@ internal static class FindGameMoreInfoPopupPatch private static string GetKillDistance(int value) { + // Convert kill distance integer to readable string switch (value) { case 0: @@ -250,6 +265,7 @@ internal static class FindGameMoreInfoPopupPatch private static string GetTaskBarMode(int value) { + // Convert task bar mode integer to readable string switch (value) { case 0: diff --git a/src/Patches/Gameplay/Managers/HudManagerPatch.cs b/src/Patches/Gameplay/Managers/HudManagerPatch.cs index 1571f79..ef5830a 100644 --- a/src/Patches/Gameplay/Managers/HudManagerPatch.cs +++ b/src/Patches/Gameplay/Managers/HudManagerPatch.cs @@ -5,7 +5,6 @@ using HarmonyLib; using TMPro; using UnityEngine; - namespace BetterAmongUs.Patches.Gameplay.Managers; [HarmonyPatch] @@ -21,6 +20,7 @@ internal static class HudManagerPatch [HarmonyPostfix] private static void HudManager_Start_Postfix(HudManager __instance) { + // Create custom BAU notification system if it doesn't exist if (BetterNotificationManager.BAUNotificationManagerObj == null) { var ChatNotifications = __instance.Chat.chatNotification; @@ -28,31 +28,46 @@ internal static class HudManagerPatch { ChatNotifications.timeOnScreen = 1f; ChatNotifications.gameObject.SetActive(true); + + // Clone chat notification system for BAU notifications GameObject BAUNotification = UnityEngine.Object.Instantiate(ChatNotifications.gameObject); BAUNotification.name = "BAUNotification"; BAUNotification.GetComponent().DestroyMono(); + + // Remove unnecessary elements from the clone GameObject.Find($"{BAUNotification.name}/Sizer/PoolablePlayer").DestroyObj(); GameObject.Find($"{BAUNotification.name}/Sizer/ColorText").DestroyObj(); + + // Position notification at bottom-left corner BAUNotification.GetComponent().DistanceFromEdge = new Vector3(-1.57f, 5.3f, -15f); GameObject.Find($"{BAUNotification.name}/Sizer/NameText").transform.localPosition = new Vector3(-3.3192f, -0.0105f); + + // Cache TextMeshPro component for text updates BetterNotificationManager.NameText = GameObject.Find($"{BAUNotification.name}/Sizer/NameText").GetComponent(); UnityEngine.Object.DontDestroyOnLoad(BAUNotification); BetterNotificationManager.BAUNotificationManagerObj = BAUNotification; BAUNotification.SetActive(false); + + // Reset original chat notification settings ChatNotifications.timeOnScreen = 0f; ChatNotifications.gameObject.SetActive(false); + + // Configure text wrapping for multi-line notifications BetterNotificationManager.TextArea.enableWordWrapping = true; BetterNotificationManager.TextArea.m_firstOverflowCharacterIndex = 0; BetterNotificationManager.TextArea.overflowMode = TextOverflowModes.Overflow; } } + // Show welcome message after 1 second delay (only once per session) LateTask.Schedule(() => { if (!HasBeenWelcomed && GameState.IsInGame && GameState.IsLobby && !GameState.IsFreePlay) { + // Show notification with welcome text BetterNotificationManager.Notify($"{string.Format(Translator.GetString("WelcomeMsg.WelcomeToBAU"), Translator.GetString("BetterAmongUs"))}!", 8f); + // Send detailed welcome message to private chat Utils.AddChatPrivate(WelcomeMessage, overrideName: " "); HasBeenWelcomed = true; } @@ -65,16 +80,17 @@ internal static class HudManagerPatch { try { + // Adjust GameStartManager position for better UI layout GameObject gameStart = GameObject.Find("GameStartManager"); if (gameStart != null) gameStart.transform.SetLocalY(-2.8f); - - // Set chat + // Manage in-game chat visibility based on settings and game state if (GameState.InGame) { if (!BAUPlugin.ChatInGameplay.Value) { + // Vanilla chat behavior: only show chat when dead or during meetings if (!PlayerControl.LocalPlayer.IsAlive()) { __instance.Chat.gameObject.SetActive(true); @@ -86,6 +102,7 @@ internal static class HudManagerPatch } else { + // BAU chat behavior: always show chat when enabled if (__instance?.Chat?.gameObject.active == false) { __instance.Chat.gameObject.SetActive(true); @@ -93,6 +110,8 @@ internal static class HudManagerPatch } } } - catch { } + catch + { + } } -} +} \ No newline at end of file diff --git a/src/Patches/Gameplay/Managers/RoleManagerPatch.cs b/src/Patches/Gameplay/Managers/RoleManagerPatch.cs index 6e30675..8215e5b 100644 --- a/src/Patches/Gameplay/Managers/RoleManagerPatch.cs +++ b/src/Patches/Gameplay/Managers/RoleManagerPatch.cs @@ -15,6 +15,7 @@ internal static class RoleManagerPatch internal static Dictionary ImpostorMultiplier = []; // HashPuid, Multiplier private static readonly Random random = new(); + // Check if client is not verified Better Among Us user static readonly Func clientCheck = (clientData) => { return clientData?.BetterData()?.IsVerifiedBetterUser != true; @@ -24,6 +25,7 @@ internal static class RoleManagerPatch [HarmonyPrefix] private static void RoleManager_SetRole_Prefix(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType) { + // Store the original role when player dies (for ghost role purposes) if (RoleManager.IsGhostRole(roleType)) { if (!RoleManager.IsGhostRole(targetPlayer.Data.RoleType)) @@ -33,16 +35,18 @@ internal static class RoleManagerPatch } } - // Better role algorithm + // Better role assignment algorithm (replaces vanilla role assignment) [HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SelectRoles))] [HarmonyPrefix] private static bool RoleManager_SelectRoles_Prefix() { + // Skip BAU role assignment if other mods disabled it if (BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_BetterRoleAlgorithm)) { return true; } + // Use different algorithms for different game modes if (!GameState.IsHideNSeek) { RegularBetterRoleAssignment(); @@ -52,6 +56,7 @@ internal static class RoleManagerPatch HideAndSeekBetterRoleAssignment(); } + // Return false to prevent vanilla role assignment from running return false; } @@ -59,14 +64,14 @@ internal static class RoleManagerPatch { Logger_.LogHeader($"Better Role Assignment Has Started", "RoleManager"); - // Set roles up + // Initialize impostor multiplier tracking for all players foreach (var addplayer in BAUPlugin.AllPlayerControls.Where(pc => !ImpostorMultiplier.ContainsKey(Utils.GetHashPuid(pc)))) ImpostorMultiplier[Utils.GetHashPuid(addplayer)] = 0; int NumImpostors = GameOptionsManager.Instance.CurrentGameOptions.NumImpostors; - int NumPlayers = BAUPlugin.AllPlayerControls.Count; + // Apply player count limits to impostor numbers var impostorLimits = new Dictionary { { 3, 1 }, @@ -86,6 +91,7 @@ internal static class RoleManagerPatch List Impostors = []; List Crewmates = []; + // Track available roles and their counts Dictionary ImpostorRoles = new() // Role, Amount { { RoleTypes.Shapeshifter, 0 }, @@ -104,6 +110,7 @@ internal static class RoleManagerPatch List Roles = [.. ImpostorRoles.Keys, .. CrewmateRoles.Keys]; + // Get role counts from game options foreach (RoleTypes role in Roles) { if (role.GetBehaviourPrefab().IsImpostor) @@ -112,7 +119,7 @@ internal static class RoleManagerPatch CrewmateRoles[role] = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role); } - // Get players in random order + // Shuffle players for random role assignment List players = [.. BAUPlugin.AllPlayerControls.Where(player => !Impostors.Contains(player) && !Crewmates.Contains(player) && player.roleAssigned == false)]; int n = players.Count; @@ -125,24 +132,26 @@ internal static class RoleManagerPatch players[n] = value; } - // Assign roles + // Assign roles to each player foreach (PlayerControl pc in players) { if (pc == null || pc.roleAssigned == true) continue; + // Check if player should be impostor based on multiplier and available slots if (Impostors.Count < NumImpostors && RNG() > ImpostorMultiplier[Utils.GetHashPuid(pc)]) { var impRoles = ImpostorRoles.Shuffle(); foreach (var kvp in impRoles) { + // Assign special impostor role based on chance and availability if (RNG() <= GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(kvp.Key) && kvp.Value > 0) { - ImpostorMultiplier[Utils.GetHashPuid(pc)] += 15; + ImpostorMultiplier[Utils.GetHashPuid(pc)] += 15; // Increase chance of being crewmate next game ImpostorRoles[kvp.Key]--; Impostors.Add(pc); pc.RpcSetRole(kvp.Key); - // Desync role to other clients to prevent revealing the true role + // Desync role to hide special role from non-BAU players if (BetterGameSettings.DesyncRoles.GetBool()) { if (kvp.Key is not RoleTypes.Phantom or RoleTypes.Viper) @@ -159,6 +168,7 @@ internal static class RoleManagerPatch } } + // If no special role assigned, give regular impostor if (!Impostors.Contains(pc)) { ImpostorMultiplier[Utils.GetHashPuid(pc)] += 15; @@ -167,19 +177,20 @@ internal static class RoleManagerPatch Logger_.LogPrivate($"Assigned {RoleTypes.Impostor.GetRoleName()} role to {pc.Data.PlayerName}", "RoleManager"); } } - else + else // Assign crewmate role { var crewRoles = CrewmateRoles.Shuffle(); foreach (var kvp in crewRoles) { + // Assign special crewmate role based on chance and availability if (RNG() <= GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(kvp.Key) && kvp.Value > 0) { - ImpostorMultiplier[Utils.GetHashPuid(pc)] = 0; + ImpostorMultiplier[Utils.GetHashPuid(pc)] = 0; // Reset impostor chance CrewmateRoles[kvp.Key]--; Crewmates.Add(pc); pc.RpcSetRole(kvp.Key); - // Desync role to other clients to prevent revealing the true role + // Desync role to hide special role from non-BAU players if (BetterGameSettings.DesyncRoles.GetBool()) { if (kvp.Key is not RoleTypes.Noisemaker) @@ -196,6 +207,7 @@ internal static class RoleManagerPatch } } + // If no special role assigned, give regular crewmate if (!Crewmates.Contains(pc)) { ImpostorMultiplier[Utils.GetHashPuid(pc)] = 0; @@ -213,6 +225,7 @@ internal static class RoleManagerPatch { Logger_.LogHeader($"Better Role Assignment Has Started", "RoleManager"); + // Get impostor count from BAU settings (defaults to 1) int NumImpostors = BetterGameSettings.HideAndSeekImpNum?.GetInt() ?? 1; if (NumImpostors > BAUPlugin.AllPlayerControls.Count) @@ -222,7 +235,7 @@ internal static class RoleManagerPatch List Crewmates = []; List CrewAndImps() => [.. Impostors, .. Crewmates]; - // Set imp from settings + // Get predefined impostors from settings (host can set specific players as impostors) int[] betterImpostorSettings = [ GameOptionsManager.Instance.currentHideNSeekGameOptions.ImpostorPlayerID, @@ -250,7 +263,7 @@ internal static class RoleManagerPatch } } - // Get players in random order + // Shuffle remaining players for random role assignment List players = [.. BAUPlugin.AllPlayerControls.Where(player => !CrewAndImps().Contains(player.Data))]; int n = players.Count; @@ -263,7 +276,7 @@ internal static class RoleManagerPatch players[n] = value; } - // Assign roles + // Assign roles to remaining players foreach (PlayerControl pc in players) { if (pc == null || pc.roleAssigned == true) continue; @@ -280,6 +293,7 @@ internal static class RoleManagerPatch } } + // Apply roles to all players foreach (var data in Impostors) { var player = Utils.PlayerFromPlayerId(data.PlayerId); @@ -299,6 +313,7 @@ internal static class RoleManagerPatch [HarmonyPrefix] internal static bool RoleManager_AssignRoleOnDeath_Prefix(PlayerControl player) { + // Track available ghost roles Dictionary GhostRoles = new() // Role, Amount { { RoleTypes.GuardianAngel, 0 }, @@ -311,6 +326,7 @@ internal static class RoleManagerPatch GhostRoles[role] = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role); } + // Deduct roles already assigned to dead players foreach (var allDeadPlayers in BAUPlugin.AllPlayerControls.Where(pc => !pc.IsAlive())) { for (int i = 0; i < Roles.Count; i++) @@ -324,6 +340,7 @@ internal static class RoleManagerPatch var ghostRoles = GhostRoles.Shuffle(); + // Assign ghost role based on chance and availability foreach (var kvp in ghostRoles) { if (player.IsImpostorTeam() && kvp.Key is RoleTypes.GuardianAngel) continue; @@ -332,7 +349,7 @@ internal static class RoleManagerPatch { player.RpcSetRole(kvp.Key); - // Desync role to other clients to prevent revealing the true role + // Desync ghost role to hide it from non-BAU players if (BetterGameSettings.DesyncRoles.GetBool()) { List messageWriter = AmongUsClient.Instance.StartRpcDesync(player.NetId, (byte)RpcCalls.SetRole, SendOption.None, player.GetClientId(), clientCheck); @@ -345,6 +362,7 @@ internal static class RoleManagerPatch } } + // Assign default ghost role if no special role available player.RpcSetRole(player.Data.Role.DefaultGhostRole); return false; @@ -352,14 +370,15 @@ internal static class RoleManagerPatch internal static int RNG() { + // Choose random number generator based on settings switch (BetterGameSettings.RoleRandomizer.GetStringValue()) { case 1: - return UnityEngine.Random.Range(0, 100); + return UnityEngine.Random.Range(0, 100); // Unity RNG default: Random Random = new Random(); - return Random.Next(0, 100); + return Random.Next(0, 100); // .NET RNG } } } \ No newline at end of file diff --git a/src/Patches/Gameplay/Player/CosmeticsLayerPatch.cs b/src/Patches/Gameplay/Player/CosmeticsLayerPatch.cs index ce7fe1b..671d000 100644 --- a/src/Patches/Gameplay/Player/CosmeticsLayerPatch.cs +++ b/src/Patches/Gameplay/Player/CosmeticsLayerPatch.cs @@ -3,7 +3,6 @@ using HarmonyLib; namespace BetterAmongUs.Patches.Gameplay.Player; -// Set text color [HarmonyPatch] internal static class CosmeticsLayerPatch { @@ -11,13 +10,15 @@ internal static class CosmeticsLayerPatch [HarmonyPrefix] private static bool CosmeticsLayer_GetColorBlindText_Prefix(CosmeticsLayer __instance, ref string __result) { + // Skip processing if color ID is out of bounds (custom colors) if (__instance.bodyMatProperties.ColorId > Palette.PlayerColors.Length) return true; string colorName = Palette.GetColorName(__instance.bodyMatProperties.ColorId); if (!string.IsNullOrEmpty(colorName)) { - __result = (char.ToUpperInvariant(colorName[0]) + colorName[1..].ToLowerInvariant()).ToColor(Palette.PlayerColors[__instance.bodyMatProperties.ColorId]); + __result = (char.ToUpperInvariant(colorName[0]) + colorName[1..].ToLowerInvariant()) + .ToColor(Palette.PlayerColors[__instance.bodyMatProperties.ColorId]); } else { @@ -26,4 +27,4 @@ internal static class CosmeticsLayerPatch return false; } -} +} \ No newline at end of file diff --git a/src/Patches/Gameplay/Player/PlayerControlPatch.cs b/src/Patches/Gameplay/Player/PlayerControlPatch.cs index e3cc85b..d0d53ab 100644 --- a/src/Patches/Gameplay/Player/PlayerControlPatch.cs +++ b/src/Patches/Gameplay/Player/PlayerControlPatch.cs @@ -14,18 +14,24 @@ internal static class PlayerControlPatch [HarmonyPostfix] private static void PlayerControl_Start_Postfix(PlayerControl __instance, ref Il2CppSystem.Collections.IEnumerator __result) { + // Add player to global player list BAUPlugin.AllPlayerControls.Add(__instance); + + // Update option UI values for all players OptionPlayerItem.UpdateAllValues(); + // Append favorite color setting to player initialization coroutine __result = Effects.Sequence(__result, CoSetFavoriteColor(__instance).WrapToIl2Cpp()); } private static IEnumerator CoSetFavoriteColor(PlayerControl player) { + // Apply player's favorite color setting if they own this character if (player.AmOwner) { if (BAUPlugin.FavoriteColor.Value >= 0 && player.cosmetics.ColorId != (byte)BAUPlugin.FavoriteColor.Value) { + // Send command to server to change color player.CmdCheckColor((byte)BAUPlugin.FavoriteColor.Value); } } @@ -37,7 +43,10 @@ internal static class PlayerControlPatch [HarmonyPostfix] private static void PlayerControl_OnDestroy_Postfix(PlayerControl __instance) { + // Remove player from global list when destroyed BAUPlugin.AllPlayerControls.Remove(__instance); + + // Update option UI values OptionPlayerItem.UpdateAllValues(); } @@ -47,8 +56,10 @@ internal static class PlayerControlPatch { if (target == null) return; + // Log kill event with player names and roles Logger_.LogPrivate($"{__instance.Data.PlayerName} Has killed {target.Data.PlayerName} as {__instance.Data.RoleType.GetRoleName()}", "EventLog"); + // Track kill count in player's BetterData __instance.BetterData().RoleInfo.Kills += 1; } @@ -58,6 +69,7 @@ internal static class PlayerControlPatch { if (targetPlayer == null) return; + // Log shapeshift events (both shifting and unshifting) if (__instance != targetPlayer) Logger_.LogPrivate($"{__instance.Data.PlayerName} Has Shapeshifted into {targetPlayer.Data.PlayerName}, did animate: {animate}", "EventLog"); else @@ -68,7 +80,7 @@ internal static class PlayerControlPatch [HarmonyPostfix] private static void PlayerControl_SetRoleInvisibility_Postfix(PlayerControl __instance, bool isActive, bool shouldAnimate) { - + // Log Phantom role visibility changes if (isActive) Logger_.LogPrivate($"{__instance.Data.PlayerName} Has Vanished as Phantom, did animate: {shouldAnimate}", "EventLog"); else @@ -79,6 +91,7 @@ internal static class PlayerControlPatch [HarmonyPostfix] private static void PlayerControl_SetName_Postfix(PlayerControl __instance, string playerName) { + // Store the last set name in player's BetterData __instance.BetterDataWait(data => { data.NameSetAsLast = playerName; @@ -89,6 +102,7 @@ internal static class PlayerControlPatch [HarmonyPostfix] private static void PlayerPhysics_BootFromVent_Postfix(PlayerPhysics __instance, int ventId) { + // Log vent boot events (when engineer boots someone) Logger_.LogPrivate($"{__instance.myPlayer.Data.PlayerName} Has been booted from vent: {ventId}, as {__instance.myPlayer.Data.RoleType.GetRoleName()}", "EventLog"); } @@ -96,6 +110,7 @@ internal static class PlayerControlPatch [HarmonyPostfix] private static void PlayerPhysics_CoEnterVent_Postfix(PlayerPhysics __instance, int id) { + // Log vent entry events Logger_.LogPrivate($"{__instance.myPlayer.Data.PlayerName} Has entered vent: {id}, as {__instance.myPlayer.Data.RoleType.GetRoleName()}", "EventLog"); } @@ -103,6 +118,7 @@ internal static class PlayerControlPatch [HarmonyPostfix] private static void PlayerPhysics_CoExitVent_Postfix(PlayerPhysics __instance, int id) { + // Log vent exit events Logger_.LogPrivate($"{__instance.myPlayer.Data.PlayerName} Has exit vent: {id}, as {__instance.myPlayer.Data.RoleType.GetRoleName()}", "EventLog"); } -} +} \ No newline at end of file diff --git a/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs b/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs index d569851..482b484 100644 --- a/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs +++ b/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs @@ -16,7 +16,7 @@ internal static class PlayerJoinAndLeftPatch [HarmonyPostfix] private static void AmongUsClient_OnGameJoined_Postfix() { - // Fix host icon in lobby on modded servers + // Fix host icon color display on modded servers if (!GameState.IsVanillaServer) { var host = AmongUsClient.Instance.GetHost().Character; @@ -31,6 +31,7 @@ internal static class PlayerJoinAndLeftPatch [HarmonyPostfix] private static void AmongUsClient_OnPlayerJoined_Postfix(ClientData data) { + // Schedule ban list checks 2.5 seconds after player joins LateTask.Schedule(() => { if (GameState.IsHost) @@ -39,7 +40,7 @@ internal static class PlayerJoinAndLeftPatch { var player = Utils.PlayerFromClientId(data.Id); - // Auto ban player on ban list + // Check if player is in ban list by friend code or PUID if (BetterGameSettings.UseBanPlayerList.GetBool()) { if (player != null) @@ -52,6 +53,8 @@ internal static class PlayerJoinAndLeftPatch } } } + + // Check if player name matches banned name patterns if (BetterGameSettings.UseBanNameList.GetBool()) { if (player != null) @@ -71,6 +74,7 @@ internal static class PlayerJoinAndLeftPatch [HarmonyPostfix] private static void AmongUsClient_OnPlayerLeft_Postfix(ClientData data, DisconnectReasons reason) { + // Reclaim favorite color when player leaves in lobby if (GameState.IsLobby) { var favColorId = (byte)BAUPlugin.FavoriteColor.Value; @@ -83,6 +87,7 @@ internal static class PlayerJoinAndLeftPatch } } + // Update host icon in meeting MeetingHudPatch.UpdateHostIcon(); } @@ -93,11 +98,13 @@ internal static class PlayerJoinAndLeftPatch [HarmonyPrefix] private static void GameData_HandleDisconnect_Prefix(PlayerControl player, DisconnectReasons reason) { + // Store disconnect reason in player's BetterData if (player.BetterData() != null) { player.BetterData().DisconnectReason = reason; } + // Show custom disconnect notification BetterShowNotification(player.Data, reason); } @@ -105,16 +112,19 @@ internal static class PlayerJoinAndLeftPatch [HarmonyPrefix] internal static bool GameData_ShowNotification_Prefix() { + // Disable vanilla disconnect notifications (use BAU's instead) return false; } internal static void BetterShowNotification(NetworkedPlayerInfo playerData, DisconnectReasons reason = DisconnectReasons.Unknown, string forceReasonText = "") { + // Prevent showing duplicate notifications if (playerData.BetterData().AntiCheatInfo.BannedByAntiCheat || playerData.BetterData().HasShowDcMsg) return; playerData.BetterData().HasShowDcMsg = true; string? playerName = playerData.BetterData().RealName; + // Use custom reason text if provided if (forceReasonText != "") { var ReasonText = $"{playerData.BetterData().RealName} {forceReasonText}"; @@ -127,6 +137,7 @@ internal static class PlayerJoinAndLeftPatch { string ReasonText; + // Format disconnect message based on reason type switch (reason) { case DisconnectReasons.ExitGame: @@ -157,7 +168,8 @@ internal static class PlayerJoinAndLeftPatch Logger_.Log(ReasonText); + // Add formatted disconnect message to game UI HudManager.Instance.Notifier.AddDisconnectMessage(ReasonText); } } -} +} \ No newline at end of file diff --git a/src/Patches/Gameplay/Systems/VoteBanSystemPatch.cs b/src/Patches/Gameplay/Systems/VoteBanSystemPatch.cs index e43938c..cdec99e 100644 --- a/src/Patches/Gameplay/Systems/VoteBanSystemPatch.cs +++ b/src/Patches/Gameplay/Systems/VoteBanSystemPatch.cs @@ -17,8 +17,10 @@ internal static class VoteBanSystemPatch [HarmonyPrefix] private static bool VoteBanSystem_AddVote_Prefix(VoteBanSystem __instance, int srcClient, int clientId) { + // Skip BAU anti-cheat if disabled by other mods if (BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_Anticheat)) return true; + // If not host, allow vote and log it if (!GameState.IsHost) { DoLog = true; @@ -28,7 +30,7 @@ internal static class VoteBanSystemPatch var client = Utils.ClientFromClientId(srcClient); if (client == null) return false; - // Skip if host client + // Allow host to vote without restrictions if (client.Id == AmongUsClient.Instance.GetHost().Id) { return true; @@ -43,53 +45,62 @@ internal static class VoteBanSystemPatch } } + // Prevent voting in lobby (anti-cheat measure) if (GameState.IsLobby) { TryFlagPlayer(); return false; } + // Initialize vote tracking for this VoteBanSystem instance if (!_voteData.TryGetValue(__instance, out var voters)) { _voteData.Clear(); _voteData[__instance] = voters = []; } + // If client has no ID, allow vote but log it if (string.IsNullOrEmpty(client.ProductUserId) && string.IsNullOrEmpty(client.FriendCode)) { DoLog = true; return true; } + // Generate hash for client's ProductUserId var clientHash = Utils.GetHashUInt16(client.ProductUserId); + // Check if this client has already voted for the same target foreach (var (targetClientId, (existingHash, existingFriendCode)) in voters) { if (targetClientId != clientId) continue; + // Detect duplicate votes by comparing hash or friend code bool isDuplicateVote = existingHash == clientHash || !string.IsNullOrEmpty(client.FriendCode) && existingFriendCode == client.FriendCode; if (isDuplicateVote) { + // Block duplicate vote attempt return false; } } + // Record the vote voters.Add((clientId, (clientHash, client.FriendCode))); DoLog = true; return true; } - [HarmonyPatch(typeof(VoteBanSystem), nameof(VoteBanSystem.AddVote))] [HarmonyPostfix] private static void VoteBanSystem_AddVote_Postfix(VoteBanSystem __instance, int srcClient, int clientId) { + // Skip logging if anti-cheat disabled if (BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_Anticheat)) return; + // Log the vote if it was allowed if (DoLog) { LogVote(__instance, srcClient, clientId); @@ -99,18 +110,21 @@ internal static class VoteBanSystemPatch private static void LogVote(VoteBanSystem voteBanSystem, int srcClient, int clientId) { + // Get source and target client info var src = Utils.ClientFromClientId(srcClient); var client = Utils.ClientFromClientId(clientId); + // Calculate current votes and required votes int currentVotes = 0; int maxVotes = 0; if (voteBanSystem.Votes.TryGetValue(clientId, out var votes)) { - currentVotes = votes.Count(v => v != 0); - maxVotes = votes.Length; + currentVotes = votes.Count(v => v != 0); // Count non-zero votes + maxVotes = votes.Length; // Total possible votes } + // Log vote with colored player names and vote count Logger_.InGame( $"{src.Character?.GetPlayerNameAndColor() ?? src.PlayerName} " + $"voted to kick {client.Character?.GetPlayerNameAndColor() ?? client.PlayerName} " + diff --git a/src/Patches/Gameplay/UI/Chat/ChatCommandsPatch.cs b/src/Patches/Gameplay/UI/Chat/ChatCommandsPatch.cs index 75300fc..d4f1abe 100644 --- a/src/Patches/Gameplay/UI/Chat/ChatCommandsPatch.cs +++ b/src/Patches/Gameplay/UI/Chat/ChatCommandsPatch.cs @@ -15,7 +15,7 @@ internal static class ChatCommandsPatch private static bool _enabled = true; internal static string CommandPrefix => BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Force_BAU_Command_Prefix) ? "bau:" : BAUPlugin.CommandPrefix.Value; - // Run code for specific commands + // Execute command when valid command is typed private static void HandleCommand() { if (closestCommand != null && isTypedOut) @@ -28,45 +28,55 @@ internal static class ChatCommandsPatch } } - // Check if command is typed when sending chat message + // Intercept chat messages to handle commands [HarmonyPatch(typeof(ChatController), nameof(ChatController.SendChat))] [HarmonyPrefix] private static bool ChatController_SendChat_Prefix(ChatController __instance) { + // Skip if commands are disabled if (!_enabled || BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_AllCommands)) { return true; } + // Check chat cooldown bool IsOnCooldown = 3f - __instance.timeSinceLastMessage > 0f; string text = __instance.freeChatField.textArea.text; + // If not a command, handle as normal chat (with restrictions) if (!text.StartsWith(CommandPrefix) || IsOnCooldown) { + // Prevent chat during gameplay if not in meeting if (GameState.IsInGame && !GameState.IsLobby && !GameState.IsFreePlay && !GameState.IsMeeting && !GameState.IsExilling && PlayerControl.LocalPlayer.IsAlive()) return false; + // Add to chat history if (ChatPatch.ChatHistory.Count == 0 || ChatPatch.ChatHistory[^1] != text) ChatPatch.ChatHistory.Add(text); ChatPatch.CurrentHistorySelection = ChatPatch.ChatHistory.Count; return true; } + // Check if command can be executed if (!closestCommand.CanRunCommand(out string _)) { return false; } + // Execute command HandleCommand(); + // Add command to chat history if (ChatPatch.ChatHistory.Count == 0 || ChatPatch.ChatHistory[^1] != text) ChatPatch.ChatHistory.Add(text); ChatPatch.CurrentHistorySelection = ChatPatch.ChatHistory.Count; + // Reset chat timer if command sets it if (closestCommand?.SetChatTimer == true) { __instance.timeSinceLastMessage = 0f; } + // Clear chat input __instance.freeChatField.Clear(); __instance.quickChatMenu.Clear(); __instance.quickChatField.Clear(); @@ -74,7 +84,7 @@ internal static class ChatCommandsPatch return false; } - // Set up command helper + // Set up command helper UI elements private static TextMeshPro? commandText; private static TextMeshPro? commandInfo; @@ -82,6 +92,7 @@ internal static class ChatCommandsPatch [HarmonyPostfix] private static void ChatController_Awake_Postfix(ChatController __instance) { + // Create semi-transparent text for command suggestions if (commandText == null) { var TextArea = __instance.freeChatField.textArea.outputText; @@ -92,6 +103,7 @@ internal static class ChatCommandsPatch commandText.GetComponent().color = new Color(1f, 1f, 1f, 0.5f); } + // Create info text for command descriptions and arguments if (commandInfo == null) { var TextArea = __instance.freeChatField.textArea.outputText; @@ -112,7 +124,7 @@ internal static class ChatCommandsPatch private static string typedCommand = ""; private static BaseCommand? closestCommand; - // Command helper + // Update command suggestion UI as user types [HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] [HarmonyPostfix] private static void ChatController_Update_Postfix(ChatController __instance) @@ -128,11 +140,13 @@ internal static class ChatCommandsPatch if (commandText == null || commandInfo == null) return; + // Check if text starts with command prefix if (text.Length > 0 && text.StartsWith(CommandPrefix)) { typedCommand = text.Length > CommandPrefix.Length ? text[CommandPrefix.Length..] : string.Empty; string[] typedParts = typedCommand.Split(' '); + // Find closest matching command closestCommand = GetClosestCommand(typedParts[0]); bool isSuggestionValid = closestCommand != null && (typedParts[0].Equals(closestCommand.Name, StringComparison.OrdinalIgnoreCase) || typedParts.Length == 1); @@ -163,36 +177,44 @@ internal static class ChatCommandsPatch { isTypedOut = true; + // Generate suggestion text string suggestion = GenerateSuggestion(typedParts); string fullSuggestion = CommandPrefix + suggestion; + // Tab completion if (Input.GetKeyDown(KeyCode.Tab) && typedParts.Length >= 1) { __instance.freeChatField.textArea.SetText(fullSuggestion); } + // Red text if command cannot be run if (!closestCommand.CanRunCommand(out string _)) { fullSuggestion = fullSuggestion.ToColor("#FF0300".HexToColor()); } + // Update UI elements commandText.text = fullSuggestion; commandInfo.text = $"{closestCommand.Description}{GenerateArgumentInfo()}{GenerateCanRunInfo()}"; } private static string GenerateSuggestion(string[] typedParts) { + // If only command name typed, return full command name if (typedParts.Length == 1) return closestCommand.Name; + // Update command arguments with typed values UpdateCommandArguments(typedParts); + // Get next argument index for autocomplete int nextArgumentIndex = typedParts.Length - 2; if (nextArgumentIndex >= 0 && nextArgumentIndex < closestCommand.Arguments.Length) { var argument = closestCommand.Arguments[nextArgumentIndex]; var closestSuggestion = argument.GetClosestSuggestion(); + // Complete partial argument return typedCommand + closestSuggestion[Math.Min(argument.Arg.Length, closestSuggestion.Length)..]; } @@ -212,12 +234,14 @@ internal static class ChatCommandsPatch if (closestCommand.Arguments.Length == 0) return string.Empty; + // Format argument help text var argumentInfo = string.Join(" ", closestCommand.Arguments.Select(arg => arg.ArgInfo)); return $" - <#a6a6a6>{argumentInfo}"; } private static string GenerateCanRunInfo() { + // Show reason if command cannot be run if (!closestCommand.CanRunCommand(out string reason)) { return $" - <#FF0300>{reason}"; @@ -228,12 +252,14 @@ internal static class ChatCommandsPatch internal static BaseCommand? GetClosestCommand(string typedCommand) { + // First try exact match var directNormalMatch = BaseCommand.allCommands .FirstOrDefault(c => FilterCommand(c, CommandType.Normal) && c.Names.Any(name => string.Equals(name, typedCommand, StringComparison.OrdinalIgnoreCase))); if (directNormalMatch != null) return directNormalMatch; + // Then try partial match var closestNormalCommand = BaseCommand.allCommands .OrderBy(c => c.Name) .FirstOrDefault(c => FilterCommand(c, CommandType.Normal) && @@ -246,6 +272,7 @@ internal static class ChatCommandsPatch private static bool FilterCommand(BaseCommand command, CommandType commandType) { + // Check if command is enabled and not disabled by other mods return command.Type == commandType && command.ShowCommand() && !BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_Command + command.Name); } -} +} \ No newline at end of file diff --git a/src/Patches/Gameplay/UI/Chat/ChatPatch.cs b/src/Patches/Gameplay/UI/Chat/ChatPatch.cs index 29d7ce6..c9d3d91 100644 --- a/src/Patches/Gameplay/UI/Chat/ChatPatch.cs +++ b/src/Patches/Gameplay/UI/Chat/ChatPatch.cs @@ -22,6 +22,7 @@ internal static class ChatPatch { if (!HudManager.InstanceExists) return; + // Clear all chat bubbles HudManager.Instance.Chat.chatBubblePool.ReclaimAll(); } @@ -29,6 +30,7 @@ internal static class ChatPatch { if (!HudManager.InstanceExists) return; + // Clear only player chat bubbles (keep command bubbles) foreach (var obj in HudManager.Instance.Chat.chatBubblePool.activeChildren.ToArray()) { var chatBubble = obj.GetComponent(); @@ -45,6 +47,7 @@ internal static class ChatPatch { if (!HudManager.InstanceExists) return; + // Clear only command chat bubbles (keep player chat) foreach (var obj in HudManager.Instance.Chat.chatBubblePool.activeChildren.ToArray()) { var chatBubble = obj.GetComponent(); @@ -61,6 +64,7 @@ internal static class ChatPatch [HarmonyPostfix] private static void ChatController_Toggle_Postfix(/*ChatController __instance*/) { + // Apply chat theme when chat is opened/closed SetChatTheme(); } @@ -69,6 +73,7 @@ internal static class ChatPatch [HarmonyPriority(Priority.First)] private static void ChatController_Update_Prefix(ChatController __instance) { + // Apply dark/light theme to chat input field if (BAUPlugin.ChatDarkMode.Value) { // Free chat color @@ -84,18 +89,21 @@ internal static class ChatPatch __instance.freeChatField.textArea.outputText.color = Color.black; } + // Ctrl+X to cut text to clipboard if ((Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) && Input.GetKeyDown(KeyCode.X)) { ClipboardHelper.PutClipboardString(__instance.freeChatField.textArea.text); __instance.freeChatField.textArea.SetText(""); } + // Up arrow for chat history navigation if (Input.GetKeyDown(KeyCode.UpArrow) && ChatHistory.Any()) { CurrentHistorySelection = Mathf.Clamp(--CurrentHistorySelection, 0, ChatHistory.Count - 1); __instance.freeChatField.textArea.SetText(ChatHistory[CurrentHistorySelection]); } + // Down arrow for chat history navigation if (Input.GetKeyDown(KeyCode.DownArrow) && ChatHistory.Any()) { CurrentHistorySelection++; @@ -105,11 +113,12 @@ internal static class ChatPatch } } - // Add extra information to chat bubble + // Log chat messages to console [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] [HarmonyPostfix] private static void ChatController_AddChat_Postfix(ChatController __instance, PlayerControl sourcePlayer, string chatText) { + // Log chat publicly if player is alive, privately if dead if (sourcePlayer.IsAlive() || !PlayerControl.LocalPlayer.IsAlive()) { Logger_.Log($"{sourcePlayer.Data.PlayerName} -> {chatText}", "ChatLog"); @@ -133,15 +142,20 @@ internal static class ChatPatch string hashPuid = Utils.GetHashPuid(sourcePlayer); string friendCode = playerInfo.FriendCode; string playerName = playerInfo.BetterData()?.RealName ?? "???"; + + // Format role display with team color string Role = $"{sourcePlayer.GetRoleName()}+++"; + // In lobby, show player tags instead of roles if (GameState.IsLobby && !GameState.IsFreePlay) { Role = ""; + // Show BAU user tag if (sourcePlayer.IsLocalPlayer() || sourcePlayer.BetterData().IsBetterUser) sbTag.AppendFormat("{1}{0}+++", Translator.GetString("Player.BetterUser"), sourcePlayer.BetterData().IsVerifiedBetterUser || sourcePlayer.IsLocalPlayer() ? "✓ " : ""); + // Show mod-specific tags based on player data if (BetterDataManager.BetterDataFile.SickoData.Any(info => info.CheckPlayerData(sourcePlayer.Data))) sbTag.Append($"{Translator.GetString("Player.SickoUser")}+++"); else if (BetterDataManager.BetterDataFile.AUMData.Any(info => info.CheckPlayerData(sourcePlayer.Data))) @@ -152,6 +166,7 @@ internal static class ChatPatch sbTag.Append($"{Translator.GetString("Player.KnownCheater")}+++"); } + // Hide roles from alive players (unless same team) if (!sourcePlayer.IsImpostorTeammate()) { if (PlayerControl.LocalPlayer.IsAlive() && !sourcePlayer.IsLocalPlayer()) @@ -160,11 +175,13 @@ internal static class ChatPatch } } + // Show role for dead players or if local player is Guardian Angel if (PlayerControl.LocalPlayer.Is(RoleTypes.GuardianAngel) && !sourcePlayer.IsAlive() || !PlayerControl.LocalPlayer.Is(RoleTypes.GuardianAngel)) { sbTag.Append(Role); } + // Format tags with separators sbInfo.Append(""); for (int i = 0; i < sbTag.ToString().Split("+++").Length; i++) { @@ -182,6 +199,7 @@ internal static class ChatPatch } sbInfo.Append(""); + // Position tags before local player name, after other players' names bool flag = sourcePlayer.IsLocalPlayer(); if (flag) { @@ -227,13 +245,14 @@ internal static class ChatPatch chat.openKeyboardButton.transform.Find("OpenKeyboardIcon").GetComponent().color = new Color(1f, 1f, 1f, 1f); } + // Apply theme to all existing chat bubbles foreach (var item in HudManager.Instance.Chat.chatBubblePool.activeChildren.SelectIl2Cpp(c => c.GetComponent())) { SetChatPoolTheme(item); } } - // Set chat theme + // Apply theme to individual chat bubble internal static ChatBubble SetChatPoolTheme(ChatBubble asChatBubble) { ChatBubble chatBubble = asChatBubble; @@ -243,6 +262,7 @@ internal static class ChatPatch chatBubble.transform.Find("ChatText (TMP)").GetComponentInChildren(true).color = new Color(1f, 1f, 1f, 1f); chatBubble.transform.Find("Background").GetComponentInChildren(true).color = new Color(0.15f, 0.15f, 0.15f, 1f); + // Make dead player chat bubbles more transparent if (chatBubble.transform.Find("PoolablePlayer/xMark") != null) { if (chatBubble.transform.Find("PoolablePlayer/xMark").GetComponentInChildren(true).enabled == true) @@ -256,6 +276,7 @@ internal static class ChatPatch chatBubble.transform.Find("ChatText (TMP)").GetComponentInChildren(true).color = new Color(0f, 0f, 0f, 1f); chatBubble.transform.Find("Background").GetComponentInChildren(true).color = new Color(1f, 1f, 1f, 1f); + // Make dead player chat bubbles more transparent if (chatBubble.transform.Find("PoolablePlayer/xMark") != null) { if (chatBubble.transform.Find("PoolablePlayer/xMark").GetComponentInChildren(true).enabled == true) @@ -272,6 +293,7 @@ internal static class ChatPatch [HarmonyPostfix] private static void FreeChatInputField_Awake_Postfix(FreeChatInputField __instance) { + // Enable extended character support for chat __instance.textArea.allowAllCharacters = true; __instance.textArea.AllowSymbols = true; __instance.textArea.AllowPaste = true; @@ -284,6 +306,7 @@ internal static class ChatPatch [HarmonyPostfix] private static void FreeChatInputField_UpdateCharCount_Postfix(FreeChatInputField __instance) { + // Update character counter with color coding int length = __instance.textArea.text.Length; __instance.charCountText.text = string.Format("{0}/118", length); __instance.charCountText.color = GetCharColor(length); @@ -291,6 +314,7 @@ internal static class ChatPatch private static Color GetCharColor(int length) { + // Color gradient: green -> yellow -> red as text length increases Color[] colorGradient = [ Color.green, @@ -301,4 +325,4 @@ internal static class ChatPatch (float min, float max) lerpRange = (0f, 117f); return colorGradient.LerpColor(lerpRange, length); } -} +} \ No newline at end of file