From eed3d24f9d7b1333d3a3a9cf7f888eaebd59aaf6 Mon Sep 17 00:00:00 2001 From: D1GQ Date: Tue, 18 Aug 2026 15:19:10 -0500 Subject: [PATCH] Add support to v18.0 and remove BetterRoleAssignment --- src/BetterAmongUs.csproj | 2 +- src/Commands/ForceSkipCommand.cs | 8 +- src/Interfaces/IMonoExtension.cs | 22 -- src/ModInfo.cs | 6 +- src/Modules/GameState.cs | 4 +- src/Modules/OptionItems/OptionItem.cs | 4 +- src/MonoScripts/BetterPingTracker.cs | 11 +- src/MonoScripts/MeetingInfoDisplay.cs | 4 +- .../Gameplay/Managers/HudManagerPatch.cs | 39 +-- .../Gameplay/Managers/RoleManagerPatch.cs | 308 ------------------ src/Patches/Gameplay/UI/Chat/ChatPatch.cs | 18 + src/Patches/Gameplay/UI/MeetingHudPatch.cs | 2 +- src/Utilities/RoleUtils.cs | 3 +- 13 files changed, 61 insertions(+), 370 deletions(-) diff --git a/src/BetterAmongUs.csproj b/src/BetterAmongUs.csproj index 87b4959..344b17c 100644 --- a/src/BetterAmongUs.csproj +++ b/src/BetterAmongUs.csproj @@ -79,7 +79,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Commands/ForceSkipCommand.cs b/src/Commands/ForceSkipCommand.cs index e95fcfe..e9d16ed 100644 --- a/src/Commands/ForceSkipCommand.cs +++ b/src/Commands/ForceSkipCommand.cs @@ -44,7 +44,7 @@ internal sealed class ForceSkipCommand : BaseCommand if (!countVotes) { var states = Array.Empty(); - MeetingHud.Instance.RpcVotingComplete(states, null, false); + MeetingHud.Instance.RpcVotingComplete(states, null, false, false, 0); } else { @@ -57,11 +57,11 @@ internal sealed class ForceSkipCommand : BaseCommand PlayerVoteArea playerVoteArea = MeetingHud.Instance.playerStates[i]; array[i] = new MeetingHud.VoterState { - VoterId = playerVoteArea.TargetPlayerId, - VotedForId = playerVoteArea.VotedFor + VoterId = playerVoteArea.PlayerId, + VotedForId = playerVoteArea.VotedForId }; } - MeetingHud.Instance.RpcVotingComplete(array, networkedPlayerInfo, tie); + MeetingHud.Instance.RpcVotingComplete(array, networkedPlayerInfo, tie, false, 0); } } } diff --git a/src/Interfaces/IMonoExtension.cs b/src/Interfaces/IMonoExtension.cs index 2103230..67579d4 100644 --- a/src/Interfaces/IMonoExtension.cs +++ b/src/Interfaces/IMonoExtension.cs @@ -312,26 +312,4 @@ internal interface IMonoExtension : IMonoExtension where T : MonoBehaviour { OnExtensionAwake((T)baseMono); } -} - -/// -/// Extension methods for Unity's UnityEngine.Object types. -/// -public static class UnityObjectExtensions -{ - /// - /// Determines whether a Unity object has been destroyed or is null. - /// - /// The Unity object to check. - /// - /// true if the object has been destroyed or is null; otherwise, false. - /// - /// - /// This method uses Unity's overloaded == operator which returns true for - /// destroyed objects even though the C# reference still exists. - /// - public static bool IsDestroyedOrNull(this UnityEngine.Object obj) - { - return obj == null; - } } \ No newline at end of file diff --git a/src/ModInfo.cs b/src/ModInfo.cs index 3c6edad..594331a 100644 --- a/src/ModInfo.cs +++ b/src/ModInfo.cs @@ -58,11 +58,7 @@ internal static class ModInfo /// internal static string[] SupportedAmongUsVersions = [ - "2026.6.5", - "2026.4.7", // Andiroid only update - "2026.3.31", - "2026.3.17", - "2025.11.18" + "2026.8.18" ]; /// diff --git a/src/Modules/GameState.cs b/src/Modules/GameState.cs index 014420a..a1bf349 100644 --- a/src/Modules/GameState.cs +++ b/src/Modules/GameState.cs @@ -195,7 +195,7 @@ internal static class GameState if (!IsMeeting || MeetingHud.Instance == null) return false; - return MeetingHud.Instance.state is MeetingHud.VoteStates.Voted or MeetingHud.VoteStates.NotVoted; + return MeetingHud.Instance.state is MeetingHud.MeetingStates.Voted or MeetingHud.MeetingStates.NotVoted; } } @@ -209,7 +209,7 @@ internal static class GameState if (!IsMeeting || MeetingHud.Instance == null) return false; - return MeetingHud.Instance.state == MeetingHud.VoteStates.Proceeding; + return MeetingHud.Instance.state == MeetingHud.MeetingStates.Proceeding; } } diff --git a/src/Modules/OptionItems/OptionItem.cs b/src/Modules/OptionItems/OptionItem.cs index e665587..5650cf6 100644 --- a/src/Modules/OptionItems/OptionItem.cs +++ b/src/Modules/OptionItems/OptionItem.cs @@ -354,7 +354,7 @@ public abstract class OptionItem /// Gets the boxed value of this option. /// /// The option value as an object. - public abstract object GetBoxedValue(); + public abstract object? GetBoxedValue(); /// /// Represents a node in the option hierarchy tree for text formatting. @@ -399,7 +399,7 @@ public abstract class OptionItem : OptionItem /// Gets the boxed value of this option. /// /// The option value as an object. - public override sealed object GetBoxedValue() + public override sealed object? GetBoxedValue() { return GetValue(); } diff --git a/src/MonoScripts/BetterPingTracker.cs b/src/MonoScripts/BetterPingTracker.cs index 7a5b04f..ceba402 100644 --- a/src/MonoScripts/BetterPingTracker.cs +++ b/src/MonoScripts/BetterPingTracker.cs @@ -2,6 +2,7 @@ using BetterAmongUs.Data.Config; using BetterAmongUs.Generated; using BetterAmongUs.Modules; +using BetterAmongUs.Patches.Gameplay.UI.Chat; using BetterAmongUs.Utilities; using System.Text; using TMPro; @@ -56,7 +57,15 @@ internal sealed class BetterPingTracker : MonoBehaviour return; // Update position and appearance - aspectPosition.DistanceFromEdge = new Vector3(4f, 0.1f, -5); + if (ChatPatch.IsChatVisible) + { + aspectPosition.DistanceFromEdge = new Vector3(4.6f, 0.1f, -5); + } + else + { + aspectPosition.DistanceFromEdge = new Vector3(4f, 0.1f, -5); + } + aspectPosition.Alignment = AspectPosition.EdgeAlignments.RightTop; text.outlineWidth = 0.3f; diff --git a/src/MonoScripts/MeetingInfoDisplay.cs b/src/MonoScripts/MeetingInfoDisplay.cs index 3f893a6..2d54fd5 100644 --- a/src/MonoScripts/MeetingInfoDisplay.cs +++ b/src/MonoScripts/MeetingInfoDisplay.cs @@ -57,7 +57,7 @@ internal sealed class MeetingInfoDisplay : PlayerInfoDisplay var IdNumber = LevelDisplay.transform.Find("LevelNumber"); IdLabel.gameObject.DestroyTextTranslators(); IdLabel.GetComponent().text = "ID"; - IdNumber.GetComponent().text = pva.TargetPlayerId.ToString(); + IdNumber.GetComponent().text = pva.PlayerId.Value.ToString(); IdLabel.name = "IdLabel"; IdNumber.name = "IdNumber"; PlayerLevel.transform.position += new Vector3(0.23f, 0f); @@ -260,7 +260,7 @@ internal sealed class MeetingInfoDisplay : PlayerInfoDisplay /// Disconnect reason text. private string GetDisconnectText() { - var playerData = GameData.Instance.GetPlayerById(_pva.TargetPlayerId); + var playerData = GameData.Instance.GetPlayerById(_pva.PlayerId); if (playerData == null) return string.Empty; diff --git a/src/Patches/Gameplay/Managers/HudManagerPatch.cs b/src/Patches/Gameplay/Managers/HudManagerPatch.cs index 0969d27..d5ae239 100644 --- a/src/Patches/Gameplay/Managers/HudManagerPatch.cs +++ b/src/Patches/Gameplay/Managers/HudManagerPatch.cs @@ -1,8 +1,8 @@ using BepInEx.Unity.IL2CPP.Utils; -using BetterAmongUs.Data.Config; using BetterAmongUs.Generated; using BetterAmongUs.Managers; using BetterAmongUs.Modules; +using BetterAmongUs.Patches.Gameplay.UI.Chat; using BetterAmongUs.Utilities; using HarmonyLib; using System.Collections; @@ -53,26 +53,23 @@ internal static class HudManagerPatch if (__instance.Chat == null) return; - if (!BAUConfigs.ChatInGameplay.Value) - { - // Vanilla chat behavior: only show chat when dead or during meetings - if (!PlayerControl.LocalPlayer.IsAlive()) - { - __instance.Chat.gameObject.SetActive(true); - } - else if (GameState.IsInGamePlay && !(GameState.IsMeeting || GameState.IsExilling)) - { - __instance.Chat.gameObject.SetActive(false); - } - } - else - { - // BAU chat behavior: always show chat when enabled - if (__instance.Chat.gameObject.active == false) - { - __instance.Chat.gameObject.SetActive(true); - } - } + __instance.Chat.gameObject.SetActive(ChatPatch.IsChatVisible); } } + + [HarmonyPatch(typeof(MatchInfoHudButton), nameof(MatchInfoHudButton.Update))] + [HarmonyPrefix] + private static bool MatchInfoHudButton_Update_Prefix(MatchInfoHudButton __instance) + { + if (ChatPatch.IsChatVisible) + { + __instance.aspectPosition.DistanceFromEdge = MatchInfoHudButton.adjustedDistanceFromEdge; + } + else + { + __instance.aspectPosition.DistanceFromEdge = MatchInfoHudButton.defaultDistanceFromEdge; + } + + return false; + } } \ No newline at end of file diff --git a/src/Patches/Gameplay/Managers/RoleManagerPatch.cs b/src/Patches/Gameplay/Managers/RoleManagerPatch.cs index 2117fe4..c00a056 100644 --- a/src/Patches/Gameplay/Managers/RoleManagerPatch.cs +++ b/src/Patches/Gameplay/Managers/RoleManagerPatch.cs @@ -1,10 +1,6 @@ using AmongUs.GameOptions; -using BetterAmongUs.Modules; -using BetterAmongUs.Modules.Support; using BetterAmongUs.MonoScripts.Extended; -using BetterAmongUs.Patches.Gameplay.UI.Settings; using BetterAmongUs.Utilities; -using BetterAmongUs.Utilities.Extension; using HarmonyLib; namespace BetterAmongUs.Patches.Gameplay.Managers; @@ -12,9 +8,6 @@ namespace BetterAmongUs.Patches.Gameplay.Managers; [HarmonyPatch] internal static class RoleManagerPatch { - internal static Dictionary ImpostorMultiplier = []; // HashPuid, Multiplier - private static readonly Random random = new(); - [HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SetRole))] [HarmonyPrefix] private static void RoleManager_SetRole_Prefix(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType) @@ -28,305 +21,4 @@ internal static class RoleManagerPatch } } } - - // 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(); - } - else - { - HideAndSeekBetterRoleAssignment(); - } - - // Return false to prevent vanilla role assignment from running - return false; - } - - internal static void RegularBetterRoleAssignment() - { - Logger_.LogHeader($"Better Role Assignment Has Started", "RoleManager"); - - // 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 }, - { 5, 2 }, - { 7, 3 } - }; - - foreach (var limit in impostorLimits) - { - if (NumPlayers <= limit.Key) - { - NumImpostors = Math.Min(NumImpostors, limit.Value); - break; - } - } - - List Impostors = []; - List Crewmates = []; - - // Track available roles and their counts - Dictionary ImpostorRoles = new() // Role, Amount - { - { RoleTypes.Shapeshifter, 0 }, - { RoleTypes.Phantom, 0 }, - { RoleTypes.Viper, 0 } - }; - - Dictionary CrewmateRoles = new() // Role, Amount - { - { RoleTypes.Engineer, 0 }, - { RoleTypes.Scientist, 0 }, - { RoleTypes.Tracker, 0 }, - { RoleTypes.Noisemaker, 0 }, - { RoleTypes.Detective, 0 } - }; - - List Roles = [.. ImpostorRoles.Keys, .. CrewmateRoles.Keys]; - - // Get role counts from game options - foreach (RoleTypes role in Roles) - { - if (role.IsImpostorRole()) - ImpostorRoles[role] = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role); - else - CrewmateRoles[role] = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role); - } - - // 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; - while (n > 1) - { - n--; - int k = random.Next(n + 1); - PlayerControl value = players[k]; - players[k] = players[n]; - players[n] = value; - } - - // 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; // Increase chance of being crewmate next game - ImpostorRoles[kvp.Key]--; - Impostors.Add(pc); - pc.RpcSetRole(kvp.Key); - - Logger_.LogPrivate($"Assigned {kvp.Key.GetRoleName()} role to {pc.Data.PlayerName}", "RoleManager"); - break; - } - } - - // If no special role assigned, give regular impostor - if (!Impostors.Contains(pc)) - { - ImpostorMultiplier[Utils.GetHashPuid(pc)] += 15; - Impostors.Add(pc); - pc.RpcSetRole(RoleTypes.Impostor); - Logger_.LogPrivate($"Assigned {RoleTypes.Impostor.GetRoleName()} role to {pc.Data.PlayerName}", "RoleManager"); - } - } - 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; // Reset impostor chance - CrewmateRoles[kvp.Key]--; - Crewmates.Add(pc); - pc.RpcSetRole(kvp.Key); - - Logger_.LogPrivate($"Assigned {kvp.Key.GetRoleName()} role to {pc.Data.PlayerName}", "RoleManager"); - break; - } - } - - // If no special role assigned, give regular crewmate - if (!Crewmates.Contains(pc)) - { - ImpostorMultiplier[Utils.GetHashPuid(pc)] = 0; - Crewmates.Add(pc); - pc.RpcSetRole(RoleTypes.Crewmate); - Logger_.LogPrivate($"Assigned {RoleTypes.Crewmate.GetRoleName()} role to {pc.Data.PlayerName}", "RoleManager"); - } - } - } - - Logger_.LogHeader($"Better Role Assignment Has Finished", "RoleManager"); - } - - internal static void HideAndSeekBetterRoleAssignment() - { - Logger_.LogHeader($"Better Role Assignment Has Started", "RoleManager"); - - // Get impostor count from BAU settings (defaults to 1) - int NumImpostors = GameOptionsManager.Instance.currentHideNSeekGameOptions.NumImpostors; - - if (NumImpostors > BAUPlugin.AllPlayerControls.Count) - NumImpostors = BAUPlugin.AllPlayerControls.Count; - - List Impostors = []; - List Crewmates = []; - List CrewAndImps() => [.. Impostors, .. Crewmates]; - - // Get predefined impostor from settings - var impId = GameOptionsManager.Instance.currentHideNSeekGameOptions.ImpostorPlayerID; - if (impId != -1) - { - var player = Utils.PlayerFromPlayerId(impId); - if (player != null) - { - if (Impostors.Count < NumImpostors) - { - Impostors.Add(player.Data); - Logger_.LogPrivate($"Settings Assigned {RoleTypes.Impostor.GetRoleName()} role to {player.Data.PlayerName}", "RoleManager"); - } - } - } - - // Shuffle remaining players for random role assignment - List players = [.. BAUPlugin.AllPlayerControls.Where(player => !CrewAndImps().Contains(player.Data))]; - - int n = players.Count; - while (n > 1) - { - n--; - int k = random.Next(n + 1); - PlayerControl value = players[k]; - players[k] = players[n]; - players[n] = value; - } - - // Assign roles to remaining players - foreach (PlayerControl pc in players) - { - if (pc == null || pc.roleAssigned == true) continue; - - if (Impostors.Count < NumImpostors) - { - Impostors.Add(pc.Data); - Logger_.LogPrivate($"Assigned {RoleTypes.Impostor.GetRoleName()} role to {pc.Data.PlayerName}", "RoleManager"); - } - else - { - Crewmates.Add(pc.Data); - Logger_.LogPrivate($"Assigned {RoleTypes.Engineer.GetRoleName()} role to {pc.Data.PlayerName}", "RoleManager"); - } - } - - // Apply roles to all players - foreach (var data in Impostors) - { - var player = Utils.PlayerFromPlayerId(data.PlayerId); - player.RpcSetRole(RoleTypes.Impostor); - } - - foreach (var data in Crewmates) - { - var player = Utils.PlayerFromPlayerId(data.PlayerId); - player.RpcSetRole(RoleTypes.Engineer); - } - - Logger_.LogHeader($"Better Role Assignment Has Finished", "RoleManager"); - } - - [HarmonyPatch(typeof(RoleManager), nameof(RoleManager.AssignRoleOnDeath))] - [HarmonyPrefix] - internal static bool RoleManager_AssignRoleOnDeath_Prefix(PlayerControl player) - { - // Track available ghost roles - Dictionary GhostRoles = new() // Role, Amount - { - { RoleTypes.GuardianAngel, 0 }, - }; - - List Roles = [.. GhostRoles.Keys]; - - foreach (RoleTypes role in Roles) - { - 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++) - { - if (allDeadPlayers.Is(Roles[i])) - { - GhostRoles[Roles[i]]--; - } - } - } - - 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; - - if (kvp.Value > 0 && RNG() <= GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(kvp.Key)) - { - player.RpcSetRole(kvp.Key); - - return false; - } - } - - // Assign default ghost role if no special role available - player.RpcSetRole(player.Data.Role.DefaultGhostRole); - - return false; - } - - internal static int RNG() - { - // Choose random number generator based on settings - switch (BetterGameSettings.RoleRandomizer.GetStringIndex()) - { - case 1: - return UnityEngine.Random.Range(0, 100); // Unity RNG - - default: - Random Random = new Random(); - return Random.Next(0, 100); // .NET RNG - } - } } \ 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 3ac809a..32ea480 100644 --- a/src/Patches/Gameplay/UI/Chat/ChatPatch.cs +++ b/src/Patches/Gameplay/UI/Chat/ChatPatch.cs @@ -21,6 +21,24 @@ internal static class ChatPatch internal const string COMMAND_POSTFIX_ID = "IsCommand"; + internal static bool IsChatVisible + { + get + { + if (BAUConfigs.ChatInGameplay.Value) + { + return true; + } + + if (!GameState.IsInGamePlay) + { + return true; + } + + return !PlayerControl.LocalPlayer.IsAlive() || GameState.IsMeeting || GameState.IsExilling; + } + } + /// /// Removes all chat bubbles from the chat. /// diff --git a/src/Patches/Gameplay/UI/MeetingHudPatch.cs b/src/Patches/Gameplay/UI/MeetingHudPatch.cs index 1242b57..bcbd944 100644 --- a/src/Patches/Gameplay/UI/MeetingHudPatch.cs +++ b/src/Patches/Gameplay/UI/MeetingHudPatch.cs @@ -32,7 +32,7 @@ internal static class MeetingHudPatch { if (pva == null) continue; - var target = Utils.PlayerFromPlayerId(pva.TargetPlayerId); + var target = Utils.PlayerFromPlayerId(pva.PlayerId); if (target == null) continue; pva.gameObject.AddComponent().Init(target, pva); diff --git a/src/Utilities/RoleUtils.cs b/src/Utilities/RoleUtils.cs index 323ff69..23439ef 100644 --- a/src/Utilities/RoleUtils.cs +++ b/src/Utilities/RoleUtils.cs @@ -140,7 +140,8 @@ internal static class RoleUtils { RoleTypes.Engineer, "#8f8f8f" }, { RoleTypes.Noisemaker, "#fc7c7c" }, { RoleTypes.Tracker, "#59f002" }, - { RoleTypes.Detective, "#0027FF" }, + { RoleTypes.Detective, "#0027ff" }, + { RoleTypes.Judge, "#e3d12b" }, { RoleTypes.ImpostorGhost, Colors.ImpostorRed.ColorToHex() }, { RoleTypes.Impostor, Colors.ImpostorRed.ColorToHex() }, { RoleTypes.Shapeshifter, "#f06102" },