Skip to content
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/MalumMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@

[BepInAutoPlugin]
[BepInProcess("Among Us.exe")]
public partial class MalumMenu : BasePlugin

Check warning on line 16 in src/MalumMenu.cs

View workflow job for this annotation

GitHub Actions / build

Class MalumMenu has BepInPlugin attribute but does not inherit from BaseUnityPlugin

Check warning on line 16 in src/MalumMenu.cs

View workflow job for this annotation

GitHub Actions / build

Class MalumMenu has BepInPlugin attribute but does not inherit from BaseUnityPlugin
{
public Harmony Harmony { get; } = new(Id);
public new static ManualLogSource Log;
public static string malumVersion = "2.6.1";
public static List<string> 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;
Expand Down Expand Up @@ -100,7 +100,7 @@
Harmony.PatchAll();

menuUI = AddComponent<MenuUI>();
// consoleUI = AddComponent<ConsoleUI>();
consoleUI = AddComponent<ConsoleUI>();
rolesUI = AddComponent<RolesUI>();
doorsUI = AddComponent<DoorsUI>();
tasksUI = AddComponent <TasksUI>();
Expand Down
23 changes: 0 additions & 23 deletions src/Patches/OtherPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,29 +298,6 @@
}
}

[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
{
Expand Down Expand Up @@ -418,7 +395,7 @@
{
if (!Utils.isHost) return true;

show &= PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data != null;

Check warning on line 398 in src/Patches/OtherPatches.cs

View workflow job for this annotation

GitHub Actions / build

Harmony non-ref patch parameter show &= PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data != null modified. This assignment have no effect.

Check warning on line 398 in src/Patches/OtherPatches.cs

View workflow job for this annotation

GitHub Actions / build

Harmony non-ref patch parameter show &= PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data != null modified. This assignment have no effect.
__instance.BanButton.gameObject.SetActive(true);
__instance.KickButton.gameObject.SetActive(true);
__instance.MenuButton.gameObject.SetActive(show);
Expand Down
63 changes: 63 additions & 0 deletions src/Patches/PlayerControlPatches.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using HarmonyLib;
using UnityEngine;

namespace MalumMenu;

Expand Down Expand Up @@ -56,6 +57,37 @@ public static bool Prefix(PlayerControl __instance, PlayerControl target)
}
}

[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.MurderPlayer))]
public static class PlayerControl_MurderPlayer
{
/// <summary>
/// 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.
/// </summary>
/// <param name="__instance">The <c>PlayerControl</c> instance.</param>
/// <param name="target">The player being killed.</param>
public static void Prefix(PlayerControl __instance, PlayerControl target)
{
if (!CheatToggles.logDeaths || target == null) return;

var (realKillerName, displayKillerName, isDisguised) = Utils.GetPlayerIdentity(__instance);
var targetName = $"<color=#{ColorUtility.ToHtmlStringRGB(target.Data.Color)}>{target.CurrentOutfit.PlayerName}</color>";
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
{
Expand Down Expand Up @@ -100,6 +132,37 @@ public static void Prefix(ref bool shouldAnimate){
}
}

[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Shapeshift))]
public static class PlayerControl_Shapeshift
{
/// <summary>
/// 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.
/// </summary>
/// <param name="__instance">The <c>PlayerControl</c> instance.</param>
/// <param name="targetPlayer">The player that is being shapeshifted into.</param>
/// <param name="animate">Used in the original method to determine whether the shapeshift animation should play.</param>
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($"<color=#{ColorUtility.ToHtmlStringRGB(GameData.Instance.GetPlayerById(__instance.PlayerId).Color)}>" +
$"{GameData.Instance.GetPlayerById(__instance.PlayerId)._object.Data.PlayerName}</color> Shapeshift was reverted");
}
else
{
ConsoleUI.Log($"<color=#{ColorUtility.ToHtmlStringRGB(GameData.Instance.GetPlayerById(__instance.PlayerId).Color)}>" +
$"{GameData.Instance.GetPlayerById(__instance.PlayerId)._object.Data.PlayerName}</color> shapeshifted into " +
$"<color=#{ColorUtility.ToHtmlStringRGB(GameData.Instance.GetPlayerById(targetPlayerInfo.PlayerId).Color)}>" +
$"{GameData.Instance.GetPlayerById(targetPlayerInfo.PlayerId)._object.Data.PlayerName}</color>");
}
}
}

[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcSyncSettings))]
public static class PlayerControl_RpcSyncSettings
{
Expand Down
75 changes: 75 additions & 0 deletions src/Patches/VentPatches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using HarmonyLib;
using UnityEngine;

namespace MalumMenu;

[HarmonyPatch(typeof(Vent), nameof(Vent.CanUse))]
public static class Vent_CanUse
{
/// <summary>
/// Postfix patch of Vent.CanUse to allow venting.
/// </summary>
/// <param name="__instance">The <c>Vent</c> instance.</param>
/// <param name="pc">The <c>PlayerControl</c> of the player trying to use the vent.</param>
/// <param name="canUse">Whether the player can currently use the vent, accounting for distance and physics obstacles.</param>
/// <param name="couldUse">Whether the player's role and game state theoretically allow vent usage.</param>
/// <param name="__result">The distance from the player to the vent, or -1 if the vent cannot be used.</param>
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
{
/// <summary>
/// Postfix patch of Vent.EnterVent to log when a player enters a vent, along with the room they entered it in.
/// </summary>
/// <param name="__instance">The <c>Vent</c> instance.</param>
/// <param name="pc">The <c>PlayerControl</c> of the player entering the vent.</param>
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
{
/// <summary>
/// Postfix patch of Vent.ExitVent to log when a player exits a vent, along with the room they exited it in.
/// </summary>
/// <param name="__instance">The <c>Vent</c> instance.</param>
/// <param name="pc">The <c>PlayerControl</c> of the player exiting the vent.</param>
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}");
}
}
6 changes: 6 additions & 0 deletions src/UI/CheatToggles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
56 changes: 34 additions & 22 deletions src/UI/ConsoleUI.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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<string> _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();
Expand Down
11 changes: 6 additions & 5 deletions src/UI/MenuUI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ToggleInfo>() {
// new ToggleInfo(" ConsoleUI", () => MalumMenu.consoleUI.isVisible, x => MalumMenu.consoleUI.isVisible = x),
//}, new List<SubmenuInfo>()));
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,
[
Expand Down
27 changes: 27 additions & 0 deletions src/Utilities/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ public static int getClientIdByPlayer(PlayerControl player)
return client == null ? -1 : client.Id;
}

/// <summary>
/// Get a player's real name, display name, and whether they are disguised or not.
/// </summary>
/// <param name="player">The PlayerControl of the player to get the identity of.</param>
/// <returns>A tuple containing the player's real name, display name, and whether they are disguised or not.</returns>
public static (string realName, string displayName, bool isDisguised) GetPlayerIdentity(PlayerControl player)
{
if (player == null || player.Data == null) return ("", "", false);

var realName = $"<color=#{ColorUtility.ToHtmlStringRGB(player.Data.Color)}>{player.Data.PlayerName}</color>";
var displayName = $"<color=#{ColorUtility.ToHtmlStringRGB(Palette.PlayerColors[player.CurrentOutfit.ColorId])}>{player.CurrentOutfit.PlayerName}</color>";
var isDisguised = player.CurrentOutfit.PlayerName != player.Data.PlayerName;

return (realName, displayName, isDisguised);
}

// Check if player is currently vanished
public static bool isVanished(NetworkedPlayerInfo playerInfo)
{
Expand Down Expand Up @@ -329,6 +345,17 @@ public static SystemTypes getCurrentRoom(){
return HudManager.Instance.roomTracker.LastRoom.RoomId;
}

/// <summary>
/// Get the PlainShipRoom from a Vector2 position.
/// </summary>
/// <param name="position">The position to check for the room.</param>
/// <returns>The PlainShipRoom at the given position, or null if none found.</returns>
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)
{
Expand Down
Loading