Merge pull request #63 from D1GQ/v1.3.2-Hotfix-3

V1.3.2 hotfix 3
This commit is contained in:
D1GQ authored and GitHub committed 2026-02-26 21:01:56 -06:00
commit 432842a7af
25 files changed
+775 -537

No files matched your search

+3
View File
@@ -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();
}
/// <summary>
+30 -33
View File
@@ -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
}
}
/// <summary>
/// Reads the content from the JSON file.
/// </summary>
/// <returns>The content of the JSON file.</returns>
protected virtual string ReadFromFile()
{
return File.ReadAllText(FilePath);
}
/// <summary>
/// Saves the current instance's data to the JSON file.
/// </summary>
@@ -136,19 +127,10 @@ internal abstract class AbstractJsonFile
}
/// <summary>
/// Writes the JSON string to the file.
/// Determines whether the file specified by the current FilePath exists and contains valid, non-empty JSON content.
/// </summary>
/// <param name="json">The JSON string to write.</param>
protected virtual void WriteToFile(string json)
{
File.WriteAllText(FilePath, json);
}
/// <summary>
/// Checks if the JSON file exists and contains valid data.
/// </summary>
/// <returns>True if the file exists and contains valid JSON data, false otherwise.</returns>
private bool CheckFile()
/// <returns>true if the file exists, is not empty, and contains valid, non-empty JSON content; otherwise, false.</returns>
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<JsonElement>(content);
var fileInfo = new FileInfo(FilePath);
if (fileInfo.Length == 0)
{
return false;
}
try
{
var jsonElement = JsonSerializer.Deserialize<JsonElement>(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;
}
/// <summary>
/// Reads the content from the JSON file.
/// </summary>
/// <returns>The content of the JSON file.</returns>
protected virtual string ReadFromFile()
{
return File.ReadAllText(FilePath);
}
/// <summary>
/// Writes the JSON string to the file.
/// </summary>
/// <param name="json">The JSON string to write.</param>
protected virtual void WriteToFile(string json)
{
File.WriteAllText(FilePath, json);
}
}
+51 -326
View File
@@ -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
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="writer">The MessageWriter to write to.</param>
/// <param name="boolsEnumerable">The boolean values to write.</param>
internal static void WriteBooleans(this MessageWriter writer, IEnumerable<bool> 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);
}
/// <summary>
/// Reads an array of boolean values from a MessageReader that were previously packed using WriteBooleans.
/// </summary>
/// <param name="reader">The MessageReader to read from.</param>
/// <returns>An array of boolean values.</returns>
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;
}
/// <summary>
/// Writes an array of bytes to a MessageWriter in a packed format, combining two bytes into one to save space.
/// </summary>
/// <param name="writer">The MessageWriter to write to.</param>
/// <param name="bytesEnumerable">The byte values to write.</param>
internal static void WriteBytes(this MessageWriter writer, IEnumerable<byte> bytesEnumerable)
{
byte[] bytes = bytesEnumerable.ToArray();
writer.Write(bytes.Length);
writer.Write(bytes);
}
/// <summary>
/// Reads an array of bytes from a MessageReader that were previously packed using WritePackedBytes.
/// </summary>
/// <param name="reader">The MessageReader to read from.</param>
/// <returns>An array of bytes.</returns>
internal static byte[] ReadBytes(this MessageReader reader)
{
int count = reader.ReadInt32();
var bytes = reader.ReadBytes(count);
return [.. bytes];
}
/// <summary>
/// Writes an array of bytes to a MessageWriter in a packed format, combining two bytes into one to save space.
/// </summary>
/// <param name="writer">The MessageWriter to write to.</param>
/// <param name="bytesEnumerable">The byte values to write.</param>
internal static void WritePackedBytes(this MessageWriter writer, IEnumerable<byte> 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);
}
}
/// <summary>
/// Reads an array of bytes from a MessageReader that were previously packed using WritePackedBytes.
/// </summary>
/// <param name="reader">The MessageReader to read from.</param>
/// <returns>An array of bytes.</returns>
internal static byte[] ReadPackedBytes(this MessageReader reader)
{
int count = reader.ReadByte();
List<byte> 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();
}
/// <summary>
/// Writes a floating-point value to a MessageWriter in a packed format to save space.
/// Handles negative values, integers, and decimal numbers efficiently.
/// </summary>
/// <param name="writer">The MessageWriter to write to.</param>
/// <param name="value">The float value to write.</param>
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<bool>
{
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);
}
}
/// <summary>
/// Reads a floating-point value from a MessageReader that was previously packed using WritePacked.
/// Handles negative values, integers, and decimal numbers efficiently.
/// </summary>
/// <param name="reader">The MessageReader to read from.</param>
/// <returns>The unpacked float value.</returns>
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;
}
}
/// <summary>
/// Writes a player's ID to a MessageWriter, using 255 for null players.
/// </summary>
@@ -343,6 +93,31 @@ internal static class InnerNetClientHelper
/// <returns>The Vent or null if not found.</returns>
internal static Vent? ReadVentId(this MessageReader reader) => BAUPlugin.AllVents.FirstOrDefault(vent => vent.Id == reader.ReadInt32());
/// <summary>
/// Writes an array of bytes to a MessageWriter in a packed format, combining two bytes into one to save space.
/// </summary>
/// <param name="writer">The MessageWriter to write to.</param>
/// <param name="bytesEnumerable">The byte values to write.</param>
internal static void WriteBytes(this MessageWriter writer, IEnumerable<byte> bytesEnumerable)
{
byte[] bytes = bytesEnumerable.ToArray();
writer.Write(bytes.Length);
writer.Write(bytes);
}
/// <summary>
/// Reads an array of bytes from a MessageReader that were previously packed using WritePackedBytes.
/// </summary>
/// <param name="reader">The MessageReader to read from.</param>
/// <returns>An array of bytes.</returns>
internal static byte[] ReadBytes(this MessageReader reader)
{
int count = reader.ReadInt32();
var bytes = reader.ReadBytes(count);
return [.. bytes];
}
/// <summary>
/// Converts a MessageWriter to a MessageReader.
/// </summary>
@@ -403,90 +178,40 @@ internal static class InnerNetClientHelper
}
/// <summary>
/// Writes multiple MessageReaders to a MessageWriter.
/// Sends an RPC message immediately to all clients that match the specified target criteria, bypassing standard
/// synchronization mechanisms.
/// </summary>
/// <param name="writer">The MessageWriter to write to.</param>
/// <param name="readers">The MessageReaders to write.</param>
/// <param name="clear">Whether to clear the writer before writing.</param>
/// <returns>The MessageWriter for method chaining.</returns>
internal static MessageWriter WriteReaders(this MessageWriter writer, IEnumerable<MessageReader> readers, bool clear = false)
/// <param name="client">The InnerNetClient instance used to send the RPC messages.</param>
/// <param name="playerNetId">The network ID of the player on whose behalf the RPC is sent.</param>
/// <param name="callId">The identifier for the RPC call to be executed.</param>
/// <param name="option">The send option that determines how the message is delivered (e.g., reliability, ordering).</param>
/// <param name="targetClients">A function that selects which clients should receive the RPC message. The RPC is sent only to clients for which
/// this function returns <see langword="true"/>.</param>
/// <param name="writeTo">An action that writes the RPC payload to the provided MessageWriter.</param>
internal static void SendRpcImmediatelyDesync(this InnerNetClient client, uint playerNetId, RpcCalls callId, SendOption option, Func<ClientData, bool> targetClients, Action<MessageWriter> 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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="writer">The MessageWriter to copy.</param>
/// <returns>A new MessageWriter with the same content.</returns>
internal static MessageWriter Copy(this MessageWriter writer)
/// <param name="client">The InnerNetClient instance used to send the RPC message.</param>
/// <param name="playerNetId">The network identifier of the target player to whom the RPC is sent.</param>
/// <param name="callId">The identifier of the RPC call to invoke.</param>
/// <param name="option">The send option that determines how the message is delivered (such as reliability or channel).</param>
/// <param name="writeTo">An action that writes the RPC message content to the provided MessageWriter.</param>
/// <param name="targetClientId">The client identifier of the specific target client. Use -1 to broadcast to all clients.</param>
internal static void SendRpcImmediately(this InnerNetClient client, uint playerNetId, RpcCalls callId, SendOption option, Action<MessageWriter> writeTo, int targetClientId = -1)
{
var newWriter = MessageWriter.Get(writer.SendOption);
newWriter.Write(writer.ToByteArray(false));
return newWriter;
}
/// <summary>
/// Starts the RPC desynchronization process for the given player, call ID, and send option.
/// </summary>
/// <param name="client">The InnerNetClient instance.</param>
/// <param name="playerNetId">The network ID of the player.</param>
/// <param name="callId">The RPC call ID.</param>
/// <param name="option">The send option for the RPC.</param>
/// <param name="ignoreClientId">The client ID to ignore. Default is -1, which means no client is ignored.</param>
/// <param name="clientCheck">Optional function to filter which clients receive the RPC.</param>
/// <returns>A list of MessageWriter instances for the RPC calls.</returns>
/// <example>
/// <code>
/// List&lt;MessageWriter&gt; messageWriter = AmongUsClient.Instance.StartRpcDesync(PlayerNetId, (byte)RpcCalls, SendOption, ClientId);
/// messageWriter.ForEach(mW => mW.Write("RPC TEST"));
/// AmongUsClient.Instance.FinishRpcDesync(messageWriter);
/// </code>
/// </example>
internal static List<MessageWriter> StartRpcDesync(this InnerNetClient client, uint playerNetId, byte callId, SendOption option, int ignoreClientId = -1, Func<ClientData, bool>? clientCheck = null)
{
List<MessageWriter> 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;
}
/// <summary>
/// Completes and sends the RPC desynchronization messages.
/// </summary>
/// <param name="client">The InnerNetClient instance.</param>
/// <param name="messageWriters">The list of MessageWriters to finish and send.</param>
internal static void FinishRpcDesync(this InnerNetClient client, List<MessageWriter> 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);
}
}
+2 -2
View File
@@ -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
/// </summary>
/// <param name="data">The player data to check.</param>
/// <returns>True if the player is on the impostor team.</returns>
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;
/// <summary>
/// Checks if a player is an impostor teammate of the local player.
+12 -1
View File
@@ -7,6 +7,9 @@ namespace BetterAmongUs.Helpers;
/// </summary>
internal static class RoleHelper
{
/// <summary>
/// Provides a lazily initialized lookup dictionary that maps each role type to its corresponding role behavior.
/// </summary>
private static readonly Lazy<Dictionary<RoleTypes, RoleBehaviour>> roleLookup =
new(() =>
{
@@ -34,9 +37,17 @@ internal static class RoleHelper
/// </summary>
/// <param name="role">The role type to check.</param>
/// <returns>True if the role is part of the impostor team, false otherwise.</returns>
internal static bool IsImpostorRole(RoleTypes role) =>
internal static bool IsImpostorRole(this RoleTypes role) =>
role.GetBehaviourPrefab().TeamType is RoleTeamTypes.Impostor;
/// <summary>
/// Determines whether the specified role is considered a ghost role.
/// </summary>
/// <param name="role">The role type to check.</param>
/// <returns>true if the role is classified as a ghost role; otherwise, false.</returns>
internal static bool IsGhostRole(this RoleTypes role) =>
RoleManager.IsGhostRole(role);
/// <summary>
/// Gets the display name of a role type.
/// </summary>
+7
View File
@@ -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;
}
+1 -1
View File
@@ -31,7 +31,7 @@ internal static class ModInfo
/// <summary>
/// The hotfix number for hotfix releases.
/// </summary>
internal const string HOTFIX_NUM = "2";
internal const string HOTFIX_NUM = "3";
/// <summary>
/// Indicates whether this is a hotfix release.
-69
View File
@@ -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
}
}
/// <summary>
/// Harmony patch for PlatformSpecificData to detect platform spoofing.
/// </summary>
[HarmonyPatch(typeof(PlatformSpecificData))]
class PlatformSpecificDataPatch
{
/// <summary>
/// Postfix patch to validate platform data after deserialization.
/// </summary>
[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 { }
}
}
}
/// <summary>
/// Handles RPCs before anti-cheat detection checks.
/// </summary>
@@ -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)
@@ -163,6 +163,18 @@ public static class BAUModdedSupportFlags
/// </summary>
public static string Disable_EndGameSummary = "gameplay.disable.endgamesummary";
/// <summary>
/// Disables custom icon on mini map.
/// When enabled by another mod, custom icons on the mini map will not be generated.
/// </summary>
public static string Disable_MinimapIcons = "gameplay.disable.minimapicons";
/// <summary>
/// Disables custom vent color highlights.
/// When enabled by another mod, vent colors when highlighted will not use vent groups.
/// </summary>
public static string Disable_VentColorGroups = "gameplay.disable.ventcolorgroups";
private static readonly HashSet<string> _flags = [];
private static bool _initialized = false;
+119
View File
@@ -0,0 +1,119 @@
using UnityEngine;
namespace BetterAmongUs.Modules;
/// <summary>
/// Provides functionality for grouping vents and assigning colors to vent groups.
/// </summary>
internal static class VentGroups
{
/// <summary>
/// Dictionary mapping each vent ID to the lowest ID in its group.
/// </summary>
private static readonly Dictionary<int, int> ventToLowestId = [];
/// <summary>
/// Dictionary mapping group IDs (lowest vent ID in the group) to their assigned colors.
/// </summary>
private static readonly Dictionary<int, Color> groupColors = [];
/// <summary>
/// Calculates all vent groups by performing a breadth-first search through connected vents.
/// </summary>
/// <param name="vents">Array of all vents in the scene to process.</param>
/// <remarks>
/// 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.
/// </remarks>
internal static void CalculateAllVentGroups(Vent[] vents)
{
ventToLowestId.Clear();
groupColors.Clear();
HashSet<Vent> unprocessedVents = [.. vents];
HashSet<int> groupLowestIds = [];
Dictionary<int, List<Vent>> groups = [];
while (unprocessedVents.Count > 0)
{
Vent startVent = unprocessedVents.First();
HashSet<Vent> group = [];
Queue<Vent> 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;
}
}
}
/// <summary>
/// Gets the group color for a specific vent.
/// </summary>
/// <param name="vent">The vent to get the group color for.</param>
/// <returns>
/// The color assigned to the vent's group, or <see cref="Color.black"/> if the vent hasn't been processed.
/// </returns>
internal static Color GetVentGroupColor(Vent vent)
{
if (ventToLowestId.TryGetValue(vent.Id, out int lowestId))
{
return groupColors[lowestId];
}
return Color.black;
}
}
+56
View File
@@ -0,0 +1,56 @@
using UnityEngine;
namespace BetterAmongUs.Mono;
/// <summary>
/// Animates a map icon by smoothly scaling it up and down when conditions are met.
/// </summary>
internal class AnimatedMapIcon : MonoBehaviour
{
/// <summary>
/// Event that determines whether the icon should animate.
/// Returns true if animation should occur, false otherwise.
/// </summary>
internal event Func<bool>? ShouldAnimate;
private Vector3 originalScale;
/// <summary>
/// The multiplier applied to the original scale when fully animated.
/// </summary>
private readonly float scaleMultiplier = 1.2f;
/// <summary>
/// The speed of the animation loop.
/// </summary>
private readonly float animationSpeed = 2f;
/// <summary>
/// Initializes the component by storing the original scale of the icon.
/// </summary>
private void Awake()
{
originalScale = transform.localScale;
}
/// <summary>
/// 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.
/// </summary>
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;
}
}
}
+8 -8
View File
@@ -29,15 +29,15 @@ internal static class RPC
/// <param name="targetClientId">The specific client ID to target, or -1 to broadcast to all clients.</param>
internal static void SendCustomRpcPacked(CustomRPC customRPC, Action<MessageWriter> 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);
}
/// <summary>
@@ -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()
+84 -52
View File
@@ -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<AudioSource>());
}
}
// 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<SpriteRenderer>().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<SpriteRenderer>().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;
}
}
@@ -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");
}
}
}
}
@@ -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 { }
}
}
}
@@ -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
@@ -15,20 +15,23 @@ internal static class RoleManagerPatch
internal static Dictionary<string, int> ImpostorMultiplier = []; // HashPuid, Multiplier
private static readonly Random random = new();
// Check if client is not verified Better Among Us user
static readonly Func<InnerNet.ClientData, bool> clientCheck = (clientData) =>
// Check if client is verified Better Among Us user
private static Func<InnerNet.ClientData, bool> 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> 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> 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> 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;
@@ -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);
}
}
+24
View File
@@ -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;
}
}
@@ -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<IHudOverrideTask>(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<IHudOverrideTask>(PlayerControl.LocalPlayer))
{
__instance.isSab = true;
__instance.BackgroundColor.SetColor(Palette.DisabledGrey);
__instance.SabotageText.gameObject.SetActive(true);
}
else if (!PlayerTask.PlayerHasTaskOfType<IHudOverrideTask>(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<MapConsole>())
{
CreateUsableIcon(map.Cast<IUsable>());
}
foreach (var system in UnityEngine.Object.FindObjectsOfType<SystemConsole>())
{
CreateSystemConsoleIcon(system);
}
foreach (var zipline in UnityEngine.Object.FindObjectsOfType<ZiplineConsole>())
{
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<Console>())
{
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>();
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<IUsable>());
}
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<SystemConsole>(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<MonoBehaviour>().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<MonoBehaviour>().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>();
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);
}
}
+6 -8
View File
@@ -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;
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB