From d2c3b779cc4261b0bd926ab6b8b704f0c108e353 Mon Sep 17 00:00:00 2001 From: fiszfasznasz Date: Mon, 11 Aug 2025 16:13:55 +0200 Subject: [PATCH 01/11] Notifications --- src/Cheats/NotificationHandler.cs | 125 ++++++++++++++++++++++++++++ src/MalumMenu.cs | 2 + src/Patches/MeetingHudPatches.cs | 1 + src/Patches/OtherPatches.cs | 46 ++++++++++ src/Patches/PlayerControlPatches.cs | 49 ++++++++++- src/Patches/PlayerPhysicsPatches.cs | 45 ++++++++-- src/UI/CheatToggles.cs | 6 ++ src/UI/MenuUI.cs | 9 +- src/UI/NotificationUI.cs | 54 ++++++++++++ src/Utilities/Utils.cs | 55 +++++++----- 10 files changed, 362 insertions(+), 30 deletions(-) create mode 100644 src/Cheats/NotificationHandler.cs create mode 100644 src/UI/NotificationUI.cs diff --git a/src/Cheats/NotificationHandler.cs b/src/Cheats/NotificationHandler.cs new file mode 100644 index 00000000..e10c5d10 --- /dev/null +++ b/src/Cheats/NotificationHandler.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using UnityEngine; +using AmongUs.GameOptions; + +namespace MalumMenu; + +/// +/// Handles the creation and storage of event-based notification logs. +/// +public static class NotificationHandler +{ + public static readonly List notificationLog = new(); + private const int MaxLogEntries = 100; + + private static void AddLog(string message) + { + if (notificationLog.Count >= MaxLogEntries) + { + notificationLog.RemoveAt(0); + } + notificationLog.Add(message); + + // Also show the notification on the bottom-left of the screen. + if (DestroyableSingleton.InstanceExists && HudManager.Instance.Notifier != null) + { + // We use AddDisconnectMessage as it's a simple way to show custom text. + HudManager.Instance.Notifier.AddDisconnectMessage(message); + } + } + + /// + /// Checks if a player is currently disguised and returns their real and displayed names. + /// + private static (string realName, string displayName, bool isDisguised) GetPlayerIdentity(PlayerControl player) + { + if (player == null || player.Data == null) return ("", "", false); + + // Get the real player's name and color from their permanent data + string realName = $"{player.Data.PlayerName}"; + + // Get the displayed (current outfit) name and color + string displayName = $"{player.CurrentOutfit.PlayerName}"; + + // A player is disguised if their currently displayed name is different from their real name. + // This is the most reliable way to check for shapeshifting on ALL clients, + // as the 'CurrentOutfit' is synchronized for rendering purposes, whereas specific + // role timers (like durationSecondsRemaining) might not be. + bool isDisguised = player.CurrentOutfit.PlayerName != player.Data.PlayerName; + + return (realName, displayName, isDisguised); + } + + public static void HandlePlayerKill(PlayerControl killer, PlayerControl victim) + { + if (!CheatToggles.notifyOnDeath || killer == null || victim == null) return; + + var (realKillerName, displayKillerName, isDisguised) = GetPlayerIdentity(killer); + string victimName = $"{victim.CurrentOutfit.PlayerName}"; + + PlainShipRoom room = Utils.getRoomFromPosition(victim.GetTruePosition()); + string roomName = room != null ? room.RoomId.ToString() : "an unknown location"; + + string message; + if (isDisguised) + { + message = $"{realKillerName} (as {displayKillerName}) killed {victimName} in {roomName}."; + } + else + { + message = $"{realKillerName} killed {victimName} in {roomName}."; + } + AddLog(message); + } + + public static void HandleGuardianAngelSave(PlayerControl killer, PlayerControl target) + { + if (!CheatToggles.notifyOnDeath || killer == null || target == null) return; + + var (realKillerName, displayKillerName, isDisguised) = GetPlayerIdentity(killer); + string targetName = $"{target.CurrentOutfit.PlayerName}"; + + PlainShipRoom room = Utils.getRoomFromPosition(target.GetTruePosition()); + string roomName = room != null ? room.RoomId.ToString() : "an unknown location"; + + string message; + if (isDisguised) + { + message = $"{realKillerName} (as {displayKillerName}) tried to kill {targetName} in {roomName}. (Saved)"; + } + else + { + message = $"{realKillerName} tried to kill {targetName} in {roomName}. (Saved)"; + } + AddLog(message); + } + + public static void HandleVent(PlayerControl player, bool entered, string roomName) + { + if (!CheatToggles.notifyOnVent || player == null) return; + + var (realPlayerName, displayPlayerName, isDisguised) = GetPlayerIdentity(player); + string action = entered ? "entered" : "exited"; + + string message; + if (isDisguised) + { + message = $"{realPlayerName} (as {displayPlayerName}) has {action} a vent in {roomName}."; + } + else + { + message = $"{realPlayerName} has {action} a vent in {roomName}."; + } + AddLog(message); + } + + public static void HandlePlayerDisconnect(NetworkedPlayerInfo player) + { + if (!CheatToggles.notifyOnDisconnect || player == null) return; + + string playerName = $"{player.PlayerName}"; + + string message = $"{playerName} has disconnected."; + AddLog(message); + } +} \ No newline at end of file diff --git a/src/MalumMenu.cs b/src/MalumMenu.cs index 569663a1..3b0ddcab 100644 --- a/src/MalumMenu.cs +++ b/src/MalumMenu.cs @@ -18,6 +18,7 @@ public partial class MalumMenu : BasePlugin public static string malumVersion = "2.5.1"; public static List supportedAU = ["2025.3.25", "2025.3.31", "2025.6.10"]; public static MenuUI menuUI; + public static NotificationUI notificationUI; // public static ConsoleUI consoleUI; public static ConfigEntry menuKeybind; public static ConfigEntry menuHtmlColor; @@ -80,6 +81,7 @@ public override void Load() Harmony.PatchAll(); menuUI = AddComponent(); + notificationUI = AddComponent(); // consoleUI = AddComponent(); // Disable Telemetry (haven't fully tested if it works, but according to Unity docs it should) diff --git a/src/Patches/MeetingHudPatches.cs b/src/Patches/MeetingHudPatches.cs index 275c31da..9e813907 100644 --- a/src/Patches/MeetingHudPatches.cs +++ b/src/Patches/MeetingHudPatches.cs @@ -96,5 +96,6 @@ public static void Prefix(MeetingHud __instance) voteSpreader.Votes.Clear(); } MeetingHud_Update.votedPlayers.Clear(); + PlayerPhysics_LateUpdate.ClearAllStates(); } } \ No newline at end of file diff --git a/src/Patches/OtherPatches.cs b/src/Patches/OtherPatches.cs index 151b61a2..1e1c448c 100644 --- a/src/Patches/OtherPatches.cs +++ b/src/Patches/OtherPatches.cs @@ -4,6 +4,7 @@ using UnityEngine; using System; using System.Security.Cryptography; +using System.Collections.Generic; namespace MalumMenu; @@ -19,6 +20,51 @@ public static void Prefix(PlatformSpecificData __instance) } } +[HarmonyPatch(typeof(GameData), nameof(GameData.RemovePlayer))] +public static class GameData_RemovePlayer_Patch +{ + private static readonly HashSet notifiedDisconnects = new(); + + public static void ClearNotifiedDisconnects() => notifiedDisconnects.Clear(); + + // Use a Prefix patch to capture the PlayerInfo *before* it gets removed from the game's data lists. + public static void Prefix(GameData __instance, byte playerId) + { + // Only notify during an active game, not in lobby or post-game. + if (CheatToggles.notifyOnDisconnect && Utils.isInGame) + { + // If we've already notified for this player, don't do it again. + if (notifiedDisconnects.Contains(playerId)) + { + return; + } + + var player = __instance.GetPlayerById(playerId); + // The check for `!player.Disconnected` was preventing this from ever firing. It's removed. + if (player != null) + { + NotificationHandler.HandlePlayerDisconnect(player); + // Add the player to the set so we don't notify again for this game session. + notifiedDisconnects.Add(playerId); + } + } + } +} + +[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))] +public static class AmongUsClient_OnGameEnd_Patch +{ + public static void Postfix() + { + // Clear the set of notified disconnected players when a game ends. + GameData_RemovePlayer_Patch.ClearNotifiedDisconnects(); + + // Clear the set of notified killed victims when a game ends. + PlayerControl_MurderPlayer_Patch.ClearNotifiedKilledVictims(); + } +} + + [HarmonyPatch(typeof(FreeChatInputField), nameof(FreeChatInputField.UpdateCharCount))] public static class FreeChatInputField_UpdateCharCount { diff --git a/src/Patches/PlayerControlPatches.cs b/src/Patches/PlayerControlPatches.cs index 1ed928cc..3d303351 100644 --- a/src/Patches/PlayerControlPatches.cs +++ b/src/Patches/PlayerControlPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using System.Collections.Generic; namespace MalumMenu; @@ -14,6 +15,50 @@ public static void Postfix(PlayerControl __instance){ } } +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.MurderPlayer))] +public static class PlayerControl_MurderPlayer_Patch +{ + // A HashSet to track victims for whom a notification has already been sent on this client. + // This prevents duplicate notifications if the event is somehow triggered more than once. + private static readonly HashSet notifiedKilledVictims = new(); + + /// + /// Clears the set of notified victims. This must be called at the end of each game. + /// + public static void ClearNotifiedKilledVictims() => notifiedKilledVictims.Clear(); + + // A Prefix runs *before* the original method. This lets us check conditions before the kill happens. + public static void Prefix(PlayerControl __instance, PlayerControl target) + { + if (target == null) + { + return; + } + + // Check if the target is protected by a Guardian Angel. + if (target.protectedByGuardianId != -1) + { + // This is a "save" event. Show the notification but do not add the player to the + // notifiedKilledVictims set, allowing a future kill notification to appear. + NotificationHandler.HandleGuardianAngelSave(__instance, target); + } + else + { + // This is a potential kill event. Check if we've already notified for this victim's death. + if (notifiedKilledVictims.Contains(target.PlayerId)) + { + return; + } + + // If not protected and not already notified, the kill is successful. + NotificationHandler.HandlePlayerKill(__instance, target); + + // Add the victim's ID to the set ONLY on a successful kill to prevent duplicate kill notifications. + notifiedKilledVictims.Add(target.PlayerId); + } + } +} + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdCheckMurder))] public static class PlayerControl_CmdCheckMurder { @@ -54,9 +99,9 @@ public static bool Prefix(PlayerControl __instance, PlayerControl target) [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.TurnOnProtection))] public static class PlayerControl_TurnOnProtection { - // Prefix patch of PlayerControl.ProtectPlayer to render all protections visible if CheatToggles.seeGhosts is enabled + // Prefix patch of PlayerControl.ProtectPlayer to render all protections visible if CheatToggles.seeGhosts is enabled public static void Prefix(ref bool visible){ - if (CheatToggles.seeGhosts){ + if (CheatToggles.seeGhosts){ visible = true; } } diff --git a/src/Patches/PlayerPhysicsPatches.cs b/src/Patches/PlayerPhysicsPatches.cs index b7af6405..14d4b975 100644 --- a/src/Patches/PlayerPhysicsPatches.cs +++ b/src/Patches/PlayerPhysicsPatches.cs @@ -1,13 +1,48 @@ using HarmonyLib; using UnityEngine; +using System.Collections.Generic; namespace MalumMenu; [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.LateUpdate))] public static class PlayerPhysics_LateUpdate { + private static readonly Dictionary wasInVent = new(); + public static readonly Dictionary lastKnownPositions = new(); + + public static void ClearAllStates() + { + wasInVent.Clear(); + lastKnownPositions.Clear(); + } + public static void Postfix(PlayerPhysics __instance) { + if (__instance.myPlayer != null && !__instance.myPlayer.Data.IsDead) + { + // Update the player's last known position every frame they are not in a vent. + if (!__instance.myPlayer.inVent) + { + lastKnownPositions[__instance.myPlayer.PlayerId] = __instance.myPlayer.GetTruePosition(); + } + + // Vent usage detection + if (CheatToggles.notifyOnVent && Utils.isInGame) + { + byte playerId = __instance.myPlayer.PlayerId; + bool currentlyInVent = __instance.myPlayer.inVent; + + if (wasInVent.TryGetValue(playerId, out bool previouslyInVent) && currentlyInVent != previouslyInVent) + { + Vector2 positionToCheck = currentlyInVent ? lastKnownPositions[playerId] : __instance.myPlayer.GetTruePosition(); + PlainShipRoom room = Utils.getRoomFromPosition(positionToCheck); + string roomName = room != null ? room.RoomId.ToString() : "an unknown location"; + + NotificationHandler.HandleVent(__instance.myPlayer, currentlyInVent, roomName); + } + wasInVent[playerId] = currentlyInVent; + } + } MalumESP.playerNametags(__instance); MalumESP.seeGhostsCheat(__instance); @@ -37,14 +72,12 @@ public static void Postfix(PlayerPhysics __instance) TracersHandler.drawPlayerTracer(__instance); GameObject[] bodyObjects = GameObject.FindGameObjectsWithTag("DeadBody"); - foreach(GameObject bodyObject in bodyObjects) // Finds and loops through all dead bodies + foreach(GameObject bodyObject in bodyObjects) { DeadBody deadBody = bodyObject.GetComponent(); - - if (deadBody){ - if (!deadBody.Reported){ // Only draw tracers for unreported dead bodies - TracersHandler.drawBodyTracer(deadBody); - } + if (deadBody && !deadBody.Reported) + { + TracersHandler.drawBodyTracer(deadBody); } } } diff --git a/src/UI/CheatToggles.cs b/src/UI/CheatToggles.cs index b4cb3d15..d6c46399 100644 --- a/src/UI/CheatToggles.cs +++ b/src/UI/CheatToggles.cs @@ -86,6 +86,12 @@ public struct CheatToggles //public static bool voteImmune; public static bool noOptionsLimits; + //Notifications + public static bool notifyOnDeath; + public static bool notifyOnDisconnect; + public static bool notifyOnVent; + public static bool showNotificationLog; + //Passive public static bool unlockFeatures; public static bool freeCosmetics; diff --git a/src/UI/MenuUI.cs b/src/UI/MenuUI.cs index 86dce37d..4f9ccedc 100644 --- a/src/UI/MenuUI.cs +++ b/src/UI/MenuUI.cs @@ -141,7 +141,7 @@ private void Start() new ToggleInfo(" Enable Chat", () => CheatToggles.alwaysChat, x => CheatToggles.alwaysChat = x), new ToggleInfo(" Unlock Textbox", () => CheatToggles.chatJailbreak, x => CheatToggles.chatJailbreak = x) ], [])); - + // Host-Only cheats are temporarly disabled because of some bugs //groups.Add(new GroupInfo("Host-Only", false, new List() { @@ -181,6 +181,13 @@ private void Start() ])); + groups.Add(new GroupInfo("Notifications", false, [ + new ToggleInfo(" On Player Death", () => CheatToggles.notifyOnDeath, x => CheatToggles.notifyOnDeath = x), + new ToggleInfo(" On Player Disconnect", () => CheatToggles.notifyOnDisconnect, x => CheatToggles.notifyOnDisconnect = x), + new ToggleInfo(" On Vent Usage", () => CheatToggles.notifyOnVent, x => CheatToggles.notifyOnVent = x), + new ToggleInfo(" Show Notification Log", () => CheatToggles.showNotificationLog, x => CheatToggles.showNotificationLog = x) + ], [])); + groups.Add(new GroupInfo("Passive", false, [ new ToggleInfo(" Free Cosmetics", () => CheatToggles.freeCosmetics, x => CheatToggles.freeCosmetics = x), new ToggleInfo(" Avoid Penalties", () => CheatToggles.avoidBans, x => CheatToggles.avoidBans = x), diff --git a/src/UI/NotificationUI.cs b/src/UI/NotificationUI.cs new file mode 100644 index 00000000..0265709d --- /dev/null +++ b/src/UI/NotificationUI.cs @@ -0,0 +1,54 @@ +using UnityEngine; + +namespace MalumMenu; + +/// +/// Renders the Notification Log window using OnGUI. +/// +public class NotificationUI : MonoBehaviour +{ + private Vector2 scrollPosition = Vector2.zero; + private Rect windowRect = new Rect(320, 10, 500, 300); + private GUIStyle logStyle; + + private void OnGUI() + { + if (!CheatToggles.showNotificationLog) return; + + if (logStyle == null){ + logStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 16, + richText = true // Essential for colored names + }; + } + + Color configUIColor; + if (ColorUtility.TryParseHtmlString(MalumMenu.menuHtmlColor.Value, out configUIColor)){ + GUI.backgroundColor = configUIColor; + } + + windowRect = GUI.Window(2, windowRect, (GUI.WindowFunction)NotificationWindow, "Notification Log"); + } + + private void NotificationWindow(int windowID) + { + GUILayout.BeginVertical(); + scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true); + + foreach (var log in NotificationHandler.notificationLog) + { + GUILayout.Label(log, logStyle); + } + + GUILayout.EndScrollView(); + + if (GUILayout.Button("Clear Log")){ + NotificationHandler.notificationLog.Clear(); + } + + GUILayout.EndVertical(); + + GUI.DragWindow(); + } +} \ No newline at end of file diff --git a/src/Utilities/Utils.cs b/src/Utilities/Utils.cs index 67b4fa56..3fbfb29c 100644 --- a/src/Utilities/Utils.cs +++ b/src/Utilities/Utils.cs @@ -84,7 +84,7 @@ public static bool isValidTarget(NetworkedPlayerInfo target) } return fullRequirements; - + } // Adjusts HUD resolution @@ -105,7 +105,7 @@ public static void murderPlayer(PlayerControl target, MurderResultFlags result) PlayerControl.LocalPlayer.MurderPlayer(target, MurderResultFlags.Succeeded); return; - + } foreach (var item in PlayerControl.AllPlayerControls) @@ -125,7 +125,7 @@ public static void reportDeadBody(NetworkedPlayerInfo playerData) PlayerControl.LocalPlayer.CmdReportDeadBody(playerData); return; - + } var HostData = AmongUsClient.Instance.GetHost(); @@ -149,7 +149,7 @@ public static void completeMyTasks() PlayerControl.LocalPlayer.RpcCompleteTask(task.Id); } return; - + } var HostData = AmongUsClient.Instance.GetHost(); @@ -205,7 +205,7 @@ public static void drawTracer(GameObject sourceObject, GameObject targetObject, lineRenderer.material = material; lineRenderer.SetColors(color, color); - + lineRenderer.SetPosition(0, sourceObject.transform.position); lineRenderer.SetPosition(1, targetObject.transform.position); } @@ -231,13 +231,13 @@ public static void closeChat() // Get the distance between two players as a float public static float getDistanceFrom(PlayerControl target, PlayerControl source = null){ - + if (source.IsNull()){ source = PlayerControl.LocalPlayer; } Vector2 vector = target.GetTruePosition() - source.GetTruePosition(); - float magnitude = vector.magnitude; + float magnitude = vector.magnitude; return magnitude; @@ -245,7 +245,7 @@ public static float getDistanceFrom(PlayerControl target, PlayerControl source = // Returns a list of all the players in the game ordered from closest to farthest (from LocalPlayer by default) public static System.Collections.Generic.List getPlayersSortedByDistance(PlayerControl source = null){ - + if (source.IsNull()){ source = PlayerControl.LocalPlayer; } @@ -263,9 +263,9 @@ public static System.Collections.Generic.List getPlayersSortedByD outputList.Add(player); } } - + outputList = outputList.OrderBy(target => getDistanceFrom(target, source)).ToList(); - + if (outputList.Count <= 0) { return null; @@ -280,10 +280,10 @@ public static byte getCurrentMapID() { // If playing the tutorial if (isFreePlay) - { + { return (byte)AmongUsClient.Instance.TutorialMapId; - }else{ + }else{ // Works for local/online games return GameOptionsManager.Instance.currentGameOptions.MapId; } @@ -294,6 +294,19 @@ public static SystemTypes getCurrentRoom(){ return HudManager.Instance.roomTracker.LastRoom.RoomId; } + // Gets the room object from a Vector2 position. + public static PlainShipRoom getRoomFromPosition(Vector2 position){ + if (ShipStatus.Instance == null) return null; + + foreach (var room in ShipStatus.Instance.AllRooms) + { + if (room != null && room.roomArea != null && room.roomArea.OverlapPoint(position)){ + return room; + } + } + return null; + } + // Fancy colored ping text public static string getColoredPingText(int ping){ @@ -317,14 +330,14 @@ public static KeyCode stringToKeycode(string keyCodeStr){ if(!string.IsNullOrEmpty(keyCodeStr)){ // Empty strings are automatically invalid try{ - + // Case-insensitive parse of UnityEngine.KeyCode to check if string is validssss KeyCode keyCode = (KeyCode)System.Enum.Parse(typeof(KeyCode), keyCodeStr, true); - + return keyCode; }catch{} - + } return KeyCode.Delete; // If string is invalid, return Delete as the default key @@ -336,14 +349,14 @@ public static bool stringToPlatformType(string platformStr, out Platforms? platf if(!string.IsNullOrEmpty(platformStr)){ // Empty strings are automatically invalid try{ - + // Case-insensitive parse of Platforms from string (if it valid) platform = (Platforms)System.Enum.Parse(typeof(Platforms), platformStr, true); - + return true; // If platform type is valid, return false }catch{} - + } platform = null; @@ -381,7 +394,7 @@ public static string getNameTag(NetworkedPlayerInfo playerInfo, string playerNam } nameTag = $"{getRoleName(playerInfo)}\r\n{nameTag}"; - + } else if (PlayerControl.LocalPlayer.Data.Role.NameColor == playerInfo.Role.NameColor){ if (isChat){ @@ -400,7 +413,7 @@ public static string getNameTag(NetworkedPlayerInfo playerInfo, string playerNam // Found here: https://github.com/NuclearPowered/Reactor/blob/6eb0bf19c30733b78532dada41db068b2b247742/Reactor/Networking/Patches/HttpPatches.cs public static void showPopup(string text){ var popup = Object.Instantiate(DiscordManager.Instance.discordPopup, Camera.main!.transform); - + var background = popup.transform.Find("Background").GetComponent(); var size = background.size; size.x *= 2.5f; @@ -438,7 +451,7 @@ public static Texture2D LoadTextureFromResources(string path) var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path); var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); using MemoryStream ms = new(); - + stream.CopyTo(ms); ImageConversion.LoadImage(texture, ms.ToArray(), false); return texture; From 4182eef326434ef7de32cd6236d5e1627a3b235c Mon Sep 17 00:00:00 2001 From: fiszfasznasz Date: Mon, 11 Aug 2025 17:40:13 +0200 Subject: [PATCH 02/11] Fix host detection --- src/Patches/PlayerControlPatches.cs | 2 +- src/Utilities/Utils.cs | 46 +++++++++++++++-------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/Patches/PlayerControlPatches.cs b/src/Patches/PlayerControlPatches.cs index 3d303351..40951663 100644 --- a/src/Patches/PlayerControlPatches.cs +++ b/src/Patches/PlayerControlPatches.cs @@ -91,7 +91,7 @@ public static bool Prefix(PlayerControl __instance, PlayerControl target) if (!Utils.isHost) return true; // __instance.isKilling = true; - PlayerControl.LocalPlayer.RpcMurderPlayer(target, true); + __instance.RpcMurderPlayer(target, true); return false; } } diff --git a/src/Utilities/Utils.cs b/src/Utilities/Utils.cs index 3fbfb29c..bb5726ab 100644 --- a/src/Utilities/Utils.cs +++ b/src/Utilities/Utils.cs @@ -23,7 +23,8 @@ public static class Utils public static bool isLocalGame => AmongUsClient.Instance && AmongUsClient.Instance.NetworkMode == NetworkModes.LocalGame; public static bool isFreePlay => AmongUsClient.Instance && AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay; public static bool isPlayer => PlayerControl.LocalPlayer; - public static bool isHost = AmongUsClient.Instance && AmongUsClient.Instance.AmHost; + // This will check the host status every time it's accessed. + public static bool isHost => AmongUsClient.Instance && AmongUsClient.Instance.AmHost; public static bool isInGame => AmongUsClient.Instance && AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && isPlayer; public static bool isMeeting => MeetingHud.Instance; public static bool isMeetingVoting => isMeeting && MeetingHud.Instance.state is MeetingHud.VoteStates.Voted or MeetingHud.VoteStates.NotVoted; @@ -65,7 +66,8 @@ public static bool isVanished(NetworkedPlayerInfo playerInfo) { PhantomRole phantomRole = playerInfo.Role as PhantomRole; - if (phantomRole != null){ + if (phantomRole != null) + { return phantomRole.fading || phantomRole.isInvisible; } @@ -79,7 +81,8 @@ public static bool isValidTarget(NetworkedPlayerInfo target) bool fullRequirements = killAnyoneRequirements && !target.IsDead && !target.Object.inVent && !target.Object.inMovingPlat && target.Role.CanBeKilled; - if (CheatToggles.killAnyone){ + if (CheatToggles.killAnyone) + { return killAnyoneRequirements; } @@ -89,39 +92,36 @@ public static bool isValidTarget(NetworkedPlayerInfo target) // Adjusts HUD resolution // Used to fix UI problems when zooming out - public static void adjustResolution() { + public static void adjustResolution() + { ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen); } // Get RoleBehaviour from a RoleType - public static RoleBehaviour getBehaviourByRoleType(RoleTypes roleType) { + public static RoleBehaviour getBehaviourByRoleType(RoleTypes roleType) + { return RoleManager.Instance.AllRoles.First(r => r.Role == roleType); } - // Kill any player using RPC calls + // ================================================================== + // [FIX 2] Replaced the body of murderPlayer. + // This now uses the game's standard method, which will be intercepted + // by your PlayerControl_CmdCheckMurder patch. This is the correct and + // more stable way to implement host-powered kills. public static void murderPlayer(PlayerControl target, MurderResultFlags result) { - if (isFreePlay){ - - PlayerControl.LocalPlayer.MurderPlayer(target, MurderResultFlags.Succeeded); - return; - - } + if (target == null) return; - foreach (var item in PlayerControl.AllPlayerControls) - { - MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)RpcCalls.MurderPlayer, SendOption.None, AmongUsClient.Instance.GetClientIdFromCharacter(item)); - writer.WriteNetObject(target); - writer.Write((int)result); - AmongUsClient.Instance.FinishRpcImmediately(writer); - } + PlayerControl.LocalPlayer.MurderPlayer(target, result); } + // ================================================================== // Report bodies using RPC calls public static void reportDeadBody(NetworkedPlayerInfo playerData) { - if (isFreePlay){ + if (isFreePlay) + { PlayerControl.LocalPlayer.CmdReportDeadBody(playerData); return; @@ -142,7 +142,8 @@ public static void reportDeadBody(NetworkedPlayerInfo playerData) public static void completeMyTasks() { - if (isFreePlay){ + if (isFreePlay) + { foreach (PlayerTask task in PlayerControl.LocalPlayer.myTasks) { @@ -157,7 +158,8 @@ public static void completeMyTasks() { foreach (PlayerTask task in PlayerControl.LocalPlayer.myTasks) { - if (!task.IsComplete){ + if (!task.IsComplete) + { foreach (var item in PlayerControl.AllPlayerControls) { From 74b8e0dff694ddef2454913062746ce4aeee880a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Astral=E2=9C=A8?= <90265231+astra1dev@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:07:12 +0000 Subject: [PATCH 03/11] Revert "Fix host detection" This reverts commit 4182eef326434ef7de32cd6236d5e1627a3b235c. --- src/Patches/PlayerControlPatches.cs | 2 +- src/Utilities/Utils.cs | 46 ++++++++++++++--------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/Patches/PlayerControlPatches.cs b/src/Patches/PlayerControlPatches.cs index 40951663..3d303351 100644 --- a/src/Patches/PlayerControlPatches.cs +++ b/src/Patches/PlayerControlPatches.cs @@ -91,7 +91,7 @@ public static bool Prefix(PlayerControl __instance, PlayerControl target) if (!Utils.isHost) return true; // __instance.isKilling = true; - __instance.RpcMurderPlayer(target, true); + PlayerControl.LocalPlayer.RpcMurderPlayer(target, true); return false; } } diff --git a/src/Utilities/Utils.cs b/src/Utilities/Utils.cs index bb5726ab..3fbfb29c 100644 --- a/src/Utilities/Utils.cs +++ b/src/Utilities/Utils.cs @@ -23,8 +23,7 @@ public static class Utils public static bool isLocalGame => AmongUsClient.Instance && AmongUsClient.Instance.NetworkMode == NetworkModes.LocalGame; public static bool isFreePlay => AmongUsClient.Instance && AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay; public static bool isPlayer => PlayerControl.LocalPlayer; - // This will check the host status every time it's accessed. - public static bool isHost => AmongUsClient.Instance && AmongUsClient.Instance.AmHost; + public static bool isHost = AmongUsClient.Instance && AmongUsClient.Instance.AmHost; public static bool isInGame => AmongUsClient.Instance && AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started && isPlayer; public static bool isMeeting => MeetingHud.Instance; public static bool isMeetingVoting => isMeeting && MeetingHud.Instance.state is MeetingHud.VoteStates.Voted or MeetingHud.VoteStates.NotVoted; @@ -66,8 +65,7 @@ public static bool isVanished(NetworkedPlayerInfo playerInfo) { PhantomRole phantomRole = playerInfo.Role as PhantomRole; - if (phantomRole != null) - { + if (phantomRole != null){ return phantomRole.fading || phantomRole.isInvisible; } @@ -81,8 +79,7 @@ public static bool isValidTarget(NetworkedPlayerInfo target) bool fullRequirements = killAnyoneRequirements && !target.IsDead && !target.Object.inVent && !target.Object.inMovingPlat && target.Role.CanBeKilled; - if (CheatToggles.killAnyone) - { + if (CheatToggles.killAnyone){ return killAnyoneRequirements; } @@ -92,36 +89,39 @@ public static bool isValidTarget(NetworkedPlayerInfo target) // Adjusts HUD resolution // Used to fix UI problems when zooming out - public static void adjustResolution() - { + public static void adjustResolution() { ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen); } // Get RoleBehaviour from a RoleType - public static RoleBehaviour getBehaviourByRoleType(RoleTypes roleType) - { + public static RoleBehaviour getBehaviourByRoleType(RoleTypes roleType) { return RoleManager.Instance.AllRoles.First(r => r.Role == roleType); } - // ================================================================== - // [FIX 2] Replaced the body of murderPlayer. - // This now uses the game's standard method, which will be intercepted - // by your PlayerControl_CmdCheckMurder patch. This is the correct and - // more stable way to implement host-powered kills. + // Kill any player using RPC calls public static void murderPlayer(PlayerControl target, MurderResultFlags result) { - if (target == null) return; + if (isFreePlay){ + + PlayerControl.LocalPlayer.MurderPlayer(target, MurderResultFlags.Succeeded); + return; + + } - PlayerControl.LocalPlayer.MurderPlayer(target, result); + foreach (var item in PlayerControl.AllPlayerControls) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)RpcCalls.MurderPlayer, SendOption.None, AmongUsClient.Instance.GetClientIdFromCharacter(item)); + writer.WriteNetObject(target); + writer.Write((int)result); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } } - // ================================================================== // Report bodies using RPC calls public static void reportDeadBody(NetworkedPlayerInfo playerData) { - if (isFreePlay) - { + if (isFreePlay){ PlayerControl.LocalPlayer.CmdReportDeadBody(playerData); return; @@ -142,8 +142,7 @@ public static void reportDeadBody(NetworkedPlayerInfo playerData) public static void completeMyTasks() { - if (isFreePlay) - { + if (isFreePlay){ foreach (PlayerTask task in PlayerControl.LocalPlayer.myTasks) { @@ -158,8 +157,7 @@ public static void completeMyTasks() { foreach (PlayerTask task in PlayerControl.LocalPlayer.myTasks) { - if (!task.IsComplete) - { + if (!task.IsComplete){ foreach (var item in PlayerControl.AllPlayerControls) { From 9683f5ca89c45c1ccb4af7b183e58a540bc4d68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Astral=E2=9C=A8?= <90265231+astra1dev@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:27:56 +0000 Subject: [PATCH 04/11] fix merge conflicts part 1 --- src/Patches/PlayerControlPatches.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Patches/PlayerControlPatches.cs b/src/Patches/PlayerControlPatches.cs index 3d303351..c349fa6a 100644 --- a/src/Patches/PlayerControlPatches.cs +++ b/src/Patches/PlayerControlPatches.cs @@ -99,7 +99,10 @@ public static bool Prefix(PlayerControl __instance, PlayerControl target) [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.TurnOnProtection))] public static class PlayerControl_TurnOnProtection { - // Prefix patch of PlayerControl.ProtectPlayer to render all protections visible if CheatToggles.seeGhosts is enabled + /// + /// Prefix patch of PlayerControl.TurnOnProtection to render all protections visible if CheatToggles.seeGhosts is enabled + /// + /// Whether the protection should be visible. public static void Prefix(ref bool visible){ if (CheatToggles.seeGhosts){ visible = true; From ad6e5c0049910b9ac74b29ed89dc2b85ae6e3db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Astral=E2=9C=A8?= <90265231+astra1dev@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:44:27 +0000 Subject: [PATCH 05/11] Revert "fix merge conflicts part 1" This reverts commit 9683f5ca89c45c1ccb4af7b183e58a540bc4d68f. --- src/Patches/PlayerControlPatches.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Patches/PlayerControlPatches.cs b/src/Patches/PlayerControlPatches.cs index c349fa6a..3d303351 100644 --- a/src/Patches/PlayerControlPatches.cs +++ b/src/Patches/PlayerControlPatches.cs @@ -99,10 +99,7 @@ public static bool Prefix(PlayerControl __instance, PlayerControl target) [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.TurnOnProtection))] public static class PlayerControl_TurnOnProtection { - /// - /// Prefix patch of PlayerControl.TurnOnProtection to render all protections visible if CheatToggles.seeGhosts is enabled - /// - /// Whether the protection should be visible. + // Prefix patch of PlayerControl.ProtectPlayer to render all protections visible if CheatToggles.seeGhosts is enabled public static void Prefix(ref bool visible){ if (CheatToggles.seeGhosts){ visible = true; From 6c4756d5c97dccbab5ee01f6d3f21dd9384ca86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Astral=E2=9C=A8?= <90265231+astra1dev@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:45:17 +0000 Subject: [PATCH 06/11] Revert "Notifications" This reverts commit d2c3b779cc4261b0bd926ab6b8b704f0c108e353. --- src/Cheats/NotificationHandler.cs | 125 ---------------------------- src/MalumMenu.cs | 2 - src/Patches/MeetingHudPatches.cs | 1 - src/Patches/OtherPatches.cs | 46 ---------- src/Patches/PlayerControlPatches.cs | 49 +---------- src/Patches/PlayerPhysicsPatches.cs | 45 ++-------- src/UI/CheatToggles.cs | 6 -- src/UI/MenuUI.cs | 9 +- src/UI/NotificationUI.cs | 54 ------------ src/Utilities/Utils.cs | 55 +++++------- 10 files changed, 30 insertions(+), 362 deletions(-) delete mode 100644 src/Cheats/NotificationHandler.cs delete mode 100644 src/UI/NotificationUI.cs diff --git a/src/Cheats/NotificationHandler.cs b/src/Cheats/NotificationHandler.cs deleted file mode 100644 index e10c5d10..00000000 --- a/src/Cheats/NotificationHandler.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; -using AmongUs.GameOptions; - -namespace MalumMenu; - -/// -/// Handles the creation and storage of event-based notification logs. -/// -public static class NotificationHandler -{ - public static readonly List notificationLog = new(); - private const int MaxLogEntries = 100; - - private static void AddLog(string message) - { - if (notificationLog.Count >= MaxLogEntries) - { - notificationLog.RemoveAt(0); - } - notificationLog.Add(message); - - // Also show the notification on the bottom-left of the screen. - if (DestroyableSingleton.InstanceExists && HudManager.Instance.Notifier != null) - { - // We use AddDisconnectMessage as it's a simple way to show custom text. - HudManager.Instance.Notifier.AddDisconnectMessage(message); - } - } - - /// - /// Checks if a player is currently disguised and returns their real and displayed names. - /// - private static (string realName, string displayName, bool isDisguised) GetPlayerIdentity(PlayerControl player) - { - if (player == null || player.Data == null) return ("", "", false); - - // Get the real player's name and color from their permanent data - string realName = $"{player.Data.PlayerName}"; - - // Get the displayed (current outfit) name and color - string displayName = $"{player.CurrentOutfit.PlayerName}"; - - // A player is disguised if their currently displayed name is different from their real name. - // This is the most reliable way to check for shapeshifting on ALL clients, - // as the 'CurrentOutfit' is synchronized for rendering purposes, whereas specific - // role timers (like durationSecondsRemaining) might not be. - bool isDisguised = player.CurrentOutfit.PlayerName != player.Data.PlayerName; - - return (realName, displayName, isDisguised); - } - - public static void HandlePlayerKill(PlayerControl killer, PlayerControl victim) - { - if (!CheatToggles.notifyOnDeath || killer == null || victim == null) return; - - var (realKillerName, displayKillerName, isDisguised) = GetPlayerIdentity(killer); - string victimName = $"{victim.CurrentOutfit.PlayerName}"; - - PlainShipRoom room = Utils.getRoomFromPosition(victim.GetTruePosition()); - string roomName = room != null ? room.RoomId.ToString() : "an unknown location"; - - string message; - if (isDisguised) - { - message = $"{realKillerName} (as {displayKillerName}) killed {victimName} in {roomName}."; - } - else - { - message = $"{realKillerName} killed {victimName} in {roomName}."; - } - AddLog(message); - } - - public static void HandleGuardianAngelSave(PlayerControl killer, PlayerControl target) - { - if (!CheatToggles.notifyOnDeath || killer == null || target == null) return; - - var (realKillerName, displayKillerName, isDisguised) = GetPlayerIdentity(killer); - string targetName = $"{target.CurrentOutfit.PlayerName}"; - - PlainShipRoom room = Utils.getRoomFromPosition(target.GetTruePosition()); - string roomName = room != null ? room.RoomId.ToString() : "an unknown location"; - - string message; - if (isDisguised) - { - message = $"{realKillerName} (as {displayKillerName}) tried to kill {targetName} in {roomName}. (Saved)"; - } - else - { - message = $"{realKillerName} tried to kill {targetName} in {roomName}. (Saved)"; - } - AddLog(message); - } - - public static void HandleVent(PlayerControl player, bool entered, string roomName) - { - if (!CheatToggles.notifyOnVent || player == null) return; - - var (realPlayerName, displayPlayerName, isDisguised) = GetPlayerIdentity(player); - string action = entered ? "entered" : "exited"; - - string message; - if (isDisguised) - { - message = $"{realPlayerName} (as {displayPlayerName}) has {action} a vent in {roomName}."; - } - else - { - message = $"{realPlayerName} has {action} a vent in {roomName}."; - } - AddLog(message); - } - - public static void HandlePlayerDisconnect(NetworkedPlayerInfo player) - { - if (!CheatToggles.notifyOnDisconnect || player == null) return; - - string playerName = $"{player.PlayerName}"; - - string message = $"{playerName} has disconnected."; - AddLog(message); - } -} \ No newline at end of file diff --git a/src/MalumMenu.cs b/src/MalumMenu.cs index 3b0ddcab..569663a1 100644 --- a/src/MalumMenu.cs +++ b/src/MalumMenu.cs @@ -18,7 +18,6 @@ public partial class MalumMenu : BasePlugin public static string malumVersion = "2.5.1"; public static List supportedAU = ["2025.3.25", "2025.3.31", "2025.6.10"]; public static MenuUI menuUI; - public static NotificationUI notificationUI; // public static ConsoleUI consoleUI; public static ConfigEntry menuKeybind; public static ConfigEntry menuHtmlColor; @@ -81,7 +80,6 @@ public override void Load() Harmony.PatchAll(); menuUI = AddComponent(); - notificationUI = AddComponent(); // consoleUI = AddComponent(); // Disable Telemetry (haven't fully tested if it works, but according to Unity docs it should) diff --git a/src/Patches/MeetingHudPatches.cs b/src/Patches/MeetingHudPatches.cs index 9e813907..275c31da 100644 --- a/src/Patches/MeetingHudPatches.cs +++ b/src/Patches/MeetingHudPatches.cs @@ -96,6 +96,5 @@ public static void Prefix(MeetingHud __instance) voteSpreader.Votes.Clear(); } MeetingHud_Update.votedPlayers.Clear(); - PlayerPhysics_LateUpdate.ClearAllStates(); } } \ No newline at end of file diff --git a/src/Patches/OtherPatches.cs b/src/Patches/OtherPatches.cs index 1e1c448c..151b61a2 100644 --- a/src/Patches/OtherPatches.cs +++ b/src/Patches/OtherPatches.cs @@ -4,7 +4,6 @@ using UnityEngine; using System; using System.Security.Cryptography; -using System.Collections.Generic; namespace MalumMenu; @@ -20,51 +19,6 @@ public static void Prefix(PlatformSpecificData __instance) } } -[HarmonyPatch(typeof(GameData), nameof(GameData.RemovePlayer))] -public static class GameData_RemovePlayer_Patch -{ - private static readonly HashSet notifiedDisconnects = new(); - - public static void ClearNotifiedDisconnects() => notifiedDisconnects.Clear(); - - // Use a Prefix patch to capture the PlayerInfo *before* it gets removed from the game's data lists. - public static void Prefix(GameData __instance, byte playerId) - { - // Only notify during an active game, not in lobby or post-game. - if (CheatToggles.notifyOnDisconnect && Utils.isInGame) - { - // If we've already notified for this player, don't do it again. - if (notifiedDisconnects.Contains(playerId)) - { - return; - } - - var player = __instance.GetPlayerById(playerId); - // The check for `!player.Disconnected` was preventing this from ever firing. It's removed. - if (player != null) - { - NotificationHandler.HandlePlayerDisconnect(player); - // Add the player to the set so we don't notify again for this game session. - notifiedDisconnects.Add(playerId); - } - } - } -} - -[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))] -public static class AmongUsClient_OnGameEnd_Patch -{ - public static void Postfix() - { - // Clear the set of notified disconnected players when a game ends. - GameData_RemovePlayer_Patch.ClearNotifiedDisconnects(); - - // Clear the set of notified killed victims when a game ends. - PlayerControl_MurderPlayer_Patch.ClearNotifiedKilledVictims(); - } -} - - [HarmonyPatch(typeof(FreeChatInputField), nameof(FreeChatInputField.UpdateCharCount))] public static class FreeChatInputField_UpdateCharCount { diff --git a/src/Patches/PlayerControlPatches.cs b/src/Patches/PlayerControlPatches.cs index 3d303351..1ed928cc 100644 --- a/src/Patches/PlayerControlPatches.cs +++ b/src/Patches/PlayerControlPatches.cs @@ -1,5 +1,4 @@ using HarmonyLib; -using System.Collections.Generic; namespace MalumMenu; @@ -15,50 +14,6 @@ public static void Postfix(PlayerControl __instance){ } } -[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.MurderPlayer))] -public static class PlayerControl_MurderPlayer_Patch -{ - // A HashSet to track victims for whom a notification has already been sent on this client. - // This prevents duplicate notifications if the event is somehow triggered more than once. - private static readonly HashSet notifiedKilledVictims = new(); - - /// - /// Clears the set of notified victims. This must be called at the end of each game. - /// - public static void ClearNotifiedKilledVictims() => notifiedKilledVictims.Clear(); - - // A Prefix runs *before* the original method. This lets us check conditions before the kill happens. - public static void Prefix(PlayerControl __instance, PlayerControl target) - { - if (target == null) - { - return; - } - - // Check if the target is protected by a Guardian Angel. - if (target.protectedByGuardianId != -1) - { - // This is a "save" event. Show the notification but do not add the player to the - // notifiedKilledVictims set, allowing a future kill notification to appear. - NotificationHandler.HandleGuardianAngelSave(__instance, target); - } - else - { - // This is a potential kill event. Check if we've already notified for this victim's death. - if (notifiedKilledVictims.Contains(target.PlayerId)) - { - return; - } - - // If not protected and not already notified, the kill is successful. - NotificationHandler.HandlePlayerKill(__instance, target); - - // Add the victim's ID to the set ONLY on a successful kill to prevent duplicate kill notifications. - notifiedKilledVictims.Add(target.PlayerId); - } - } -} - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdCheckMurder))] public static class PlayerControl_CmdCheckMurder { @@ -99,9 +54,9 @@ public static bool Prefix(PlayerControl __instance, PlayerControl target) [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.TurnOnProtection))] public static class PlayerControl_TurnOnProtection { - // Prefix patch of PlayerControl.ProtectPlayer to render all protections visible if CheatToggles.seeGhosts is enabled + // Prefix patch of PlayerControl.ProtectPlayer to render all protections visible if CheatToggles.seeGhosts is enabled public static void Prefix(ref bool visible){ - if (CheatToggles.seeGhosts){ + if (CheatToggles.seeGhosts){ visible = true; } } diff --git a/src/Patches/PlayerPhysicsPatches.cs b/src/Patches/PlayerPhysicsPatches.cs index 14d4b975..b7af6405 100644 --- a/src/Patches/PlayerPhysicsPatches.cs +++ b/src/Patches/PlayerPhysicsPatches.cs @@ -1,48 +1,13 @@ using HarmonyLib; using UnityEngine; -using System.Collections.Generic; namespace MalumMenu; [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.LateUpdate))] public static class PlayerPhysics_LateUpdate { - private static readonly Dictionary wasInVent = new(); - public static readonly Dictionary lastKnownPositions = new(); - - public static void ClearAllStates() - { - wasInVent.Clear(); - lastKnownPositions.Clear(); - } - public static void Postfix(PlayerPhysics __instance) { - if (__instance.myPlayer != null && !__instance.myPlayer.Data.IsDead) - { - // Update the player's last known position every frame they are not in a vent. - if (!__instance.myPlayer.inVent) - { - lastKnownPositions[__instance.myPlayer.PlayerId] = __instance.myPlayer.GetTruePosition(); - } - - // Vent usage detection - if (CheatToggles.notifyOnVent && Utils.isInGame) - { - byte playerId = __instance.myPlayer.PlayerId; - bool currentlyInVent = __instance.myPlayer.inVent; - - if (wasInVent.TryGetValue(playerId, out bool previouslyInVent) && currentlyInVent != previouslyInVent) - { - Vector2 positionToCheck = currentlyInVent ? lastKnownPositions[playerId] : __instance.myPlayer.GetTruePosition(); - PlainShipRoom room = Utils.getRoomFromPosition(positionToCheck); - string roomName = room != null ? room.RoomId.ToString() : "an unknown location"; - - NotificationHandler.HandleVent(__instance.myPlayer, currentlyInVent, roomName); - } - wasInVent[playerId] = currentlyInVent; - } - } MalumESP.playerNametags(__instance); MalumESP.seeGhostsCheat(__instance); @@ -72,12 +37,14 @@ public static void Postfix(PlayerPhysics __instance) TracersHandler.drawPlayerTracer(__instance); GameObject[] bodyObjects = GameObject.FindGameObjectsWithTag("DeadBody"); - foreach(GameObject bodyObject in bodyObjects) + foreach(GameObject bodyObject in bodyObjects) // Finds and loops through all dead bodies { DeadBody deadBody = bodyObject.GetComponent(); - if (deadBody && !deadBody.Reported) - { - TracersHandler.drawBodyTracer(deadBody); + + if (deadBody){ + if (!deadBody.Reported){ // Only draw tracers for unreported dead bodies + TracersHandler.drawBodyTracer(deadBody); + } } } } diff --git a/src/UI/CheatToggles.cs b/src/UI/CheatToggles.cs index d6c46399..b4cb3d15 100644 --- a/src/UI/CheatToggles.cs +++ b/src/UI/CheatToggles.cs @@ -86,12 +86,6 @@ public struct CheatToggles //public static bool voteImmune; public static bool noOptionsLimits; - //Notifications - public static bool notifyOnDeath; - public static bool notifyOnDisconnect; - public static bool notifyOnVent; - public static bool showNotificationLog; - //Passive public static bool unlockFeatures; public static bool freeCosmetics; diff --git a/src/UI/MenuUI.cs b/src/UI/MenuUI.cs index 4f9ccedc..86dce37d 100644 --- a/src/UI/MenuUI.cs +++ b/src/UI/MenuUI.cs @@ -141,7 +141,7 @@ private void Start() new ToggleInfo(" Enable Chat", () => CheatToggles.alwaysChat, x => CheatToggles.alwaysChat = x), new ToggleInfo(" Unlock Textbox", () => CheatToggles.chatJailbreak, x => CheatToggles.chatJailbreak = x) ], [])); - + // Host-Only cheats are temporarly disabled because of some bugs //groups.Add(new GroupInfo("Host-Only", false, new List() { @@ -181,13 +181,6 @@ private void Start() ])); - groups.Add(new GroupInfo("Notifications", false, [ - new ToggleInfo(" On Player Death", () => CheatToggles.notifyOnDeath, x => CheatToggles.notifyOnDeath = x), - new ToggleInfo(" On Player Disconnect", () => CheatToggles.notifyOnDisconnect, x => CheatToggles.notifyOnDisconnect = x), - new ToggleInfo(" On Vent Usage", () => CheatToggles.notifyOnVent, x => CheatToggles.notifyOnVent = x), - new ToggleInfo(" Show Notification Log", () => CheatToggles.showNotificationLog, x => CheatToggles.showNotificationLog = x) - ], [])); - groups.Add(new GroupInfo("Passive", false, [ new ToggleInfo(" Free Cosmetics", () => CheatToggles.freeCosmetics, x => CheatToggles.freeCosmetics = x), new ToggleInfo(" Avoid Penalties", () => CheatToggles.avoidBans, x => CheatToggles.avoidBans = x), diff --git a/src/UI/NotificationUI.cs b/src/UI/NotificationUI.cs deleted file mode 100644 index 0265709d..00000000 --- a/src/UI/NotificationUI.cs +++ /dev/null @@ -1,54 +0,0 @@ -using UnityEngine; - -namespace MalumMenu; - -/// -/// Renders the Notification Log window using OnGUI. -/// -public class NotificationUI : MonoBehaviour -{ - private Vector2 scrollPosition = Vector2.zero; - private Rect windowRect = new Rect(320, 10, 500, 300); - private GUIStyle logStyle; - - private void OnGUI() - { - if (!CheatToggles.showNotificationLog) return; - - if (logStyle == null){ - logStyle = new GUIStyle(GUI.skin.label) - { - fontSize = 16, - richText = true // Essential for colored names - }; - } - - Color configUIColor; - if (ColorUtility.TryParseHtmlString(MalumMenu.menuHtmlColor.Value, out configUIColor)){ - GUI.backgroundColor = configUIColor; - } - - windowRect = GUI.Window(2, windowRect, (GUI.WindowFunction)NotificationWindow, "Notification Log"); - } - - private void NotificationWindow(int windowID) - { - GUILayout.BeginVertical(); - scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true); - - foreach (var log in NotificationHandler.notificationLog) - { - GUILayout.Label(log, logStyle); - } - - GUILayout.EndScrollView(); - - if (GUILayout.Button("Clear Log")){ - NotificationHandler.notificationLog.Clear(); - } - - GUILayout.EndVertical(); - - GUI.DragWindow(); - } -} \ No newline at end of file diff --git a/src/Utilities/Utils.cs b/src/Utilities/Utils.cs index 3fbfb29c..67b4fa56 100644 --- a/src/Utilities/Utils.cs +++ b/src/Utilities/Utils.cs @@ -84,7 +84,7 @@ public static bool isValidTarget(NetworkedPlayerInfo target) } return fullRequirements; - + } // Adjusts HUD resolution @@ -105,7 +105,7 @@ public static void murderPlayer(PlayerControl target, MurderResultFlags result) PlayerControl.LocalPlayer.MurderPlayer(target, MurderResultFlags.Succeeded); return; - + } foreach (var item in PlayerControl.AllPlayerControls) @@ -125,7 +125,7 @@ public static void reportDeadBody(NetworkedPlayerInfo playerData) PlayerControl.LocalPlayer.CmdReportDeadBody(playerData); return; - + } var HostData = AmongUsClient.Instance.GetHost(); @@ -149,7 +149,7 @@ public static void completeMyTasks() PlayerControl.LocalPlayer.RpcCompleteTask(task.Id); } return; - + } var HostData = AmongUsClient.Instance.GetHost(); @@ -205,7 +205,7 @@ public static void drawTracer(GameObject sourceObject, GameObject targetObject, lineRenderer.material = material; lineRenderer.SetColors(color, color); - + lineRenderer.SetPosition(0, sourceObject.transform.position); lineRenderer.SetPosition(1, targetObject.transform.position); } @@ -231,13 +231,13 @@ public static void closeChat() // Get the distance between two players as a float public static float getDistanceFrom(PlayerControl target, PlayerControl source = null){ - + if (source.IsNull()){ source = PlayerControl.LocalPlayer; } Vector2 vector = target.GetTruePosition() - source.GetTruePosition(); - float magnitude = vector.magnitude; + float magnitude = vector.magnitude; return magnitude; @@ -245,7 +245,7 @@ public static float getDistanceFrom(PlayerControl target, PlayerControl source = // Returns a list of all the players in the game ordered from closest to farthest (from LocalPlayer by default) public static System.Collections.Generic.List getPlayersSortedByDistance(PlayerControl source = null){ - + if (source.IsNull()){ source = PlayerControl.LocalPlayer; } @@ -263,9 +263,9 @@ public static System.Collections.Generic.List getPlayersSortedByD outputList.Add(player); } } - + outputList = outputList.OrderBy(target => getDistanceFrom(target, source)).ToList(); - + if (outputList.Count <= 0) { return null; @@ -280,10 +280,10 @@ public static byte getCurrentMapID() { // If playing the tutorial if (isFreePlay) - { + { return (byte)AmongUsClient.Instance.TutorialMapId; - }else{ + }else{ // Works for local/online games return GameOptionsManager.Instance.currentGameOptions.MapId; } @@ -294,19 +294,6 @@ public static SystemTypes getCurrentRoom(){ return HudManager.Instance.roomTracker.LastRoom.RoomId; } - // Gets the room object from a Vector2 position. - public static PlainShipRoom getRoomFromPosition(Vector2 position){ - if (ShipStatus.Instance == null) return null; - - foreach (var room in ShipStatus.Instance.AllRooms) - { - if (room != null && room.roomArea != null && room.roomArea.OverlapPoint(position)){ - return room; - } - } - return null; - } - // Fancy colored ping text public static string getColoredPingText(int ping){ @@ -330,14 +317,14 @@ public static KeyCode stringToKeycode(string keyCodeStr){ if(!string.IsNullOrEmpty(keyCodeStr)){ // Empty strings are automatically invalid try{ - + // Case-insensitive parse of UnityEngine.KeyCode to check if string is validssss KeyCode keyCode = (KeyCode)System.Enum.Parse(typeof(KeyCode), keyCodeStr, true); - + return keyCode; }catch{} - + } return KeyCode.Delete; // If string is invalid, return Delete as the default key @@ -349,14 +336,14 @@ public static bool stringToPlatformType(string platformStr, out Platforms? platf if(!string.IsNullOrEmpty(platformStr)){ // Empty strings are automatically invalid try{ - + // Case-insensitive parse of Platforms from string (if it valid) platform = (Platforms)System.Enum.Parse(typeof(Platforms), platformStr, true); - + return true; // If platform type is valid, return false }catch{} - + } platform = null; @@ -394,7 +381,7 @@ public static string getNameTag(NetworkedPlayerInfo playerInfo, string playerNam } nameTag = $"{getRoleName(playerInfo)}\r\n{nameTag}"; - + } else if (PlayerControl.LocalPlayer.Data.Role.NameColor == playerInfo.Role.NameColor){ if (isChat){ @@ -413,7 +400,7 @@ public static string getNameTag(NetworkedPlayerInfo playerInfo, string playerNam // Found here: https://github.com/NuclearPowered/Reactor/blob/6eb0bf19c30733b78532dada41db068b2b247742/Reactor/Networking/Patches/HttpPatches.cs public static void showPopup(string text){ var popup = Object.Instantiate(DiscordManager.Instance.discordPopup, Camera.main!.transform); - + var background = popup.transform.Find("Background").GetComponent(); var size = background.size; size.x *= 2.5f; @@ -451,7 +438,7 @@ public static Texture2D LoadTextureFromResources(string path) var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path); var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); using MemoryStream ms = new(); - + stream.CopyTo(ms); ImageConversion.LoadImage(texture, ms.ToArray(), false); return texture; From 67e64451bea2fa1464977b3b32cbbe479b79bace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Astral=E2=9C=A8?= <90265231+astra1dev@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:09:00 +0000 Subject: [PATCH 07/11] feat: enable and improve ConsoleUI --- src/MalumMenu.cs | 4 ++-- src/UI/ConsoleUI.cs | 56 +++++++++++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/MalumMenu.cs b/src/MalumMenu.cs index a002660f..68d57fcc 100644 --- a/src/MalumMenu.cs +++ b/src/MalumMenu.cs @@ -20,7 +20,7 @@ public partial class MalumMenu : BasePlugin public static string malumVersion = "2.6.1"; public static List supportedAU = ["2025.9.9", "2025.10.14", "2025.11.18"]; public static MenuUI menuUI; - // public static ConsoleUI consoleUI; + public static ConsoleUI consoleUI; public static RolesUI rolesUI; public static DoorsUI doorsUI; public static TasksUI tasksUI; @@ -100,7 +100,7 @@ public override void Load() Harmony.PatchAll(); menuUI = AddComponent(); - // consoleUI = AddComponent(); + consoleUI = AddComponent(); rolesUI = AddComponent(); doorsUI = AddComponent(); tasksUI = AddComponent (); diff --git a/src/UI/ConsoleUI.cs b/src/UI/ConsoleUI.cs index cca4fffd..ffd0c1c9 100644 --- a/src/UI/ConsoleUI.cs +++ b/src/UI/ConsoleUI.cs @@ -1,58 +1,70 @@ +using Il2CppSystem; using UnityEngine; -using Il2CppSystem.Collections.Generic; +using System.Collections.Generic; namespace MalumMenu; public class ConsoleUI : MonoBehaviour { - public bool isVisible = false; - private Vector2 scrollPosition = Vector2.zero; - private static List logEntries = new(); - private const int MaxLogEntries = 100; - private Rect windowRect = new Rect(320, 10, 500, 300); // Adjust size and position as needed - private GUIStyle logStyle; - - public void Log(string message) + private static Vector2 _scrollPosition = Vector2.zero; + private static List _logEntries = new(); + private const int MaxLogEntries = 300; + private Rect _windowRect = new(320, 10, 500, 300); + private GUIStyle _logStyle; + + public static void Log(string message) { - if (logEntries.Count >= MaxLogEntries) // Limit the number of logs to keep memory usage in check + if (_logEntries.Count >= MaxLogEntries) // Limit the number of logs to keep memory usage in check { - logEntries.RemoveAt(0); // Remove the oldest log entry + _logEntries.RemoveAt(0); // Remove the oldest log entry } - logEntries.Add(message); + _logEntries.Add(message); // Scroll to the bottom - scrollPosition.y = float.MaxValue; + _scrollPosition.y = float.MaxValue; } private void OnGUI() { + if (!CheatToggles.showConsole) return; - if (!isVisible) return; - - logStyle ??= new GUIStyle(GUI.skin.label) + _logStyle ??= new GUIStyle(GUI.skin.label) { - fontSize = 20 + fontSize = 16 }; - if(ColorUtility.TryParseHtmlString(MalumMenu.menuHtmlColor.Value, out var configUIColor)){ + if(ColorUtility.TryParseHtmlString(MalumMenu.menuHtmlColor.Value, out var configUIColor)) + { GUI.backgroundColor = configUIColor; } - windowRect = GUI.Window(1, windowRect, (GUI.WindowFunction)ConsoleWindow, "MalumConsole"); + _windowRect = GUI.Window(1, _windowRect, (GUI.WindowFunction)ConsoleWindow, "MalumConsole"); } private void ConsoleWindow(int windowID) { GUILayout.BeginVertical(); - scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true); + _scrollPosition = GUILayout.BeginScrollView(_scrollPosition, false, true); - foreach (var log in logEntries) + foreach (var log in _logEntries) { - GUILayout.Label(log, logStyle); // Use the custom GUIStyle with the specified font size + GUILayout.Label(log, _logStyle); } GUILayout.EndScrollView(); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Clear Log", GUILayout.Width(235))) + { + _logEntries.Clear(); + } + if (GUILayout.Button("Copy Log to clipboard")) + { + GUIUtility.systemCopyBuffer = String.Join("\n", _logEntries.ToArray()); + } + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); GUI.DragWindow(); From 966347e4888110346a2d922271a0ead6e842986e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Astral=E2=9C=A8?= <90265231+astra1dev@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:13:52 +0000 Subject: [PATCH 08/11] feat: add new toggles for notifications --- src/UI/CheatToggles.cs | 6 ++++++ src/UI/MenuUI.cs | 11 ++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/UI/CheatToggles.cs b/src/UI/CheatToggles.cs index 72ed3f72..9a123970 100644 --- a/src/UI/CheatToggles.cs +++ b/src/UI/CheatToggles.cs @@ -80,6 +80,12 @@ public struct CheatToggles public static bool alwaysChat; public static bool chatJailbreak; + // Console + public static bool showConsole; + public static bool logDeaths; + public static bool logShapeshifts; + public static bool logVents; + //Ship public static bool closeMeeting; public static bool sabotageMap; diff --git a/src/UI/MenuUI.cs b/src/UI/MenuUI.cs index 9b903f1a..1ed42cf6 100644 --- a/src/UI/MenuUI.cs +++ b/src/UI/MenuUI.cs @@ -142,11 +142,12 @@ private void Start() new ToggleInfo(" Unlock Textbox", () => CheatToggles.chatJailbreak, x => CheatToggles.chatJailbreak = x) ], [])); - // Console is temporarly disabled until we implement some features for it - - //groups.Add(new GroupInfo("Console", false, new List() { - // new ToggleInfo(" ConsoleUI", () => MalumMenu.consoleUI.isVisible, x => MalumMenu.consoleUI.isVisible = x), - //}, new List())); + groups.Add(new GroupInfo("Console", false, [ + new ToggleInfo(" Show Console", () => CheatToggles.showConsole, x => CheatToggles.showConsole = x), + new ToggleInfo(" Log Deaths", () => CheatToggles.logDeaths, x => CheatToggles.logDeaths = x), + new ToggleInfo(" Log Shapeshifts", () => CheatToggles.logShapeshifts, x => CheatToggles.logShapeshifts = x), + new ToggleInfo(" Log Vents", () => CheatToggles.logVents, x => CheatToggles.logVents = x), + ], [])); groups.Add(new GroupInfo("Host-Only", false, [ From 0eb9fa7858af14d3cd7404328b1d08c349888442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Astral=E2=9C=A8?= <90265231+astra1dev@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:26:53 +0000 Subject: [PATCH 09/11] feat: add patches for logging vent events --- src/Patches/OtherPatches.cs | 23 ------------ src/Patches/VentPatches.cs | 75 +++++++++++++++++++++++++++++++++++++ src/Utilities/Utils.cs | 27 +++++++++++++ 3 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 src/Patches/VentPatches.cs diff --git a/src/Patches/OtherPatches.cs b/src/Patches/OtherPatches.cs index 3428209d..3d9d9049 100644 --- a/src/Patches/OtherPatches.cs +++ b/src/Patches/OtherPatches.cs @@ -298,29 +298,6 @@ public static void Prefix(Console __instance) } } -[HarmonyPatch(typeof(Vent), nameof(Vent.CanUse))] -public static class Vent_CanUse -{ - // Prefix patch of Vent.CanUse to allow venting for cheaters - // Basically does what the original method did with the required modifications - public static void Postfix(Vent __instance, NetworkedPlayerInfo pc, ref bool canUse, ref bool couldUse, ref float __result) - { - if (!PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data) return; - if (PlayerControl.LocalPlayer.Data.Role.CanVent || PlayerControl.LocalPlayer.Data.IsDead) return; - if (!CheatToggles.useVents) return; - var @object = pc.Object; - - var center = @object.Collider.bounds.center; - var position = __instance.transform.position; - var num = Vector2.Distance(center, position); - - // Allow usage of vents unless the vent is too far or there are objects blocking the player's path - canUse = num <= __instance.UsableDistance && !PhysicsHelpers.AnythingBetween(@object.Collider, center, position, Constants.ShipOnlyMask, false); - couldUse = true; - __result = num; - } -} - [HarmonyPatch(typeof(IntroCutscene), "CoBegin")] public static class IntroCutscene_CoBegin { diff --git a/src/Patches/VentPatches.cs b/src/Patches/VentPatches.cs new file mode 100644 index 00000000..fc8affe9 --- /dev/null +++ b/src/Patches/VentPatches.cs @@ -0,0 +1,75 @@ +using HarmonyLib; +using UnityEngine; + +namespace MalumMenu; + +[HarmonyPatch(typeof(Vent), nameof(Vent.CanUse))] +public static class Vent_CanUse +{ + /// + /// Postfix patch of Vent.CanUse to allow venting. + /// + /// The Vent instance. + /// The PlayerControl of the player trying to use the vent. + /// Whether the player can currently use the vent, accounting for distance and physics obstacles. + /// Whether the player's role and game state theoretically allow vent usage. + /// The distance from the player to the vent, or -1 if the vent cannot be used. + public static void Postfix(Vent __instance, NetworkedPlayerInfo pc, ref bool canUse, ref bool couldUse, ref float __result) + { + if (!PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data) return; + if (PlayerControl.LocalPlayer.Data.Role.CanVent || PlayerControl.LocalPlayer.Data.IsDead) return; + if (!CheatToggles.useVents) return; + var @object = pc.Object; + + var center = @object.Collider.bounds.center; + var position = __instance.transform.position; + var num = Vector2.Distance(center, position); + + // Allow usage of vents unless the vent is too far or there are objects blocking the player's path + canUse = num <= __instance.UsableDistance && !PhysicsHelpers.AnythingBetween(@object.Collider, center, position, Constants.ShipOnlyMask, false); + couldUse = true; + __result = num; + } +} + +[HarmonyPatch(typeof(Vent), nameof(Vent.EnterVent))] +public static class Vent_EnterVent +{ + /// + /// Postfix patch of Vent.EnterVent to log when a player enters a vent, along with the room they entered it in. + /// + /// The Vent instance. + /// The PlayerControl of the player entering the vent. + public static void Postfix(Vent __instance, PlayerControl pc) + { + if (!CheatToggles.logVents || !Utils.isShip) return; + + var (realPlayerName, displayPlayerName, isDisguised) = Utils.GetPlayerIdentity(pc); + var room = Utils.GetRoomFromPosition(__instance.transform.position - (Vector3) pc.Collider.offset); + var roomName = room != null ? room.RoomId.ToString() : "an unknown location"; + ConsoleUI.Log(isDisguised + ? $"{realPlayerName} (as {displayPlayerName}) entered a vent in {roomName}" + : $"{realPlayerName} entered a vent in {roomName}"); + } +} + +[HarmonyPatch(typeof(Vent), nameof(Vent.ExitVent))] +public static class Vent_ExitVent +{ + /// + /// Postfix patch of Vent.ExitVent to log when a player exits a vent, along with the room they exited it in. + /// + /// The Vent instance. + /// The PlayerControl of the player exiting the vent. + public static void Postfix(Vent __instance, PlayerControl pc) + { + if (!CheatToggles.logVents || !Utils.isShip) return; + + var (realPlayerName, displayPlayerName, isDisguised) = Utils.GetPlayerIdentity(pc); + var room = Utils.GetRoomFromPosition(__instance.transform.position - (Vector3) pc.Collider.offset); + var roomName = room != null ? room.RoomId.ToString() : "an unknown location"; + ConsoleUI.Log(isDisguised + ? $"{realPlayerName} (as {displayPlayerName}) exited a vent in {roomName}" + : $"{realPlayerName} exited a vent in {roomName}"); + } +} diff --git a/src/Utilities/Utils.cs b/src/Utilities/Utils.cs index 7b38a73a..e5d95995 100644 --- a/src/Utilities/Utils.cs +++ b/src/Utilities/Utils.cs @@ -98,6 +98,22 @@ public static int getClientIdByPlayer(PlayerControl player) return client == null ? -1 : client.Id; } + /// + /// Get a player's real name, display name, and whether they are disguised or not. + /// + /// The PlayerControl of the player to get the identity of. + /// A tuple containing the player's real name, display name, and whether they are disguised or not. + public static (string realName, string displayName, bool isDisguised) GetPlayerIdentity(PlayerControl player) + { + if (player == null || player.Data == null) return ("", "", false); + + var realName = $"{player.Data.PlayerName}"; + var displayName = $"{player.CurrentOutfit.PlayerName}"; + var isDisguised = player.CurrentOutfit.PlayerName != player.Data.PlayerName; + + return (realName, displayName, isDisguised); + } + // Check if player is currently vanished public static bool isVanished(NetworkedPlayerInfo playerInfo) { @@ -329,6 +345,17 @@ public static SystemTypes getCurrentRoom(){ return HudManager.Instance.roomTracker.LastRoom.RoomId; } + /// + /// Get the PlainShipRoom from a Vector2 position. + /// + /// The position to check for the room. + /// The PlainShipRoom at the given position, or null if none found. + public static PlainShipRoom GetRoomFromPosition(Vector2 position) + { + return ShipStatus.Instance == null ? null : ShipStatus.Instance.AllRooms.FirstOrDefault( + room => room != null && room.roomArea != null && room.roomArea.OverlapPoint(position)); + } + // Fancy colored ping text public static string getColoredPingText(int ping) { From b9dfa3b5d47b3f67478cf026bab2dfcf04f47b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Astral=E2=9C=A8?= <90265231+astra1dev@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:39:19 +0000 Subject: [PATCH 10/11] feat: add patches for logging kills and shapeshift events --- src/Patches/PlayerControlPatches.cs | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/Patches/PlayerControlPatches.cs b/src/Patches/PlayerControlPatches.cs index 8a9e4247..e30f4400 100644 --- a/src/Patches/PlayerControlPatches.cs +++ b/src/Patches/PlayerControlPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using UnityEngine; namespace MalumMenu; @@ -56,6 +57,37 @@ public static bool Prefix(PlayerControl __instance, PlayerControl target) } } +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.MurderPlayer))] +public static class PlayerControl_MurderPlayer +{ + /// + /// Prefix patch of PlayerControl.MurderPlayer to log when a player tries to kill another player, who the killer and target are, + /// and where the kill happened. Also logs when a kill gets saved by a guardian angel. + /// + /// The PlayerControl instance. + /// The player being killed. + public static void Prefix(PlayerControl __instance, PlayerControl target) + { + if (!CheatToggles.logDeaths || target == null) return; + + var (realKillerName, displayKillerName, isDisguised) = Utils.GetPlayerIdentity(__instance); + var targetName = $"{target.CurrentOutfit.PlayerName}"; + var room = Utils.GetRoomFromPosition(target.GetTruePosition()); + var roomName = room != null ? room.RoomId.ToString() : "an unknown location"; + + if (target.protectedByGuardianId != -1) + { + ConsoleUI.Log(isDisguised ? $"{realKillerName} (as {displayKillerName}) tried to kill {targetName} in {roomName} (Saved)" + : $"{realKillerName} tried to kill {targetName} in {roomName} (Saved)"); + } + else + { + ConsoleUI.Log(isDisguised ? $"{realKillerName} (as {displayKillerName}) killed {targetName} in {roomName}" + : $"{realKillerName} killed {targetName} in {roomName}"); + } + } +} + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.TurnOnProtection))] public static class PlayerControl_TurnOnProtection { @@ -100,6 +132,37 @@ public static void Prefix(ref bool shouldAnimate){ } } +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Shapeshift))] +public static class PlayerControl_Shapeshift +{ + /// + /// Postfix patch of PlayerControl.Shapeshift to log when a player shapeshifts into another player, + /// and who they shapeshifted into. Also logs when a shapeshift gets reverted. + /// + /// The PlayerControl instance. + /// The player that is being shapeshifted into. + /// Used in the original method to determine whether the shapeshift animation should play. + public static void Postfix(PlayerControl __instance, PlayerControl targetPlayer, bool animate) + { + if (!CheatToggles.logShapeshifts) return; + + if (__instance.CurrentOutfitType == PlayerOutfitType.MushroomMixup) return; + var targetPlayerInfo = targetPlayer.Data; + if (targetPlayerInfo.PlayerId == __instance.Data.PlayerId) + { + ConsoleUI.Log($"" + + $"{GameData.Instance.GetPlayerById(__instance.PlayerId)._object.Data.PlayerName} Shapeshift was reverted"); + } + else + { + ConsoleUI.Log($"" + + $"{GameData.Instance.GetPlayerById(__instance.PlayerId)._object.Data.PlayerName} shapeshifted into " + + $"" + + $"{GameData.Instance.GetPlayerById(targetPlayerInfo.PlayerId)._object.Data.PlayerName}"); + } + } +} + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcSyncSettings))] public static class PlayerControl_RpcSyncSettings { From 9e97244b05585185b79c4d899c4e25ccbb6f4d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Astral=E2=9C=A8?= <90265231+astra1dev@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:45:02 +0000 Subject: [PATCH 11/11] docs: add Console tab to changelist in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bfb31834..ccb7bc56 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Make sure you are only having one version of MalumMenu installed at a time, as h - **ESP**: Show Player Info, More Lobby Info, Show Task Arrows - **Roles**: Do Tasks as Impostor, Tasks Menu (to complete individual tasks and see other players' tasks), Track Reach, Interrogate Reach - **Ship**: Call Meeting, Open Sabotage Map, Trigger Spores ([#40](https://github.com/scp222thj/MalumMenu/pull/40)), Auto-Open Doors On Use, Doors Menu (to close / open individual doors) +- **Console** (NEW!): Show Console, Log Deaths, Log Shapeshifts, Log Vents - **Host-Only**: No Options Limits, Protect Player Menu, Force Role - **Meetings** (NEW!): Skip Meeting, VoteImmune, Eject Player - **Game State** (NEW!) ([#49](https://github.com/scp222thj/MalumMenu/pull/49)): Force Start Game, No Game End