diff --git a/README.md b/README.md index bfb3183..ccb7bc5 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 diff --git a/src/MalumMenu.cs b/src/MalumMenu.cs index a002660..68d57fc 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/Patches/OtherPatches.cs b/src/Patches/OtherPatches.cs index 3428209..3d9d904 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/PlayerControlPatches.cs b/src/Patches/PlayerControlPatches.cs index 8a9e424..e30f440 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 { diff --git a/src/Patches/VentPatches.cs b/src/Patches/VentPatches.cs new file mode 100644 index 0000000..fc8affe --- /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/UI/CheatToggles.cs b/src/UI/CheatToggles.cs index 72ed3f7..9a12397 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/ConsoleUI.cs b/src/UI/ConsoleUI.cs index cca4fff..ffd0c1c 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(); diff --git a/src/UI/MenuUI.cs b/src/UI/MenuUI.cs index 9b903f1..1ed42cf 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, [ diff --git a/src/Utilities/Utils.cs b/src/Utilities/Utils.cs index 7b38a73..e5d9599 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) {