diff --git a/src/BAUPlugin.cs b/src/BAUPlugin.cs
index befe47c..464746d 100644
--- a/src/BAUPlugin.cs
+++ b/src/BAUPlugin.cs
@@ -11,6 +11,7 @@ using BetterAmongUs.Modules;
using BetterAmongUs.Modules.OptionItems;
using BetterAmongUs.Modules.Support;
using BetterAmongUs.Network;
+using BetterAmongUs.Patches.Client;
using BetterAmongUs.Patches.Gameplay.UI.Settings;
using HarmonyLib;
using Il2CppInterop.Runtime.Injection;
@@ -302,6 +303,8 @@ internal class BAUPlugin : BasePlugin
UnlockFPS, ShowFPS, CommandPrefix,
FavoriteColor, SettingsPreset
]);
+
+ OptionsMenuBehaviourPatch.UpdateFrameRate();
}
///
diff --git a/src/Data/Json/AbstractJsonFile.cs b/src/Data/Json/AbstractJsonFile.cs
index 3021772..70802ab 100644
--- a/src/Data/Json/AbstractJsonFile.cs
+++ b/src/Data/Json/AbstractJsonFile.cs
@@ -35,7 +35,7 @@ internal abstract class AbstractJsonFile
if (_hasInit) return;
_hasInit = true;
- if (!CheckFile())
+ if (!IsFileValid())
{
Save();
return;
@@ -106,15 +106,6 @@ internal abstract class AbstractJsonFile
}
}
- ///
- /// Reads the content from the JSON file.
- ///
- /// The content of the JSON file.
- protected virtual string ReadFromFile()
- {
- return File.ReadAllText(FilePath);
- }
-
///
/// Saves the current instance's data to the JSON file.
///
@@ -136,19 +127,10 @@ internal abstract class AbstractJsonFile
}
///
- /// Writes the JSON string to the file.
+ /// Determines whether the file specified by the current FilePath exists and contains valid, non-empty JSON content.
///
- /// The JSON string to write.
- protected virtual void WriteToFile(string json)
- {
- File.WriteAllText(FilePath, json);
- }
-
- ///
- /// Checks if the JSON file exists and contains valid data.
- ///
- /// True if the file exists and contains valid JSON data, false otherwise.
- private bool CheckFile()
+ /// true if the file exists, is not empty, and contains valid, non-empty JSON content; otherwise, false.
+ private bool IsFileValid()
{
var directory = Path.GetDirectoryName(FilePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
@@ -163,26 +145,23 @@ internal abstract class AbstractJsonFile
var content = TryReadFromFile();
if (string.IsNullOrEmpty(content.Trim())) return false;
- var jsonElement = JsonSerializer.Deserialize(content);
-
- var fileInfo = new FileInfo(FilePath);
- if (fileInfo.Length == 0)
- {
- return false;
- }
try
{
+ var jsonElement = JsonSerializer.Deserialize(content);
+
+ var fileInfo = new FileInfo(FilePath);
+ if (fileInfo.Length == 0)
+ {
+ return false;
+ }
+
if (jsonElement.ValueKind == JsonValueKind.Object && !jsonElement.EnumerateObject().Any() ||
jsonElement.ValueKind == JsonValueKind.Array && !jsonElement.EnumerateArray().Any())
{
return false;
}
}
- catch (JsonException)
- {
- return false;
- }
catch (Exception)
{
return false;
@@ -190,4 +169,22 @@ internal abstract class AbstractJsonFile
return true;
}
+
+ ///
+ /// Reads the content from the JSON file.
+ ///
+ /// The content of the JSON file.
+ protected virtual string ReadFromFile()
+ {
+ return File.ReadAllText(FilePath);
+ }
+
+ ///
+ /// Writes the JSON string to the file.
+ ///
+ /// The JSON string to write.
+ protected virtual void WriteToFile(string json)
+ {
+ File.WriteAllText(FilePath, json);
+ }
}
\ No newline at end of file
diff --git a/src/Helpers/InnerNetClientHelper.cs b/src/Helpers/InnerNetClientHelper.cs
index 8ff3e1e..ddfafa1 100644
--- a/src/Helpers/InnerNetClientHelper.cs
+++ b/src/Helpers/InnerNetClientHelper.cs
@@ -1,7 +1,6 @@
using AmongUs.InnerNet.GameDataMessages;
using Hazel;
using InnerNet;
-using System.Globalization;
namespace BetterAmongUs.Helpers;
@@ -38,255 +37,6 @@ internal static class InnerNetClientHelper
}
}
- ///
- /// Writes an array of boolean values to a MessageWriter in a packed format to save space.
- /// Each byte stores up to 8 boolean values as bits.
- ///
- /// The MessageWriter to write to.
- /// The boolean values to write.
- internal static void WriteBooleans(this MessageWriter writer, IEnumerable boolsEnumerable)
- {
- bool[] bools = [.. boolsEnumerable];
-
- writer.Write(bools.Length);
-
- byte currentByte = 0;
- int bitIndex = 0;
-
- foreach (bool b in bools)
- {
- if (b) currentByte |= (byte)(1 << bitIndex);
-
- bitIndex++;
-
- if (bitIndex == 8)
- {
- writer.Write(currentByte);
- currentByte = 0;
- bitIndex = 0;
- }
- }
-
- if (bitIndex > 0) writer.Write(currentByte);
- }
-
- ///
- /// Reads an array of boolean values from a MessageReader that were previously packed using WriteBooleans.
- ///
- /// The MessageReader to read from.
- /// An array of boolean values.
- internal static bool[] ReadBooleans(this MessageReader reader)
- {
- int length = reader.ReadInt32();
- bool[] bools = new bool[length];
-
- int bitIndex = 0;
- byte currentByte = 0;
-
- for (int i = 0; i < length; i++)
- {
-
- if (bitIndex == 0) currentByte = reader.ReadByte();
-
- bools[i] = (currentByte & 1 << bitIndex) != 0;
-
- bitIndex++;
-
- if (bitIndex == 8) bitIndex = 0;
- }
-
- return bools;
- }
-
- ///
- /// Writes an array of bytes to a MessageWriter in a packed format, combining two bytes into one to save space.
- ///
- /// The MessageWriter to write to.
- /// The byte values to write.
- internal static void WriteBytes(this MessageWriter writer, IEnumerable bytesEnumerable)
- {
- byte[] bytes = bytesEnumerable.ToArray();
-
- writer.Write(bytes.Length);
- writer.Write(bytes);
- }
-
- ///
- /// Reads an array of bytes from a MessageReader that were previously packed using WritePackedBytes.
- ///
- /// The MessageReader to read from.
- /// An array of bytes.
- internal static byte[] ReadBytes(this MessageReader reader)
- {
- int count = reader.ReadInt32();
- var bytes = reader.ReadBytes(count);
- return [.. bytes];
- }
-
- ///
- /// Writes an array of bytes to a MessageWriter in a packed format, combining two bytes into one to save space.
- ///
- /// The MessageWriter to write to.
- /// The byte values to write.
- internal static void WritePackedBytes(this MessageWriter writer, IEnumerable bytesEnumerable)
- {
- byte[] bytes = bytesEnumerable.ToArray();
-
- writer.Write((byte)bytes.Length);
-
- for (int i = 0; i < bytes.Length; i += 2)
- {
- byte packedBytes;
- if (i + 1 < bytes.Length) packedBytes = (byte)((bytes[i] & 0x0F) << 4 | bytes[i + 1] & 0x0F);
- else packedBytes = (byte)((bytes[i] & 0x0F) << 4);
- writer.Write(packedBytes);
- }
- }
-
- ///
- /// Reads an array of bytes from a MessageReader that were previously packed using WritePackedBytes.
- ///
- /// The MessageReader to read from.
- /// An array of bytes.
- internal static byte[] ReadPackedBytes(this MessageReader reader)
- {
- int count = reader.ReadByte();
- List bytes = [];
-
- for (int i = 0; i < (count + 1) / 2; i++)
- {
- byte packedBytes = reader.ReadByte();
- bytes.Add((byte)(packedBytes >> 4 & 0x0F));
-
- if (bytes.Count < count) bytes.Add((byte)(packedBytes & 0x0F));
- }
-
- return bytes.ToArray();
- }
-
- ///
- /// Writes a floating-point value to a MessageWriter in a packed format to save space.
- /// Handles negative values, integers, and decimal numbers efficiently.
- ///
- /// The MessageWriter to write to.
- /// The float value to write.
- internal static void WritePacked(this MessageWriter writer, float value)
- {
- bool isNegative = value < 0;
- float absValue = Math.Abs(value);
-
- // Check if it's a solid number (integer)
- bool isSolidNumber = absValue == (uint)absValue && absValue <= uint.MaxValue;
-
- // Track the number of decimal places dynamically
- int decimalPlaces = 0;
- int scaleFactor = 1;
- bool needsFallback = false; // Flag to determine if we need to send the value as a regular float
-
- // Check if the value is not a solid number
- if (!isSolidNumber)
- {
- // Convert to string to count decimals (avoiding floating point precision issues)
- string absValueStr = absValue.ToString("G", CultureInfo.InvariantCulture);
- if (absValueStr.Contains('.'))
- {
- decimalPlaces = absValueStr.Split('.')[1].Length; // Count decimals after the period
- scaleFactor = (int)Math.Pow(10, decimalPlaces); // Set scale factor dynamically
- }
- }
-
- // If the value can't be packed efficiently, we need a fallback
- if (!isSolidNumber || absValue * scaleFactor > uint.MaxValue)
- {
- needsFallback = true; // Flag for fallback as a regular float
- }
-
- // Pack the flags into a list of booleans
- var flags = new List
- {
- isNegative,
- isSolidNumber,
- decimalPlaces > 0,
- needsFallback
- };
-
- // Write the flags using WriteBooleans
- writer.WriteBooleans(flags);
-
- // Write the decimal places as a byte (0-3 for simplicity)
- writer.Write((byte)decimalPlaces);
-
- // If it's a solid number, pack as uint
- if (isSolidNumber && !needsFallback)
- {
- writer.Write((uint)absValue);
- }
-
- // If it's a decimal number, scale and pack it as uint (but check if it can fit)
- else if (decimalPlaces > 0 && !needsFallback)
- {
- // Check if the scaled value fits into a uint
- if (absValue * scaleFactor <= uint.MaxValue)
- {
- uint scaledValue = (uint)(absValue * scaleFactor); // Dynamically scale the value
- writer.Write(scaledValue);
- }
- else
- {
- // If it can't fit into a uint, send as regular float
- writer.Write(value);
- }
- }
- else
- {
- // Send as regular float if it requires fallback
- writer.Write(value);
- }
- }
-
- ///
- /// Reads a floating-point value from a MessageReader that was previously packed using WritePacked.
- /// Handles negative values, integers, and decimal numbers efficiently.
- ///
- /// The MessageReader to read from.
- /// The unpacked float value.
- internal static float ReadPackedSingle(this MessageReader reader)
- {
- var flags = reader.ReadBooleans();
- bool isNegative = flags[0];
- bool isSolidNumber = flags[1];
- bool hasDecimalPart = flags[2];
- bool needsFallback = flags[3];
-
- byte decimalPlaces = reader.ReadByte(); // Read the decimal places count
-
- // If the value was packed as a regular float, just read it (don't reapply the sign)
- if (needsFallback)
- {
- return reader.ReadSingle(); // The sign is already handled in the packed value
- }
-
- // Handle solid number (uint) case
- if (isSolidNumber)
- {
- uint packedValue = reader.ReadUInt32();
- return isNegative ? -(float)packedValue : packedValue;
- }
- // Handle decimal number (scaled uint)
- else if (hasDecimalPart)
- {
- uint scaledValue = reader.ReadUInt32();
- float scaleFactor = (float)Math.Pow(10, decimalPlaces); // Use the stored decimal places count
- return isNegative ? -(scaledValue / scaleFactor) : scaledValue / scaleFactor;
- }
- else
- {
- // For other cases where it's a normal float
- float value = reader.ReadSingle();
- return isNegative ? -value : value;
- }
- }
-
///
/// Writes a player's ID to a MessageWriter, using 255 for null players.
///
@@ -343,6 +93,31 @@ internal static class InnerNetClientHelper
/// The Vent or null if not found.
internal static Vent? ReadVentId(this MessageReader reader) => BAUPlugin.AllVents.FirstOrDefault(vent => vent.Id == reader.ReadInt32());
+ ///
+ /// Writes an array of bytes to a MessageWriter in a packed format, combining two bytes into one to save space.
+ ///
+ /// The MessageWriter to write to.
+ /// The byte values to write.
+ internal static void WriteBytes(this MessageWriter writer, IEnumerable bytesEnumerable)
+ {
+ byte[] bytes = bytesEnumerable.ToArray();
+
+ writer.Write(bytes.Length);
+ writer.Write(bytes);
+ }
+
+ ///
+ /// Reads an array of bytes from a MessageReader that were previously packed using WritePackedBytes.
+ ///
+ /// The MessageReader to read from.
+ /// An array of bytes.
+ internal static byte[] ReadBytes(this MessageReader reader)
+ {
+ int count = reader.ReadInt32();
+ var bytes = reader.ReadBytes(count);
+ return [.. bytes];
+ }
+
///
/// Converts a MessageWriter to a MessageReader.
///
@@ -403,90 +178,40 @@ internal static class InnerNetClientHelper
}
///
- /// Writes multiple MessageReaders to a MessageWriter.
+ /// Sends an RPC message immediately to all clients that match the specified target criteria, bypassing standard
+ /// synchronization mechanisms.
///
- /// The MessageWriter to write to.
- /// The MessageReaders to write.
- /// Whether to clear the writer before writing.
- /// The MessageWriter for method chaining.
- internal static MessageWriter WriteReaders(this MessageWriter writer, IEnumerable readers, bool clear = false)
+ /// The InnerNetClient instance used to send the RPC messages.
+ /// The network ID of the player on whose behalf the RPC is sent.
+ /// The identifier for the RPC call to be executed.
+ /// The send option that determines how the message is delivered (e.g., reliability, ordering).
+ /// A function that selects which clients should receive the RPC message. The RPC is sent only to clients for which
+ /// this function returns .
+ /// An action that writes the RPC payload to the provided MessageWriter.
+ internal static void SendRpcImmediatelyDesync(this InnerNetClient client, uint playerNetId, RpcCalls callId, SendOption option, Func targetClients, Action writeTo)
{
- if (clear) writer.Clear(writer.SendOption);
-
- foreach (MessageReader reader in readers)
+ foreach (var allClients in AmongUsClient.Instance.allClients)
{
- writer.StartMessage(reader.Tag);
- writer.Write(reader.ReadBytes(reader.Length));
- writer.EndMessage();
- }
+ if (!targetClients(allClients)) continue;
- return writer;
+ client.SendRpcImmediately(playerNetId, callId, option, writeTo, allClients.Id);
+ }
}
///
- /// Creates a copy of a MessageWriter.
+ /// Sends a remote procedure call (RPC) message immediately to the specified player, using the provided message
+ /// writer action and send option.
///
- /// The MessageWriter to copy.
- /// A new MessageWriter with the same content.
- internal static MessageWriter Copy(this MessageWriter writer)
+ /// The InnerNetClient instance used to send the RPC message.
+ /// The network identifier of the target player to whom the RPC is sent.
+ /// The identifier of the RPC call to invoke.
+ /// The send option that determines how the message is delivered (such as reliability or channel).
+ /// An action that writes the RPC message content to the provided MessageWriter.
+ /// The client identifier of the specific target client. Use -1 to broadcast to all clients.
+ internal static void SendRpcImmediately(this InnerNetClient client, uint playerNetId, RpcCalls callId, SendOption option, Action writeTo, int targetClientId = -1)
{
- var newWriter = MessageWriter.Get(writer.SendOption);
- newWriter.Write(writer.ToByteArray(false));
- return newWriter;
- }
-
- ///
- /// Starts the RPC desynchronization process for the given player, call ID, and send option.
- ///
- /// The InnerNetClient instance.
- /// The network ID of the player.
- /// The RPC call ID.
- /// The send option for the RPC.
- /// The client ID to ignore. Default is -1, which means no client is ignored.
- /// Optional function to filter which clients receive the RPC.
- /// A list of MessageWriter instances for the RPC calls.
- ///
- ///
- /// List<MessageWriter> messageWriter = AmongUsClient.Instance.StartRpcDesync(PlayerNetId, (byte)RpcCalls, SendOption, ClientId);
- /// messageWriter.ForEach(mW => mW.Write("RPC TEST"));
- /// AmongUsClient.Instance.FinishRpcDesync(messageWriter);
- ///
- ///
- internal static List StartRpcDesync(this InnerNetClient client, uint playerNetId, byte callId, SendOption option, int ignoreClientId = -1, Func? clientCheck = null)
- {
- List messageWriters = [];
-
- if (ignoreClientId < 0)
- {
- messageWriters.Add(client.StartRpcImmediately(playerNetId, callId, option, -1));
- }
- else
- {
- foreach (var allClients in AmongUsClient.Instance.allClients.WhereIl2Cpp(c => c.Id != ignoreClientId))
- {
- if (clientCheck == null || clientCheck.Invoke(allClients))
- {
- messageWriters.Add(client.StartRpcImmediately(playerNetId, callId, option, allClients.Id));
- }
- }
- }
-
- return messageWriters;
- }
-
- ///
- /// Completes and sends the RPC desynchronization messages.
- ///
- /// The InnerNetClient instance.
- /// The list of MessageWriters to finish and send.
- internal static void FinishRpcDesync(this InnerNetClient client, List messageWriters)
- {
- foreach (var msg in messageWriters)
- {
- msg.EndMessage();
- msg.EndMessage();
- client.SendOrDisconnect(msg);
- msg.Recycle();
- }
+ var writer = client.StartRpcImmediately(playerNetId, (byte)callId, option, targetClientId);
+ writeTo(writer);
+ client.FinishRpcImmediately(writer);
}
}
\ No newline at end of file
diff --git a/src/Helpers/PlayerControlHelper.cs b/src/Helpers/PlayerControlHelper.cs
index 669e81d..c4c9a27 100644
--- a/src/Helpers/PlayerControlHelper.cs
+++ b/src/Helpers/PlayerControlHelper.cs
@@ -99,7 +99,7 @@ static class PlayerControlHelper
{
var Ban = ban || forceBan;
- if (!GameState.IsHost || player.IsLocalPlayer() || !player.DataIsCollected() && !bypassDataCheck || player.IsHost() || player.isDummy)
+ if (!GameState.IsHost || player.IsLocalPlayer() || (!player.DataIsCollected() && !bypassDataCheck) || player.IsHost() || player.isDummy)
{
return;
}
@@ -311,7 +311,7 @@ static class PlayerControlHelper
///
/// The player data to check.
/// True if the player is on the impostor team.
- internal static bool IsImpostorTeam(this NetworkedPlayerInfo data) => data?.RoleType is RoleTypes.Impostor or RoleTypes.ImpostorGhost or RoleTypes.Shapeshifter or RoleTypes.Phantom or RoleTypes.Viper;
+ internal static bool IsImpostorTeam(this NetworkedPlayerInfo data) => data?.RoleType.GetBehaviourPrefab().IsImpostor == true;
///
/// Checks if a player is an impostor teammate of the local player.
diff --git a/src/Helpers/RoleHelper.cs b/src/Helpers/RoleHelper.cs
index 6ea6d4f..133baa4 100644
--- a/src/Helpers/RoleHelper.cs
+++ b/src/Helpers/RoleHelper.cs
@@ -7,6 +7,9 @@ namespace BetterAmongUs.Helpers;
///
internal static class RoleHelper
{
+ ///
+ /// Provides a lazily initialized lookup dictionary that maps each role type to its corresponding role behavior.
+ ///
private static readonly Lazy> roleLookup =
new(() =>
{
@@ -34,9 +37,17 @@ internal static class RoleHelper
///
/// The role type to check.
/// True if the role is part of the impostor team, false otherwise.
- internal static bool IsImpostorRole(RoleTypes role) =>
+ internal static bool IsImpostorRole(this RoleTypes role) =>
role.GetBehaviourPrefab().TeamType is RoleTeamTypes.Impostor;
+ ///
+ /// Determines whether the specified role is considered a ghost role.
+ ///
+ /// The role type to check.
+ /// true if the role is classified as a ghost role; otherwise, false.
+ internal static bool IsGhostRole(this RoleTypes role) =>
+ RoleManager.IsGhostRole(role);
+
///
/// Gets the display name of a role type.
///
diff --git a/src/Managers/NetworkManager.cs b/src/Managers/NetworkManager.cs
index 03d9d5e..35042a5 100644
--- a/src/Managers/NetworkManager.cs
+++ b/src/Managers/NetworkManager.cs
@@ -432,6 +432,13 @@ internal static class NetworkManager
return false;
}
}
+ else if (netObj is CustomNetworkTransform networkTransform)
+ {
+ if (!PlayerRpc(networkTransform.myPlayer, callId, reader))
+ {
+ return false;
+ }
+ }
return true;
}
diff --git a/src/ModInfo.cs b/src/ModInfo.cs
index 68837ef..02d94e6 100644
--- a/src/ModInfo.cs
+++ b/src/ModInfo.cs
@@ -31,7 +31,7 @@ internal static class ModInfo
///
/// The hotfix number for hotfix releases.
///
- internal const string HOTFIX_NUM = "2";
+ internal const string HOTFIX_NUM = "3";
///
/// Indicates whether this is a hotfix release.
diff --git a/src/Modules/AntiCheat/BetterAntiCheat.cs b/src/Modules/AntiCheat/BetterAntiCheat.cs
index 9b8bac2..c6b0c43 100644
--- a/src/Modules/AntiCheat/BetterAntiCheat.cs
+++ b/src/Modules/AntiCheat/BetterAntiCheat.cs
@@ -7,9 +7,7 @@ using BetterAmongUs.Modules.Support;
using BetterAmongUs.Mono;
using BetterAmongUs.Network;
using BetterAmongUs.Patches.Gameplay.UI.Settings;
-using HarmonyLib;
using Hazel;
-using InnerNet;
namespace BetterAmongUs.Modules.AntiCheat;
@@ -60,73 +58,6 @@ internal static class BetterAntiCheat
}
}
- ///
- /// Harmony patch for PlatformSpecificData to detect platform spoofing.
- ///
- [HarmonyPatch(typeof(PlatformSpecificData))]
- class PlatformSpecificDataPatch
- {
- ///
- /// Postfix patch to validate platform data after deserialization.
- ///
- [HarmonyPatch(nameof(PlatformSpecificData.Deserialize))]
- [HarmonyPostfix]
- internal static void Deserialize_Postfix(PlatformSpecificData __instance)
- {
- if (!BAUPlugin.AntiCheat.Value || BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_Anticheat) || !GameState.IsVanillaServer) return;
-
- if (GameState.IsLobby)
- {
- try
- {
- LateTask.Schedule(() =>
- {
- var player = BAUPlugin.AllPlayerControls.FirstOrDefault(pc => pc.GetClient().PlatformData == __instance);
-
- if (player != null && __instance?.Platform != null)
- {
- if (__instance.Platform is Platforms.StandaloneWin10 or Platforms.Xbox)
- {
- if (__instance.XboxPlatformId.ToString().Length is < 10 or > 16)
- {
- player.ReportPlayer(ReportReasons.Cheating_Hacking);
- BetterNotificationManager.NotifyCheat(player,
- Translator.GetString("AntiCheat.Reason.PlatformSpoofer"),
- Translator.GetString("AntiCheat.HasBeenDetectedWithCheat")
- );
- Logger_.LogCheat($"{player.BetterData().RealName} {Translator.GetString("AntiCheat.PlatformSpoofer")}: {__instance.XboxPlatformId}");
- }
- }
-
- if (__instance.Platform is Platforms.Playstation)
- {
- if (__instance.PsnPlatformId.ToString().Length is < 14 or > 20)
- {
- player.ReportPlayer(ReportReasons.Cheating_Hacking);
- BetterNotificationManager.NotifyCheat(player,
- Translator.GetString("AntiCheat.Reason.PlatformSpoofer"),
- Translator.GetString("AntiCheat.HasBeenDetectedWithCheat")
- );
- Logger_.LogCheat($"{player.BetterData().RealName} {Translator.GetString("AntiCheat.PlatformSpoofer")}: {__instance.PsnPlatformId}");
- }
- }
-
- if (__instance.Platform is Platforms.Unknown || !Enum.IsDefined(__instance.Platform))
- {
- BetterNotificationManager.NotifyCheat(player,
- Translator.GetString("AntiCheat.Reason.PlatformSpoofer"),
- Translator.GetString("AntiCheat.HasBeenDetectedWithCheat")
- );
- }
- }
-
- }, 3.5f, shouldLog: false);
- }
- catch { }
- }
- }
- }
-
///
/// Handles RPCs before anti-cheat detection checks.
///
diff --git a/src/Modules/AntiCheat/RPCHandlers/SetLevelHandler.cs b/src/Modules/AntiCheat/RPCHandlers/SetLevelHandler.cs
index 5665102..e8e7344 100644
--- a/src/Modules/AntiCheat/RPCHandlers/SetLevelHandler.cs
+++ b/src/Modules/AntiCheat/RPCHandlers/SetLevelHandler.cs
@@ -12,16 +12,6 @@ internal sealed class SetLevelHandler : RPCHandler
{
internal override byte CallId => (byte)RpcCalls.SetLevel;
- internal override void Handle(PlayerControl? sender, MessageReader reader)
- {
- uint level = reader.ReadPackedUInt32() + 1;
-
- if (level < BetterGameSettings.KickLevelBelow.GetInt())
- {
- sender.Kick(setReasonInfo: $" is level {level}, level must be equal or above {BetterGameSettings.KickLevelBelow.GetInt()} to join");
- }
- }
-
internal override bool HandleAntiCheatCancel(PlayerControl? sender, MessageReader reader)
{
if (sender.DataIsCollected() == true && sender.BetterData().AntiCheatInfo.HasSetLevel && !GameState.IsLocalGame && GameState.IsVanillaServer)
diff --git a/src/Modules/Support/BAUModdedSupportFlags.cs b/src/Modules/Support/BAUModdedSupportFlags.cs
index 6f40025..e517c4c 100644
--- a/src/Modules/Support/BAUModdedSupportFlags.cs
+++ b/src/Modules/Support/BAUModdedSupportFlags.cs
@@ -163,6 +163,18 @@ public static class BAUModdedSupportFlags
///
public static string Disable_EndGameSummary = "gameplay.disable.endgamesummary";
+ ///
+ /// Disables custom icon on mini map.
+ /// When enabled by another mod, custom icons on the mini map will not be generated.
+ ///
+ public static string Disable_MinimapIcons = "gameplay.disable.minimapicons";
+
+ ///
+ /// Disables custom vent color highlights.
+ /// When enabled by another mod, vent colors when highlighted will not use vent groups.
+ ///
+ public static string Disable_VentColorGroups = "gameplay.disable.ventcolorgroups";
+
private static readonly HashSet _flags = [];
private static bool _initialized = false;
diff --git a/src/Modules/VentGroups.cs b/src/Modules/VentGroups.cs
new file mode 100644
index 0000000..2a844c3
--- /dev/null
+++ b/src/Modules/VentGroups.cs
@@ -0,0 +1,119 @@
+using UnityEngine;
+
+namespace BetterAmongUs.Modules;
+
+///
+/// Provides functionality for grouping vents and assigning colors to vent groups.
+///
+internal static class VentGroups
+{
+ ///
+ /// Dictionary mapping each vent ID to the lowest ID in its group.
+ ///
+ private static readonly Dictionary ventToLowestId = [];
+
+ ///
+ /// Dictionary mapping group IDs (lowest vent ID in the group) to their assigned colors.
+ ///
+ private static readonly Dictionary groupColors = [];
+
+ ///
+ /// Calculates all vent groups by performing a breadth-first search through connected vents.
+ ///
+ /// Array of all vents in the scene to process.
+ ///
+ /// Groups vents that are connected via Left, Right, or Center references.
+ /// Each group is assigned a color, and the lowest vent ID in the group becomes the group identifier.
+ ///
+ internal static void CalculateAllVentGroups(Vent[] vents)
+ {
+ ventToLowestId.Clear();
+ groupColors.Clear();
+
+ HashSet unprocessedVents = [.. vents];
+
+ HashSet groupLowestIds = [];
+ Dictionary> groups = [];
+
+ while (unprocessedVents.Count > 0)
+ {
+ Vent startVent = unprocessedVents.First();
+
+ HashSet group = [];
+ Queue toVisit = new();
+ int lowestId = startVent.Id;
+
+ toVisit.Enqueue(startVent);
+
+ while (toVisit.Count > 0)
+ {
+ Vent current = toVisit.Dequeue();
+ if (group.Contains(current)) continue;
+
+ group.Add(current);
+ unprocessedVents.Remove(current);
+
+ if (current.Id < lowestId)
+ {
+ lowestId = current.Id;
+ }
+
+ Vent[] connections = { current.Left, current.Right, current.Center };
+ foreach (var connection in connections)
+ {
+ if (connection != null && !group.Contains(connection) && !toVisit.Contains(connection))
+ {
+ toVisit.Enqueue(connection);
+ }
+ }
+ }
+
+ groupLowestIds.Add(lowestId);
+ groups[lowestId] = [.. group];
+ }
+
+ Color[] colorGroups =
+ [
+ Color.red,
+ Color.green,
+ Color.blue,
+ Color.gray,
+ Color.magenta,
+ Color.cyan,
+ Color.white
+ ];
+
+ for (int i = 0; i < groupLowestIds.Count; i++)
+ {
+ int lowestId = groupLowestIds.ElementAt(i);
+ int colorIndex = i % colorGroups.Length;
+ groupColors[lowestId] = colorGroups[colorIndex] + (new Color(1f, 1f, 1f, 0f) * 0.2f);
+ }
+
+ foreach (var kvp in groups)
+ {
+ int lowestId = kvp.Key;
+ foreach (var vent in kvp.Value)
+ {
+ ventToLowestId[vent.Id] = lowestId;
+ }
+ }
+ }
+
+ ///
+ /// Gets the group color for a specific vent.
+ ///
+ /// The vent to get the group color for.
+ ///
+ /// The color assigned to the vent's group, or if the vent hasn't been processed.
+ ///
+ internal static Color GetVentGroupColor(Vent vent)
+ {
+ if (ventToLowestId.TryGetValue(vent.Id, out int lowestId))
+ {
+ return groupColors[lowestId];
+ }
+
+ return Color.black;
+ }
+}
\ No newline at end of file
diff --git a/src/Mono/AnimatedMapIcon.cs b/src/Mono/AnimatedMapIcon.cs
new file mode 100644
index 0000000..65d3f36
--- /dev/null
+++ b/src/Mono/AnimatedMapIcon.cs
@@ -0,0 +1,56 @@
+using UnityEngine;
+
+namespace BetterAmongUs.Mono;
+
+///
+/// Animates a map icon by smoothly scaling it up and down when conditions are met.
+///
+internal class AnimatedMapIcon : MonoBehaviour
+{
+ ///
+ /// Event that determines whether the icon should animate.
+ /// Returns true if animation should occur, false otherwise.
+ ///
+ internal event Func? ShouldAnimate;
+
+ private Vector3 originalScale;
+
+ ///
+ /// The multiplier applied to the original scale when fully animated.
+ ///
+ private readonly float scaleMultiplier = 1.2f;
+
+ ///
+ /// The speed of the animation loop.
+ ///
+ private readonly float animationSpeed = 2f;
+
+ ///
+ /// Initializes the component by storing the original scale of the icon.
+ ///
+ private void Awake()
+ {
+ originalScale = transform.localScale;
+ }
+
+ ///
+ /// Updates the icon scale each frame based on the animation state.
+ /// When ShouldAnimate returns true, creates a smooth ping-pong scaling effect.
+ /// When ShouldAnimate returns false, resets to the original scale.
+ ///
+ private void Update()
+ {
+ if (ShouldAnimate?.Invoke() == true)
+ {
+ float pingPong = Mathf.PingPong(Time.time * animationSpeed, 1f);
+ float t = Mathf.SmoothStep(0f, 1f, pingPong);
+ Vector3 targetScale = originalScale * (1f + (scaleMultiplier - 1f) * t);
+
+ transform.localScale = targetScale;
+ }
+ else
+ {
+ transform.localScale = originalScale;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Network/RPC.cs b/src/Network/RPC.cs
index 879866f..fc27b30 100644
--- a/src/Network/RPC.cs
+++ b/src/Network/RPC.cs
@@ -29,15 +29,15 @@ internal static class RPC
/// The specific client ID to target, or -1 to broadcast to all clients.
internal static void SendCustomRpcPacked(CustomRPC customRPC, Action action, int targetClientId = -1)
{
- var writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)RpcCalls.SetNamePlateStr, SendOption.Reliable, targetClientId);
+ AmongUsClient.Instance.SendRpcImmediately(PlayerControl.LocalPlayer.NetId, RpcCalls.SetNamePlateStr, SendOption.Reliable, writer =>
+ {
+ writer.Write(DataManager.Player.Customization.NamePlate);
+ writer.Write(PlayerControl.LocalPlayer.GetNextRpcSequenceId(RpcCalls.SetNamePlateStr));
- writer.Write(DataManager.Player.Customization.NamePlate);
- writer.Write(PlayerControl.LocalPlayer.GetNextRpcSequenceId(RpcCalls.SetNamePlateStr));
-
- writer.Write(CUSTOM_RPC_FLAG); // Flag to check if its a rpc packed into SetNamePlateStr
- writer.Write((byte)customRPC);
- action(writer);
- AmongUsClient.Instance.FinishRpcImmediately(writer);
+ writer.Write(CUSTOM_RPC_FLAG); // Flag to check if its a rpc packed into SetNamePlateStr
+ writer.Write((byte)customRPC);
+ action(writer);
+ }, targetClientId);
}
///
diff --git a/src/Patches/Client/OptionsMenuBehaviourPatch.cs b/src/Patches/Client/OptionsMenuBehaviourPatch.cs
index f084c92..d7e14a1 100644
--- a/src/Patches/Client/OptionsMenuBehaviourPatch.cs
+++ b/src/Patches/Client/OptionsMenuBehaviourPatch.cs
@@ -113,10 +113,10 @@ internal static class OptionsMenuBehaviourPatch
}
}
- private static void UpdateFrameRate()
+ internal static void UpdateFrameRate()
{
// Toggle between 60 FPS (default) and 165 FPS
- Application.targetFrameRate = BAUPlugin.UnlockFPS.Value ? 165 : 60;
+ Application.targetFrameRate = BAUPlugin.UnlockFPS.Value ? 999 : 60;
}
private static void OpenSaveData()
diff --git a/src/Patches/Client/SplashIntroPatch.cs b/src/Patches/Client/SplashIntroPatch.cs
index 3bd3034..d4a043f 100644
--- a/src/Patches/Client/SplashIntroPatch.cs
+++ b/src/Patches/Client/SplashIntroPatch.cs
@@ -7,10 +7,14 @@ namespace BetterAmongUs.Patches.Client;
[HarmonyPatch]
internal static class SplashIntroPatch
{
+ private const float MinIntroDuration = 3f;
+ private const float AudioDestroyTime = 1.8f;
+
internal static bool Skip = false;
internal static bool BetterIntro = false;
internal static bool IsReallyDoneLoading = false;
- private static GameObject? BetterLogo;
+
+ private static GameObject? _betterLogo;
[HarmonyPatch(typeof(SplashManager), nameof(SplashManager.Start))]
[HarmonyPrefix]
@@ -22,84 +26,112 @@ internal static class SplashIntroPatch
IsReallyDoneLoading = false;
// Hide black overlay by moving it out of view
- __instance.logoAnimFinish.transform.Find("BlackOverlay").transform.SetLocalY(100f);
+ __instance.logoAnimFinish.transform
+ .Find("BlackOverlay").transform
+ .SetLocalY(100f);
}
[HarmonyPatch(typeof(SplashManager), nameof(SplashManager.Update))]
[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)
+ HandleAudioDestruction(__instance);
+
+ if (TryHandleSkipClick(__instance))
+ {
+ Skip = true;
+ return false;
+ }
+
+ if (CanStartBetterIntro(__instance))
+ {
+ StartBetterIntro(__instance);
+ return false;
+ }
+
+ CheckIfDone(__instance);
+ return false;
+ }
+
+ private static void HandleAudioDestruction(SplashManager __instance)
+ {
+ if (!BetterIntro) return;
+
+ if (Time.time - __instance.startTime > AudioDestroyTime)
{
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))
- {
- Skip = true;
- return false;
- }
- }
+ private static bool TryHandleSkipClick(SplashManager __instance)
+ {
+ if (!Input.GetKeyDown(KeyCode.Mouse0) && !Input.GetKeyDown(KeyCode.Mouse1))
+ return false;
- // 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);
+ return CheckIfDone(__instance, 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);
+ private static bool CanStartBetterIntro(SplashManager __instance)
+ {
+ return __instance.doneLoadingRefdata &&
+ !__instance.startedSceneLoad &&
+ Time.time - __instance.startTime > __instance.minimumSecondsBeforeSceneChange;
+ }
- // Show black overlay
- __instance.logoAnimFinish.transform.Find("BlackOverlay").transform.SetLocalY(0f);
+ private static void StartBetterIntro(SplashManager __instance)
+ {
+ if (BetterIntro) return;
- BetterIntro = true;
- return false;
- }
+ // Start BAU custom intro sequence
+ __instance.startTime = Time.time;
+ __instance.logoAnimFinish.gameObject.SetActive(false);
+ __instance.logoAnimFinish.gameObject.SetActive(true);
- // Check if BAU intro has completed
- CheckIfDone(__instance);
- }
+ // Replace InnerSloth logo with BAU logo
+ ReplaceLogo(__instance);
- // Return false to prevent original Update from running (we handle everything)
- return false;
+ // Show black overlay
+ __instance.logoAnimFinish.transform
+ .Find("BlackOverlay").transform
+ .SetLocalY(0f);
+
+ BetterIntro = true;
+ }
+
+ private static void ReplaceLogo(SplashManager __instance)
+ {
+ var 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);
}
private static bool CheckIfDone(SplashManager __instance, bool isSkip = false)
{
- // 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 transition if intro played for minimum duration or user skipped
+ var introComplete = (Time.time - __instance.startTime > MinIntroDuration && BetterIntro) ||
+ (isSkip && BetterIntro);
- // Allow scene transition to proceed
- __instance.sceneChanger.AllowFinishLoadingScene();
- __instance.startedSceneLoad = true;
- __instance.loadingObject.SetActive(true);
- return true;
- }
+ if (!introComplete) return false;
- return false;
+ IsReallyDoneLoading = true;
+
+ // Allow scene transition to proceed
+ __instance.sceneChanger.AllowFinishLoadingScene();
+ __instance.startedSceneLoad = true;
+ __instance.loadingObject.SetActive(true);
+
+ return true;
}
}
\ No newline at end of file
diff --git a/src/Patches/Gameplay/Anticheat/CheckPlayerLevelPatch.cs b/src/Patches/Gameplay/Anticheat/CheckPlayerLevelPatch.cs
new file mode 100644
index 0000000..d1e33c8
--- /dev/null
+++ b/src/Patches/Gameplay/Anticheat/CheckPlayerLevelPatch.cs
@@ -0,0 +1,23 @@
+using BetterAmongUs.Helpers;
+using BetterAmongUs.Modules;
+using BetterAmongUs.Patches.Gameplay.UI.Settings;
+using HarmonyLib;
+
+namespace BetterAmongUs.Patches.Gameplay.Anticheat;
+
+[HarmonyPatch]
+internal class CheckPlayerLevelPatch
+{
+ [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))]
+ [HarmonyPostfix]
+ private static void PlayerControl_FixedUpdate_Postfix(PlayerControl __instance)
+ {
+ if (GameState.IsHost)
+ {
+ if (!__instance.IsLocalPlayer() && (__instance.Data.PlayerLevel < BetterGameSettings.KickLevelBelow.GetInt()))
+ {
+ __instance.Kick(setReasonInfo: $" is level {__instance.Data.PlayerLevel}, level must be equal or above {BetterGameSettings.KickLevelBelow.GetInt()} to join");
+ }
+ }
+ }
+}
diff --git a/src/Patches/Gameplay/Anticheat/PlatformSpoofPatch.cs b/src/Patches/Gameplay/Anticheat/PlatformSpoofPatch.cs
new file mode 100644
index 0000000..ab2dd95
--- /dev/null
+++ b/src/Patches/Gameplay/Anticheat/PlatformSpoofPatch.cs
@@ -0,0 +1,70 @@
+using BetterAmongUs.Helpers;
+using BetterAmongUs.Managers;
+using BetterAmongUs.Modules;
+using BetterAmongUs.Modules.Support;
+using BetterAmongUs.Mono;
+using HarmonyLib;
+using InnerNet;
+
+namespace BetterAmongUs.Patches.Gameplay.Anticheat;
+
+[HarmonyPatch]
+internal class PlatformSpoofPatch
+{
+ [HarmonyPatch(typeof(PlatformSpecificData), nameof(PlatformSpecificData.Deserialize))]
+ [HarmonyPostfix]
+ internal static void PlatformSpecificData_Deserialize_Postfix(PlatformSpecificData __instance)
+ {
+ if (!BAUPlugin.AntiCheat.Value || BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_Anticheat) || !GameState.IsVanillaServer) return;
+
+ if (GameState.IsLobby)
+ {
+ try
+ {
+ LateTask.Schedule(() =>
+ {
+ var player = BAUPlugin.AllPlayerControls.FirstOrDefault(pc => pc.GetClient().PlatformData == __instance);
+
+ if (player != null && __instance?.Platform != null)
+ {
+ if (__instance.Platform is Platforms.StandaloneWin10 or Platforms.Xbox)
+ {
+ if (__instance.XboxPlatformId.ToString().Length is < 10 or > 16)
+ {
+ player.ReportPlayer(ReportReasons.Cheating_Hacking);
+ BetterNotificationManager.NotifyCheat(player,
+ Translator.GetString("AntiCheat.Reason.PlatformSpoofer"),
+ Translator.GetString("AntiCheat.HasBeenDetectedWithCheat")
+ );
+ Logger_.LogCheat($"{player.BetterData().RealName} {Translator.GetString("AntiCheat.PlatformSpoofer")}: {__instance.XboxPlatformId}");
+ }
+ }
+
+ if (__instance.Platform is Platforms.Playstation)
+ {
+ if (__instance.PsnPlatformId.ToString().Length is < 14 or > 20)
+ {
+ player.ReportPlayer(ReportReasons.Cheating_Hacking);
+ BetterNotificationManager.NotifyCheat(player,
+ Translator.GetString("AntiCheat.Reason.PlatformSpoofer"),
+ Translator.GetString("AntiCheat.HasBeenDetectedWithCheat")
+ );
+ Logger_.LogCheat($"{player.BetterData().RealName} {Translator.GetString("AntiCheat.PlatformSpoofer")}: {__instance.PsnPlatformId}");
+ }
+ }
+
+ if (__instance.Platform is Platforms.Unknown || !Enum.IsDefined(__instance.Platform))
+ {
+ BetterNotificationManager.NotifyCheat(player,
+ Translator.GetString("AntiCheat.Reason.PlatformSpoofer"),
+ Translator.GetString("AntiCheat.HasBeenDetectedWithCheat")
+ );
+ }
+ }
+
+ }, 3.5f, shouldLog: false);
+ }
+ catch { }
+ }
+ }
+}
diff --git a/src/Patches/Gameplay/Systems/VoteBanSystemPatch.cs b/src/Patches/Gameplay/Anticheat/VoteBanSystemPatch.cs
similarity index 98%
rename from src/Patches/Gameplay/Systems/VoteBanSystemPatch.cs
rename to src/Patches/Gameplay/Anticheat/VoteBanSystemPatch.cs
index cdec99e..a83d1c8 100644
--- a/src/Patches/Gameplay/Systems/VoteBanSystemPatch.cs
+++ b/src/Patches/Gameplay/Anticheat/VoteBanSystemPatch.cs
@@ -4,7 +4,7 @@ using BetterAmongUs.Modules;
using BetterAmongUs.Modules.Support;
using HarmonyLib;
-namespace BetterAmongUs.Patches.Gameplay.Systems;
+namespace BetterAmongUs.Patches.Gameplay.Anticheat;
[HarmonyPatch]
internal static class VoteBanSystemPatch
diff --git a/src/Patches/Gameplay/Managers/RoleManagerPatch.cs b/src/Patches/Gameplay/Managers/RoleManagerPatch.cs
index 8215e5b..512c2ad 100644
--- a/src/Patches/Gameplay/Managers/RoleManagerPatch.cs
+++ b/src/Patches/Gameplay/Managers/RoleManagerPatch.cs
@@ -15,20 +15,23 @@ 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) =>
+ // Check if client is verified Better Among Us user
+ private static Func SendTo(PlayerControl target)
{
- return clientData?.BetterData()?.IsVerifiedBetterUser != true;
- };
+ return (clientData) =>
+ {
+ return clientData.Id != target.GetClientId() && clientData?.BetterData()?.IsVerifiedBetterUser != true;
+ };
+ }
[HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SetRole))]
[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 (roleType.IsGhostRole())
{
- if (!RoleManager.IsGhostRole(targetPlayer.Data.RoleType))
+ if (!targetPlayer.Data.RoleType.IsGhostRole())
{
targetPlayer.BetterData().RoleInfo.DeadDisplayRole = targetPlayer.Data.RoleType;
}
@@ -113,7 +116,7 @@ internal static class RoleManagerPatch
// Get role counts from game options
foreach (RoleTypes role in Roles)
{
- if (role.GetBehaviourPrefab().IsImpostor)
+ if (role.IsImpostorRole())
ImpostorRoles[role] = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role);
else
CrewmateRoles[role] = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role);
@@ -154,12 +157,13 @@ internal static class RoleManagerPatch
// Desync role to hide special role from non-BAU players
if (BetterGameSettings.DesyncRoles.GetBool())
{
- if (kvp.Key is not RoleTypes.Phantom or RoleTypes.Viper)
+ if (kvp.Key is not (RoleTypes.Phantom or RoleTypes.Viper))
{
- List messageWriter = AmongUsClient.Instance.StartRpcDesync(pc.NetId, (byte)RpcCalls.SetRole, SendOption.None, pc.GetClientId(), clientCheck);
- messageWriter.ForEach(mW => mW.Write((ushort)RoleTypes.Impostor));
- messageWriter.ForEach(mW => mW.Write(false));
- AmongUsClient.Instance.FinishRpcDesync(messageWriter);
+ AmongUsClient.Instance.SendRpcImmediatelyDesync(pc.NetId, RpcCalls.SetRole, SendOption.None, SendTo(pc), writer =>
+ {
+ writer.Write((ushort)RoleTypes.Impostor);
+ writer.Write(false);
+ });
}
}
@@ -195,10 +199,11 @@ internal static class RoleManagerPatch
{
if (kvp.Key is not RoleTypes.Noisemaker)
{
- List messageWriter = AmongUsClient.Instance.StartRpcDesync(pc.NetId, (byte)RpcCalls.SetRole, SendOption.None, pc.GetClientId(), clientCheck);
- messageWriter.ForEach(mW => mW.Write((ushort)RoleTypes.Crewmate));
- messageWriter.ForEach(mW => mW.Write(false));
- AmongUsClient.Instance.FinishRpcDesync(messageWriter);
+ AmongUsClient.Instance.SendRpcImmediatelyDesync(pc.NetId, RpcCalls.SetRole, SendOption.None, SendTo(pc), writer =>
+ {
+ writer.Write((ushort)RoleTypes.Crewmate);
+ writer.Write(false);
+ });
}
}
@@ -352,10 +357,11 @@ internal static class RoleManagerPatch
// 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);
- messageWriter.ForEach(mW => mW.Write((ushort)player.Data.Role.DefaultGhostRole));
- messageWriter.ForEach(mW => mW.Write(false));
- AmongUsClient.Instance.FinishRpcDesync(messageWriter);
+ AmongUsClient.Instance.SendRpcImmediatelyDesync(player.NetId, RpcCalls.SetRole, SendOption.None, SendTo(player), writer =>
+ {
+ writer.Write((ushort)player.Data.Role.DefaultGhostRole);
+ writer.Write(false);
+ });
}
return false;
diff --git a/src/Patches/Gameplay/Ship/ShipStatusPatch.cs b/src/Patches/Gameplay/Ship/ShipStatusPatch.cs
new file mode 100644
index 0000000..216fcf5
--- /dev/null
+++ b/src/Patches/Gameplay/Ship/ShipStatusPatch.cs
@@ -0,0 +1,15 @@
+using BetterAmongUs.Modules;
+using HarmonyLib;
+
+namespace BetterAmongUs.Patches.Gameplay.Ship;
+
+[HarmonyPatch]
+internal static class ShipStatusPatch
+{
+ [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.Awake))]
+ [HarmonyPostfix]
+ private static void ShipStatus_Awake_Postfix(ShipStatus __instance)
+ {
+ VentGroups.CalculateAllVentGroups(__instance.AllVents);
+ }
+}
diff --git a/src/Patches/Gameplay/Ship/VentPatch.cs b/src/Patches/Gameplay/Ship/VentPatch.cs
new file mode 100644
index 0000000..eb67049
--- /dev/null
+++ b/src/Patches/Gameplay/Ship/VentPatch.cs
@@ -0,0 +1,24 @@
+using BetterAmongUs.Modules;
+using BetterAmongUs.Modules.Support;
+using HarmonyLib;
+using UnityEngine;
+
+namespace BetterAmongUs.Patches.Gameplay.Ship;
+
+[HarmonyPatch]
+internal static class VentPatch
+{
+ [HarmonyPatch(typeof(Vent), nameof(Vent.SetOutline))]
+ [HarmonyPrefix]
+ private static bool Vent_SetOutline_Prefix(Vent __instance, bool on, bool mainTarget)
+ {
+ if (BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_VentColorGroups)) return true;
+
+ Color color = VentGroups.GetVentGroupColor(__instance);
+ __instance.myRend.material.SetFloat("_Outline", on ? 1f : 0f);
+ __instance.myRend.material.SetColor("_OutlineColor", color);
+ __instance.myRend.material.SetColor("_AddColor", mainTarget ? color : Color.clear);
+
+ return false;
+ }
+}
diff --git a/src/Patches/Gameplay/UI/MiniMapBehaviourPatch.cs b/src/Patches/Gameplay/UI/MiniMapBehaviourPatch.cs
index 9b998a9..55790c2 100644
--- a/src/Patches/Gameplay/UI/MiniMapBehaviourPatch.cs
+++ b/src/Patches/Gameplay/UI/MiniMapBehaviourPatch.cs
@@ -1,4 +1,8 @@
-using HarmonyLib;
+using BetterAmongUs.Helpers;
+using BetterAmongUs.Modules;
+using BetterAmongUs.Modules.Support;
+using BetterAmongUs.Mono;
+using HarmonyLib;
using UnityEngine;
namespace BetterAmongUs.Patches.Gameplay.UI;
@@ -6,6 +10,10 @@ namespace BetterAmongUs.Patches.Gameplay.UI;
[HarmonyPatch]
internal static class MiniMapBehaviourPatch
{
+ private const float VentLayerOffset = -0.1f;
+ private const float VentArrowLayerOffset = -0.1f; // VentLayerOffset + VentArrowLayerOffset = -0.2
+ private const float UsableLayerOffset = -0.3f;
+
[HarmonyPatch(typeof(MapBehaviour), nameof(MapBehaviour.ShowNormalMap))]
[HarmonyPostfix]
private static void MapBehaviour_ShowNormalMap_Postfix(MapBehaviour __instance)
@@ -21,13 +29,224 @@ internal static class MiniMapBehaviourPatch
private static void MapBehaviour_ShowSabotageMap_Postfix(MapBehaviour __instance)
=> __instance.ColorControl.SetColor(new Color(1f, 0.3f, 0f, 1f));
- [HarmonyPatch(typeof(MapBehaviour), nameof(MapBehaviour.ShowCountOverlay))]
+ [HarmonyPatch(typeof(MapCountOverlay), nameof(MapCountOverlay.OnEnable))]
[HarmonyPostfix]
- private static void MapBehaviour_ShowCountOverlay_Postfix(MapBehaviour __instance)
- => __instance.ColorControl.SetColor(new Color(0.2f, 0.5f, 0f, 1f));
+ private static void MapCountOverlay_OnEnable_Postfix(MapCountOverlay __instance)
+ => __instance.BackgroundColor.SetColor(PlayerTask.PlayerHasTaskOfType(PlayerControl.LocalPlayer) ? Palette.DisabledGrey : new Color(0.2f, 0.5f, 0f, 1f));
+
+ [HarmonyPatch(typeof(MapCountOverlay), nameof(MapCountOverlay.Update))]
+ [HarmonyPrefix]
+ private static void MapCountOverlay_Update_Prefix(MapCountOverlay __instance)
+ {
+ if (PlayerTask.PlayerHasTaskOfType(PlayerControl.LocalPlayer))
+ {
+ __instance.isSab = true;
+ __instance.BackgroundColor.SetColor(Palette.DisabledGrey);
+ __instance.SabotageText.gameObject.SetActive(true);
+ }
+ else if (!PlayerTask.PlayerHasTaskOfType(PlayerControl.LocalPlayer))
+ {
+ __instance.isSab = false;
+ __instance.BackgroundColor.SetColor(new Color(0.2f, 0.5f, 0f, 1f));
+ __instance.SabotageText.gameObject.SetActive(false);
+ }
+ }
[HarmonyPatch(typeof(MapConsole), nameof(MapConsole.Use))]
[HarmonyPostfix]
private static void MapConsole_ShowCountOverlay_Postfix()
=> MapBehaviour.Instance.ColorControl.SetColor(new Color(0.2f, 0.5f, 0f, 1f));
+
+ private static Transform? _icons;
+
+ [HarmonyPatch(typeof(MapBehaviour), nameof(MapBehaviour.Show))]
+ [HarmonyPostfix]
+ private static void MapBehaviour_Show_Postfix(MapBehaviour __instance)
+ {
+ if (BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_MinimapIcons)) return;
+
+ foreach (var button in __instance.infectedOverlay.allButtons)
+ {
+ button.spriteRenderer.color = new Color(1f, 1f, 1f, 0.8f);
+ button.transform.localScale = new Vector3(0.6f, 0.6f, 1f);
+ }
+
+ if (_icons == null)
+ {
+ var icons = new GameObject("Icons")
+ {
+ layer = LayerMask.NameToLayer("UI")
+ };
+ icons.transform.SetParent(__instance.transform.Find("HereIndicatorParent"));
+ icons.transform.localPosition = Vector3.zero;
+ icons.transform.localScale = Vector3.one;
+ _icons = icons.transform;
+
+ foreach (var vent in BAUPlugin.AllVents)
+ {
+ CreateVentIcon(vent);
+ }
+
+ foreach (var map in UnityEngine.Object.FindObjectsOfType())
+ {
+ CreateUsableIcon(map.Cast());
+ }
+
+ foreach (var system in UnityEngine.Object.FindObjectsOfType())
+ {
+ CreateSystemConsoleIcon(system);
+ }
+
+ foreach (var zipline in UnityEngine.Object.FindObjectsOfType())
+ {
+ var icon = CreateIcon(zipline.image.sprite, "SystemIcon");
+ icon.color = Color.white * 0.7f;
+ icon.transform.localScale = Vector3.one * 0.25f;
+ SetPosFromShip(zipline.transform.position, icon.transform, new Vector3(0f, 0.16f, UsableLayerOffset));
+ }
+
+ foreach (var console in UnityEngine.Object.FindObjectsOfType())
+ {
+ foreach (var taskType in console.TaskTypes)
+ {
+ if (taskType is TaskTypes.FixLights or TaskTypes.FixComms or TaskTypes.RestoreOxy or TaskTypes.ResetReactor or TaskTypes.ResetSeismic)
+ {
+ var icon = CreateIcon(console.Image.sprite, "ConsoleIcon");
+ icon.color = Color.white * 0.7f;
+ icon.transform.localScale = Vector3.one * 0.5f;
+ SetPosFromShip(console.transform.position, icon.transform, new Vector3(0f, 0f, UsableLayerOffset));
+ var animatedMapIcon = icon.gameObject.AddComponent();
+ animatedMapIcon.ShouldAnimate += () =>
+ {
+ return console.FindTask(PlayerControl.LocalPlayer) != null;
+ };
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private static void CreateVentIcon(Vent vent)
+ {
+ var icon = CreateIcon(Utils.LoadSprite("BetterAmongUs.Resources.Images.Icons.Vent.png", 380), "VentIcon");
+ var color = VentGroups.GetVentGroupColor(vent);
+ icon.color = new Color(color.r, color.g, color.b, 0.7f);
+
+ SetPosFromShip(vent.transform.position, icon.transform, new Vector3(0f, 0f, VentLayerOffset));
+
+ Vent[] nearbyVents = [vent.Left, vent.Right, vent.Center];
+ float maxSpreadShift = 0.5f;
+ float minSpreadShift = 0.2f;
+ float closeDistance = 10f;
+
+ for (int i = 0; i < nearbyVents.Length; i++)
+ {
+ Vent neighborVent = nearbyVents[i];
+ if (neighborVent)
+ {
+ var arrowIcon = CreateIcon(Utils.LoadSprite("BetterAmongUs.Resources.Images.Icons.Arrow.png", 600), "VentArrowIcon");
+ arrowIcon.color = VentGroups.GetVentGroupColor(neighborVent);
+ arrowIcon.transform.SetParent(icon.transform);
+
+ Vector3 directionToNeighbor = neighborVent.transform.position - vent.transform.position;
+ float distanceToNeighbor = directionToNeighbor.magnitude;
+
+ float spreadShift;
+ if (distanceToNeighbor < closeDistance)
+ {
+ float t = distanceToNeighbor / closeDistance;
+ spreadShift = Mathf.Lerp(minSpreadShift, maxSpreadShift, t);
+ }
+ else
+ {
+ spreadShift = maxSpreadShift;
+ }
+
+ Vector3 arrowOffset = directionToNeighbor.normalized * (0.7f + spreadShift);
+ arrowOffset.y -= 0.08f;
+
+ SetPosFromShip(arrowOffset, arrowIcon.transform, new Vector3(0f, 0f, VentArrowLayerOffset));
+
+ Vector3 ventMapPos = vent.transform.position / ShipStatus.Instance.MapScale;
+ ventMapPos.x *= Mathf.Sign(ShipStatus.Instance.transform.localScale.x);
+
+ Vector3 neighborMapPos = neighborVent.transform.position / ShipStatus.Instance.MapScale;
+ neighborMapPos.x *= Mathf.Sign(ShipStatus.Instance.transform.localScale.x);
+
+ Vector3 mapDirection = neighborMapPos - ventMapPos;
+
+ float angle = Mathf.Atan2(mapDirection.y, mapDirection.x) * Mathf.Rad2Deg;
+ arrowIcon.transform.rotation = Quaternion.Euler(0, 0, angle - 90);
+ }
+ }
+ }
+
+ private static void CreateSystemConsoleIcon(SystemConsole systemConsole)
+ {
+ if (systemConsole.UseIcon is not ImageNames.UseButton)
+ {
+ CreateUsableIcon(systemConsole.Cast());
+ }
+ else
+ {
+ var icon = CreateIcon(systemConsole.Image.sprite, "SystemIcon");
+ icon.color = Color.white * 0.7f;
+ icon.transform.localScale = Vector3.one * 0.4f;
+ SetPosFromShip(systemConsole.transform.position, icon.transform, new Vector3(0f, 0f, UsableLayerOffset));
+ }
+ }
+
+ private static void CreateUsableIcon(IUsable usable)
+ {
+ if (usable.TryCast(out var systemConsole))
+ {
+ if (systemConsole.MinigamePrefab.name == "EmergencyMinigame")
+ {
+ var icon = CreateIcon(Utils.LoadSprite("BetterAmongUs.Resources.Images.Icons.Meeting.png", 500), "MeetingIcon");
+ icon.color = new Color(1f, 1f, 1f, 0.7f);
+ icon.transform.localScale = Vector3.one * 0.35f;
+ SetPosFromShip(usable.Cast().transform.position, icon.transform, new Vector3(0f, 0f, UsableLayerOffset));
+ return;
+ }
+ else
+ {
+ if (systemConsole.UseIcon is ImageNames.UseButton)
+ {
+ return;
+ }
+ }
+ }
+
+ if (HudManager.Instance.UseButton.fastUseSettings.TryGetValue(usable.UseIcon, out var settings))
+ {
+ var icon = CreateIcon(settings.Image, "UsableIcon");
+ icon.color = new Color(1f, 1f, 1f, 0.7f);
+ icon.transform.localScale = Vector3.one * 0.35f;
+ SetPosFromShip(usable.Cast().transform.position, icon.transform, new Vector3(0f, 0f, UsableLayerOffset));
+ }
+ }
+
+ private static SpriteRenderer CreateIcon(Sprite sprite, string name = "Icon")
+ {
+ var go = new GameObject(name)
+ {
+ layer = LayerMask.NameToLayer("UI")
+ };
+ go.transform.SetParent(_icons);
+ go.transform.localScale = Vector3.one;
+ var spriteRenderer = go.AddComponent();
+ spriteRenderer.sprite = sprite;
+ return spriteRenderer;
+ }
+
+ private static void SetPosFromShip(Vector3 shipPos, Transform mapTransform, Vector3? offset = null)
+ {
+ offset ??= Vector3.zero;
+ Vector3 vector = shipPos;
+ vector /= ShipStatus.Instance.MapScale;
+ vector.x *= Mathf.Sign(ShipStatus.Instance.transform.localScale.x);
+ vector.z = -1f;
+ mapTransform.transform.localPosition = new Vector3(vector.x + offset.Value.x, vector.y + offset.Value.y, offset.Value.z);
+ }
}
diff --git a/src/Patches/Gameplay/ZoomPatch.cs b/src/Patches/Gameplay/ZoomPatch.cs
index 4633911..f37069f 100644
--- a/src/Patches/Gameplay/ZoomPatch.cs
+++ b/src/Patches/Gameplay/ZoomPatch.cs
@@ -10,7 +10,7 @@ namespace BetterAmongUs.Patches.Gameplay;
internal class ZoomPatch
{
private static bool _wasZooming = false;
- private static bool _shouldResetButtons = false;
+ private static float _lastOrthographicSize = 0f;
[HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))]
[HarmonyPostfix]
@@ -22,6 +22,8 @@ internal class ZoomPatch
bool shouldReset = !canZoom && _wasZooming;
+ _lastOrthographicSize = Camera.main.orthographicSize;
+
if (shouldReset)
{
SetZoomSize(reset: true);
@@ -29,19 +31,16 @@ internal class ZoomPatch
}
else if (canZoom)
{
- _wasZooming = true;
-
- if (Camera.main.orthographicSize > 3.0f)
- _shouldResetButtons = true;
-
if (Input.mouseScrollDelta.y > 0 && Camera.main.orthographicSize > 3.0f)
{
+ _wasZooming = true;
SetZoomSize(zoomIn: true);
}
else if (Input.mouseScrollDelta.y < 0 &&
(GameState.IsDead || GameState.IsFreePlay || GameState.IsLobby) &&
Camera.main.orthographicSize < 18.0f)
{
+ _wasZooming = true;
SetZoomSize(zoomOut: true);
}
}
@@ -69,13 +68,12 @@ internal class ZoomPatch
(reset || Camera.main.orthographicSize == 3.0f) &&
PlayerControl.LocalPlayer.IsAlive());
- if (_shouldResetButtons)
+ if (Camera.main.orthographicSize != _lastOrthographicSize)
{
ResolutionManager.ResolutionChanged.Invoke(
(float)Screen.width / Screen.height,
Screen.width, Screen.height,
Screen.fullScreen);
- _shouldResetButtons = false;
}
}
}
diff --git a/src/Resources/Images/Icons/Arrow.png b/src/Resources/Images/Icons/Arrow.png
new file mode 100644
index 0000000..ccc3d70
Binary files /dev/null and b/src/Resources/Images/Icons/Arrow.png differ
diff --git a/src/Resources/Images/Icons/Vent.png b/src/Resources/Images/Icons/Vent.png
new file mode 100644
index 0000000..498f70d
Binary files /dev/null and b/src/Resources/Images/Icons/Vent.png differ