Add support to v18.0 and remove BetterRoleAssignment

This commit is contained in:
D1GQ committed 2026-08-18 15:19:10 -05:00
1 parent 6aea28e9e9
commit eed3d24f9d
13 files changed
+61 -370

No files matched your search

+1 -1
View File
@@ -79,7 +79,7 @@
<!-- NuGet Core Packages -->
<ItemGroup>
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.6.5" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.8.18" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+4 -4
View File
@@ -44,7 +44,7 @@ internal sealed class ForceSkipCommand : BaseCommand
if (!countVotes)
{
var states = Array.Empty<MeetingHud.VoterState>();
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);
}
}
}
-22
View File
@@ -312,26 +312,4 @@ internal interface IMonoExtension<T> : IMonoExtension where T : MonoBehaviour
{
OnExtensionAwake((T)baseMono);
}
}
/// <summary>
/// Extension methods for Unity's UnityEngine.Object types.
/// </summary>
public static class UnityObjectExtensions
{
/// <summary>
/// Determines whether a Unity object has been destroyed or is null.
/// </summary>
/// <param name="obj">The Unity object to check.</param>
/// <returns>
/// <c>true</c> if the object has been destroyed or is null; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// This method uses Unity's overloaded == operator which returns true for
/// destroyed objects even though the C# reference still exists.
/// </remarks>
public static bool IsDestroyedOrNull(this UnityEngine.Object obj)
{
return obj == null;
}
}
+1 -5
View File
@@ -58,11 +58,7 @@ internal static class ModInfo
/// </summary>
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"
];
/// <summary>
+2 -2
View File
@@ -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;
}
}
+2 -2
View File
@@ -354,7 +354,7 @@ public abstract class OptionItem
/// Gets the boxed value of this option.
/// </summary>
/// <returns>The option value as an object.</returns>
public abstract object GetBoxedValue();
public abstract object? GetBoxedValue();
/// <summary>
/// Represents a node in the option hierarchy tree for text formatting.
@@ -399,7 +399,7 @@ public abstract class OptionItem<T> : OptionItem
/// Gets the boxed value of this option.
/// </summary>
/// <returns>The option value as an object.</returns>
public override sealed object GetBoxedValue()
public override sealed object? GetBoxedValue()
{
return GetValue();
}
+10 -1
View File
@@ -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;
+2 -2
View File
@@ -57,7 +57,7 @@ internal sealed class MeetingInfoDisplay : PlayerInfoDisplay
var IdNumber = LevelDisplay.transform.Find("LevelNumber");
IdLabel.gameObject.DestroyTextTranslators();
IdLabel.GetComponent<TextMeshPro>().text = "ID";
IdNumber.GetComponent<TextMeshPro>().text = pva.TargetPlayerId.ToString();
IdNumber.GetComponent<TextMeshPro>().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
/// <returns>Disconnect reason text.</returns>
private string GetDisconnectText()
{
var playerData = GameData.Instance.GetPlayerById(_pva.TargetPlayerId);
var playerData = GameData.Instance.GetPlayerById(_pva.PlayerId);
if (playerData == null)
return string.Empty;
@@ -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;
}
}
@@ -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<string, int> 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<int, int>
{
{ 3, 1 },
{ 5, 2 },
{ 7, 3 }
};
foreach (var limit in impostorLimits)
{
if (NumPlayers <= limit.Key)
{
NumImpostors = Math.Min(NumImpostors, limit.Value);
break;
}
}
List<PlayerControl> Impostors = [];
List<PlayerControl> Crewmates = [];
// Track available roles and their counts
Dictionary<RoleTypes, int> ImpostorRoles = new() // Role, Amount
{
{ RoleTypes.Shapeshifter, 0 },
{ RoleTypes.Phantom, 0 },
{ RoleTypes.Viper, 0 }
};
Dictionary<RoleTypes, int> CrewmateRoles = new() // Role, Amount
{
{ RoleTypes.Engineer, 0 },
{ RoleTypes.Scientist, 0 },
{ RoleTypes.Tracker, 0 },
{ RoleTypes.Noisemaker, 0 },
{ RoleTypes.Detective, 0 }
};
List<RoleTypes> 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<PlayerControl> 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<NetworkedPlayerInfo> Impostors = [];
List<NetworkedPlayerInfo> Crewmates = [];
List<NetworkedPlayerInfo> 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<PlayerControl> 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<RoleTypes, int> GhostRoles = new() // Role, Amount
{
{ RoleTypes.GuardianAngel, 0 },
};
List<RoleTypes> 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
}
}
}
+18
View File
@@ -21,6 +21,24 @@ internal static class ChatPatch
internal const string COMMAND_POSTFIX_ID = "<size=0%>IsCommand</size>";
internal static bool IsChatVisible
{
get
{
if (BAUConfigs.ChatInGameplay.Value)
{
return true;
}
if (!GameState.IsInGamePlay)
{
return true;
}
return !PlayerControl.LocalPlayer.IsAlive() || GameState.IsMeeting || GameState.IsExilling;
}
}
/// <summary>
/// Removes all chat bubbles from the chat.
/// </summary>
+1 -1
View File
@@ -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<MeetingInfoDisplay>().Init(target, pva);
+2 -1
View File
@@ -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" },