From faec32d378c4d7d6208515c359019e8d68e756f7 Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:40:21 +0100 Subject: [PATCH 01/12] Update Utils.cs --- src/Utilities/Utils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utilities/Utils.cs b/src/Utilities/Utils.cs index a27fd9ed..beb45742 100644 --- a/src/Utilities/Utils.cs +++ b/src/Utilities/Utils.cs @@ -20,7 +20,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; - public static bool isHost = AmongUsClient.Instance && AmongUsClient.Instance.AmHost; + public static bool isHost => AmongUsClient.Instance && AmongUsClient.Instance.AmHost; // Check if is host every time instead of once 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; From 944275003c375b0cc7ac4d20c82afe401925a483 Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:47:13 +0100 Subject: [PATCH 02/12] Update ConsoleUI.cs --- src/UI/ConsoleUI.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/UI/ConsoleUI.cs b/src/UI/ConsoleUI.cs index 076f3c4d..4b188975 100644 --- a/src/UI/ConsoleUI.cs +++ b/src/UI/ConsoleUI.cs @@ -33,7 +33,8 @@ private void OnGUI() logStyle = new GUIStyle(GUI.skin.label) { - fontSize = 20 + fontSize = 20, + richText = true // Essential for colored names }; } @@ -57,6 +58,10 @@ private void ConsoleWindow(int windowID) GUILayout.Label(log, logStyle); // Use the custom GUIStyle with the specified font size } + if (GUILayout.Button("Clear Log")){ + logEntries.Clear(); + } + GUILayout.EndScrollView(); GUILayout.EndVertical(); From a77ae2e97ec275a90ec7b0b26f1af8246d3d03c5 Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:51:21 +0100 Subject: [PATCH 03/12] Create NotificationHandler.cs --- src/Utilities/NotificationHandler.cs | 119 +++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/Utilities/NotificationHandler.cs diff --git a/src/Utilities/NotificationHandler.cs b/src/Utilities/NotificationHandler.cs new file mode 100644 index 00000000..89c22e41 --- /dev/null +++ b/src/Utilities/NotificationHandler.cs @@ -0,0 +1,119 @@ +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 +{ + private const int MaxLogEntries = 100; + private static void AddLog(string message) + { + ConsoleUI.logEntries.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); + } +} From 95c408386e14c7682fb9f4006df8cefe0152bebc Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:51:44 +0100 Subject: [PATCH 04/12] Update ConsoleUI.cs --- src/UI/ConsoleUI.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/ConsoleUI.cs b/src/UI/ConsoleUI.cs index 4b188975..aa3973f2 100644 --- a/src/UI/ConsoleUI.cs +++ b/src/UI/ConsoleUI.cs @@ -7,7 +7,7 @@ public class ConsoleUI : MonoBehaviour { public bool isVisible = false; private Vector2 scrollPosition = Vector2.zero; - private List logEntries = new List(); + public static List logEntries = new List(); private Rect windowRect = new Rect(320, 10, 500, 300); // Adjust size and position as needed private GUIStyle logStyle; From 18671bd79ed4716c7b9bb2116de1abae25ebf87b Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:54:20 +0100 Subject: [PATCH 05/12] Update MenuUI.cs --- src/UI/MenuUI.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/UI/MenuUI.cs b/src/UI/MenuUI.cs index 03a79e97..135239a1 100644 --- a/src/UI/MenuUI.cs +++ b/src/UI/MenuUI.cs @@ -102,7 +102,7 @@ private void Start() groups.Add(new GroupInfo("Chat", false, new List() { new ToggleInfo(" Enable Chat", () => CheatToggles.alwaysChat, x => CheatToggles.alwaysChat = x), new ToggleInfo(" Unlock Textbox", () => CheatToggles.chatJailbreak, x => CheatToggles.chatJailbreak = x) - }, new List())); + }, [])); // Host-Only cheats are temporarly disabled because of some bugs @@ -113,12 +113,6 @@ private void Start() // new ToggleInfo(" VoteImmune", () => CheatSettings.voteImmune, x => CheatSettings.voteImmune = x) //}, new List())); - // 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("Host-Only", false, new List{ new ToggleInfo(" Kill While Vanished", () => CheatToggles.killVanished, x => CheatToggles.killVanished = x), @@ -139,7 +133,15 @@ private void Start() new ToggleInfo(" Free Cosmetics", () => CheatToggles.freeCosmetics, x => CheatToggles.freeCosmetics = x), new ToggleInfo(" Avoid Penalties", () => CheatToggles.avoidBans, x => CheatToggles.avoidBans = x), new ToggleInfo(" Unlock Extra Features", () => CheatToggles.unlockFeatures, x => CheatToggles.unlockFeatures = x), - }, new List())); + }, [])); + + groups.Add(new GroupInfo("Notifications", false, [ + new ToggleInfo(" Console", () => MalumMenu.consoleUI.isVisible, x => MalumMenu.consoleUI.isVisible = x), + 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) + ], [])); } private void Update(){ From 466b37328d3137725a5d2f54292484d0af319e53 Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:55:42 +0100 Subject: [PATCH 06/12] Update MalumMenu.cs --- src/MalumMenu.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/MalumMenu.cs b/src/MalumMenu.cs index b7b3e27d..e3531378 100644 --- a/src/MalumMenu.cs +++ b/src/MalumMenu.cs @@ -15,10 +15,10 @@ namespace MalumMenu; public partial class MalumMenu : BasePlugin { public Harmony Harmony { get; } = new(Id); - public static string malumVersion = "2.4.2"; - public static List supportedAU = new List { "2024.9.4" }; + public static string malumVersion = "2.5.3"; + public static List supportedAU = new List { "2025.9.9" }; public static MenuUI menuUI; - // public static ConsoleUI consoleUI; + public static ConsoleUI consoleUI; public static ConfigEntry menuKeybind; public static ConfigEntry menuHtmlColor; public static ConfigEntry spoofLevel; @@ -77,15 +77,16 @@ public override void Load() Harmony.PatchAll(); menuUI = AddComponent(); - // consoleUI = AddComponent(); + consoleUI = AddComponent(); - // Disable Telemetry (haven't fully tested if it works, but according to Unity docs it should) + // Disable Telemetry (better one) if (noTelemetry.Value){ - - Analytics.enabled = false; Analytics.deviceStatsEnabled = false; + Analytics.enabled = false; + Analytics.initializeOnStartup = false; + Analytics.limitUserTracking = true; + CrashReportHandler.enableCaptureExceptions = false; PerformanceReporting.enabled = false; - } SceneManager.add_sceneLoaded((Action) ((scene, _) => From a80c68cd2c18983c8f3149f4ebaaf239bc1d0c94 Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:56:25 +0100 Subject: [PATCH 07/12] Update CheatToggles.cs --- src/UI/CheatToggles.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/UI/CheatToggles.cs b/src/UI/CheatToggles.cs index ba0211ed..d8e5f665 100644 --- a/src/UI/CheatToggles.cs +++ b/src/UI/CheatToggles.cs @@ -89,6 +89,12 @@ public struct CheatToggles public static bool freeCosmetics = true; public static bool avoidBans = true; + //Notifications + public static bool notifyOnDeath; + public static bool notifyOnDisconnect; + public static bool notifyOnVent; + public static bool showNotificationLog; + public static void DisablePPMCheats(string variableToKeep) { reportBody = variableToKeep != "reportBody" ? false : reportBody; From 54ee6030dc19c59eec870665bba965da5a4f2835 Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:57:09 +0100 Subject: [PATCH 08/12] Update Utils.cs --- src/Utilities/Utils.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Utilities/Utils.cs b/src/Utilities/Utils.cs index beb45742..bb2560b3 100644 --- a/src/Utilities/Utils.cs +++ b/src/Utilities/Utils.cs @@ -90,6 +90,19 @@ public static void adjustResolution() { ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen); } + // 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; + } + // Get RoleBehaviour from a RoleType public static RoleBehaviour getBehaviourByRoleType(RoleTypes roleType) { return RoleManager.Instance.AllRoles.First(r => r.Role == roleType); From 9a03a1db340cc8a581baa2b9e9f272478e95b393 Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:58:30 +0100 Subject: [PATCH 09/12] Update PlayerPhysicsPatches.cs --- src/Patches/PlayerPhysicsPatches.cs | 44 +++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/Patches/PlayerPhysicsPatches.cs b/src/Patches/PlayerPhysicsPatches.cs index 03f0b35e..bf1e4dbf 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); @@ -40,11 +75,10 @@ public static void Postfix(PlayerPhysics __instance) { 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); } } } -} \ No newline at end of file +} From c989f99cd45a49b55baa9dd1a0403e280e2b4c8c Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:59:14 +0100 Subject: [PATCH 10/12] Update PlayerControlPatches.cs --- src/Patches/PlayerControlPatches.cs | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/Patches/PlayerControlPatches.cs b/src/Patches/PlayerControlPatches.cs index 36ea7ee0..fa27c8be 100644 --- a/src/Patches/PlayerControlPatches.cs +++ b/src/Patches/PlayerControlPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using System.Collections.Generic; namespace MalumMenu; @@ -80,3 +81,47 @@ public static void Prefix(ref bool shouldAnimate){ } } } + +[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); + } + } +} From 13b361ba0305f90287c628e569e5c5082899797c Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:02:25 +0100 Subject: [PATCH 11/12] Update MeetingHudPatches.cs --- src/Patches/MeetingHudPatches.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Patches/MeetingHudPatches.cs b/src/Patches/MeetingHudPatches.cs index 275c31da..8deb0db7 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 +} From 140e6b60cfe920b19ded2afee2420ac3b6c44360 Mon Sep 17 00:00:00 2001 From: ReactorCoreDev <165254059+ReactorCoreDev@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:03:00 +0100 Subject: [PATCH 12/12] Update OtherPatches.cs --- src/Patches/OtherPatches.cs | 47 ++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/Patches/OtherPatches.cs b/src/Patches/OtherPatches.cs index c8fb3c36..3d7c579f 100644 --- a/src/Patches/OtherPatches.cs +++ b/src/Patches/OtherPatches.cs @@ -3,6 +3,7 @@ using UnityEngine; using System; using System.Security.Cryptography; +using System.Collections.Generic; namespace MalumMenu; @@ -227,4 +228,48 @@ public static void Postfix(Vent __instance, NetworkedPlayerInfo pc, ref bool can } } } -} \ No newline at end of file +} + +[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(); + } +}