From 90eaa94ab033b28e5c4f354e0cc4595c96b912ea Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 00:04:53 +0200 Subject: [PATCH 01/39] Fix chat clipboard and ghost chat --- Class1.cs | 2822 ------- ClassLibrary10.slnx | 3 - ElysiumModMenu.cs | 8830 +++++++++++++++++++++ NjordMenu.csproj => ElysiumModMenu.csproj | 28 +- ElysiumModMenu.slnx | 3 + README.md | 159 +- 6 files changed, 9006 insertions(+), 2839 deletions(-) delete mode 100644 Class1.cs delete mode 100644 ClassLibrary10.slnx create mode 100644 ElysiumModMenu.cs rename NjordMenu.csproj => ElysiumModMenu.csproj (65%) create mode 100644 ElysiumModMenu.slnx diff --git a/Class1.cs b/Class1.cs deleted file mode 100644 index 1e17bcd..0000000 --- a/Class1.cs +++ /dev/null @@ -1,2822 +0,0 @@ -#nullable disable -#pragma warning disable CS0162, CS0108, CS0219 - -using AmongUs.Data.Player; -using AmongUs.GameOptions; -using BepInEx; -using BepInEx.Unity.IL2CPP; -using BepInEx.Unity.IL2CPP.Utils.Collections; // для WrapToIl2Cpp() -using HarmonyLib; -using Hazel; -using Il2CppInterop.Runtime.Injection; -using Il2CppInterop.Runtime.InteropTypes.Arrays; // для Il2CppReferenceArray -using InnerNet; -using NjordMenu; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using UnityEngine; -using Color = UnityEngine.Color; -using Object = UnityEngine.Object; -using Vector3 = UnityEngine.Vector3; - -namespace NjordMenu -{ - [BepInPlugin("com.njord.menu", "NjordMenu", "1.0")] - public class Plugin : BasePlugin - { - public static Plugin Instance { get; private set; } = null!; - - public override void Load() - { - Instance = this; - Log.LogInfo("NjordMenu Loaded!"); - - ClassInjector.RegisterTypeInIl2Cpp(); - - var guiObject = new GameObject("NjordMenu_Object"); - UnityEngine.Object.DontDestroyOnLoad(guiObject); - guiObject.hideFlags = HideFlags.HideAndDontSave; - guiObject.AddComponent(); - - var harmony = new Harmony("com.njord.harmony"); - harmony.PatchAll(); - } - } - - public class NjordMenuGUI : MonoBehaviour - { - // === МАССИВЫ ДЛЯ СПУФЕРА (NjordMenu с ID 133 на первом месте) === - public static string[] spoofMenuNames = { "NjordMenu", "HostGuard/TOH", "Polar", "BanMod", "Better Among Us", "Sicko Menu" }; - public static byte[] spoofMenuRPCs = { 133, 176, 204, 212, 151, 164 }; - - // === ПЕРЕМЕННЫЕ ДЛЯ БИНДОВ === - public static Dictionary keyBinds = new Dictionary(); - public static string bindingAction = ""; - - public static bool SpoofMenuEnabled = false; - public static int selectedSpoofMenuIndex = 0; - private float uiSpoofTimer = 0f; - public static bool noClip = false; - public static bool tpToCursor = false; - public static bool dragToCursor = false; - public static float walkSpeed = 1f; - - // --- ДОБАВЛЕННЫЕ ПЕРЕМЕННЫЕ --- - public static float engineSpeed = 1f; - public static bool invertControls = false; - public static bool autoFollowCursor = false; - // === ПЕРЕМЕННЫЕ ДЛЯ НОВЫХ РОЛЕЙ === - public static int fakeRoleIdx = 0; - public static RoleTypes[] forceRoleOptions = { RoleTypes.Crewmate, RoleTypes.Impostor, RoleTypes.Engineer, RoleTypes.Scientist, RoleTypes.Shapeshifter, RoleTypes.GuardianAngel }; - public static bool NoShapeshiftAnim = false; - public static bool EndlessTracking = false; - public static bool NoTrackingCooldown = false; - public static bool UnlimitedInterrogateRange = false; - public static bool noTaskMode = false; - - // === ПЕРЕМЕННЫЕ ДЛЯ ЦВЕТА === - public static bool enableColorCommand = false; - public static bool hostChatColor = false; - public static Color hostChatColorValue = new Color32(0, 128, 128, 255); - // === БАЗОВЫЕ ПЕРЕМЕННЫЕ === - public static bool showMenu = false; - public static Rect windowRect = new Rect(100, 100, 750, 480); - public static bool freecam = false; - private static bool _freecamActive = false; - public static bool cameraZoom = false; - public static bool RevealVotesEnabled = false; - - // === ЦВЕТА И ФОН === - public static Color currentAccentColor = new Color32(0, 128, 128, 255); - public static bool rgbMenuMode = false; - private float rgbMenuHue = 0f; - public static bool enableBackground = false; - public static Texture2D customMenuBg = null; - private bool wasShowMenu = false; // Выносим её из метода Update сюда - private int currentMenuColorIndex = 0; - private string[] menuColorNames = { - "Njord Blue", "Dark Forest", "Green", "Sea Green", "Mint", "Chartreuse", - "Sun Yellow", "Marigold", "Old Gold", - "Bright Amber", "Vivid Orange", "Dark Orange", - "Blood Red", - "Hot Pink", "Pale Mauve", "Lilac", - "Lavender", "Deep Indigo", "Indigo", - "Med Slate Blue", "Slate Blue", "Navy", "Slate Grey" - }; - private Color[] menuColors = { - new Color32(51, 51, 255, 255), new Color(0.192f, 0.290f, 0.196f, 1f), new Color(0f, 0.502f, 0f, 1f), new Color(0.235f, 0.702f, 0.443f, 1f), new Color(0.243f, 0.706f, 0.537f, 1f), new Color(0.498f, 1f, 0f, 1f), - new Color(0.996f, 0.718f, 0.082f, 1f), new Color(0.812f, 0.651f, 0.004f, 1f), - new Color(0.996f, 0.612f, 0.063f, 1f), new Color(0.957f, 0.455f, 0.004f, 1f), new Color(1f, 0.549f, 0f, 1f), - new Color(0.871f, 0.071f, 0.149f, 1f), - new Color(0.992f, 0.529f, 0.859f, 1f), new Color(0.882f, 0.678f, 0.800f, 1f), new Color(0.784f, 0.635f, 0.784f, 1f), - new Color(0.925f, 0.686f, 0.996f, 1f), new Color(0.314f, 0.267f, 0.675f, 1f), new Color(0.294f, 0f, 0.51f, 1f), - new Color(0.482f, 0.408f, 0.933f, 1f), new Color(0.416f, 0.353f, 0.804f, 1f), new Color(0f, 0f, 0.502f, 1f), new Color(0.439f, 0.502f, 0.565f, 1f) - }; - - // === ПЕРЕМЕННЫЕ ДЛЯ MULTI (MOVEMENT И ДР.) === - public static float speedMultiplier = 1f; - public static bool noSettingLimit = false; // используется в патчах No Setting Limits - // НОВЫЕ ПЕРЕМЕННЫЕ ДЛЯ ЦВЕТА - public static float globalRoomColorId = 0f; - // === НАВИГАЦИЯ === - private int currentTab = 0; - private int targetTabIndex = 0; - private float tabTransitionProgress = 1f; - private Vector2 scrollPosition = Vector2.zero; - private string[] tabNames = { "GENERAL", "SELF", "VISUALS", "PLAYERS", "ROLES", "SABOTAGES", "HOST ONLY", "OUTFITS", "MENU" }; - - // === ПОДВКЛАДКИ === - private int currentVisualsSubTab = 0; - private string[] visualsSubTabs = { "IN-GAME" }; - private int currentSelfSubTab = 0; - private string[] selfSubTabs = { "SPOOF", "MOVEMENT" }; - private int currentHostOnlySubTab = 0; - private string[] hostOnlySubTabs = { "LOBBY CONTROLS", "ROLE MANAGER" }; - - // === ПЕРЕМЕННЫЕ ДЛЯ PLAYERS TAB === - private Vector2 playerListScrollPos = Vector2.zero; - private Vector2 playerActionScrollPos = Vector2.zero; - private byte selectedHydraPlayerId = 255; - public static List rainbowPlayers = new List(); - private float colorTimer = 0f; - private byte currentColorId = 0; - - // === ПЕРЕМЕННЫЕ SPOOF === - public static string spoofLevelString = "100"; - public static string customNameInput = "Njord"; - public static bool isEditingLevel = false; - public static bool isEditingName = false; - public static bool enablePlatformSpoof = true; - public static int currentPlatformIndex = 1; - public static string[] platformNames = { "Epic", "Steam", "Mac", "Win 10", "Itch", "iOS", "Android", "Switch", "Xbox", "PS", "Unknown" }; - public static Platforms[] platformValues = { Platforms.StandaloneEpicPC, Platforms.StandaloneSteamPC, Platforms.StandaloneMac, Platforms.StandaloneWin10, Platforms.StandaloneItch, Platforms.IPhone, Platforms.Android, Platforms.Switch, Platforms.Xbox, Platforms.Playstation, Platforms.Unknown }; - public static bool unlockFeatures = true; - - // === ПЕРЕМЕННЫЕ РОЛЕЙ И САБОТАЖЕЙ === - public static bool killReach = false, killAnyone = false; - public static bool endlessSsDuration = false, noVitalsCooldown = false; - public static bool endlessBattery = false, endlessVentTime = false, noVentCooldown = false; - public static bool reactorSab = false, oxygenSab = false, commsSab = false, elecSab = false; - public static bool autoOpenDoors = false; - - // === Вспомогательные переменные для визуала === - public static bool seeGhosts = false; - public static bool seeRoles = false; - public static bool showPlayerInfo = false; - public static bool revealMeetingRoles = false; - public static bool showTracers = false; - public static bool fullBright = false; - public static bool extendedLobby = false; - public static bool DarkModeEnabled = false; - public static float customLightRadius = 5f; - - // === НОВЫЕ ПЕРЕМЕННЫЕ ЧАТА === - public static bool alwaysChat = false; // Always Show Chat - public static bool readGhostChat = false; // Read Ghost Chat - - // === ПЕРЕМЕННЫЕ ХОСТА (HOST ONLY) === - public static bool neverEndGame = false; - public static bool fakeStartCounterTroll = false; - public static bool fakeStartCounterCustom = false; - public static string fakeStartInput = "99"; - public static bool isEditingFakeStart = false; - - public static HashSet forcedImpostors = new HashSet(); - public static Dictionary forcedPreGameRoles = new Dictionary(); - public static bool enablePreGameRoleForce = false; - private Vector2 preRolesListScrollPos = Vector2.zero; - private Vector2 preRolesActionScrollPos = Vector2.zero; - private byte selectedPreRoleId = 255; - public static List lockedPlayersList = new List(); - - // === СТИЛИ === - private bool stylesInited = false; - private GUIStyle windowStyle, btnStyle, activeTabStyle, headerStyle, boxStyle; - private GUIStyle sidebarStyle, sidebarBtnStyle, activeSidebarBtnStyle, titleStyle; - private GUIStyle toggleOnStyle, toggleOffStyle, toggleLabelStyle, safeLineStyle; - private GUIStyle sliderStyle, sliderThumbStyle, subTabStyle, activeSubTabStyle; - public GUIStyle inputBlockStyle; - private Texture2D texWindowBg, texBoxBg, texBtnBg, texAccent, texSidebarBg; - private Texture2D texToggleOff, texToggleOn, texSliderBg, texSliderThumb, texInputBg; - - // ========================================== - // === МЕТОДЫ ДЛЯ КОМАНД /w И /color === - // ========================================== - public static int GetColorIdByName(string name) - { - string[] names = { "red", "blue", "green", "pink", "orange", "yellow", "black", "white", "purple", "brown", "cyan", "lime", "maroon", "rose", "banana", "gray", "tan", "coral", "fortegreen" }; - for (int i = 0; i < names.Length; i++) - if (names[i] == name.ToLower().Trim()) return i; - return -1; - } - - // ========================================== - // === НОВЫЕ КОРУТИНЫ ДЛЯ ФРЕЙМА (фикс античита) === - // ========================================== - private IEnumerator AttemptShapeshiftFrame(PlayerControl target) - { - if (target == null || PlayerControl.LocalPlayer == null || AmongUsClient.Instance == null) yield break; - - bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); - - PlayerControl randomPl = null; - foreach (var pc in PlayerControl.AllPlayerControls) { if (pc != target && !pc.Data.IsDead) { randomPl = pc; break; } } - if (randomPl == null) randomPl = PlayerControl.LocalPlayer; - - if (target.Data.RoleType != RoleTypes.Shapeshifter && hasAnticheat) - { - RoleTypes currentRole = target.Data.RoleType; - target.RpcSetRole(RoleTypes.Shapeshifter, true); - yield return new WaitForSeconds(0.5f); - target.RpcShapeshift(randomPl, true); - target.RpcSetRole(currentRole, true); - } - else - { - target.RpcShapeshift(randomPl, true); - } - } - - private IEnumerator FrameAllCoroutine() - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) yield break; - bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); - - Dictionary originalRoles = new Dictionary(); - - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null && !pc.Data.IsDead) - { - originalRoles[pc.PlayerId] = pc.Data.RoleType; - if (hasAnticheat && pc.Data.RoleType != RoleTypes.Shapeshifter) - { - pc.RpcSetRole(RoleTypes.Shapeshifter, true); - } - } - } - - if (hasAnticheat) yield return new WaitForSeconds(0.5f); - - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null && !pc.Data.IsDead) - { - PlayerControl randomPl = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p != pc && !p.Data.IsDead && !p.Data.Disconnected) ?? PlayerControl.LocalPlayer; - pc.RpcShapeshift(randomPl, true); - - if (hasAnticheat && originalRoles.ContainsKey(pc.PlayerId)) - { - pc.RpcSetRole(originalRoles[pc.PlayerId], true); - } - } - } - } - - // ========================================== - // === НОВЫЙ МЕТОД FORCE MEETING AS PLAYER === - // ========================================== - private void ForceMeetingAsPlayer(PlayerControl target) - { - if (target == null || AmongUsClient.Instance == null) return; - if (!AmongUsClient.Instance.AmHost) return; - - try - { - MeetingRoomManager.Instance.AssignSelf(target, null); - target.RpcStartMeeting(null); - DestroyableSingleton.Instance.OpenMeetingRoom(target); - } - catch { } - } - - // ========================================== - // === МЕТОДЫ KILL ALL, KICK ALL (старые, но корутины используются отдельно) === - // ========================================== - private void KillAll() - { - if (PlayerControl.LocalPlayer == null || PlayerControl.AllPlayerControls == null) return; - Vector3 op = PlayerControl.LocalPlayer.transform.position; - foreach (var t in PlayerControl.AllPlayerControls) - { - if (t != null && t != PlayerControl.LocalPlayer && !t.Data.IsDead && !t.Data.Disconnected) - { - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(t.transform.position); - PlayerControl.LocalPlayer.CmdCheckMurder(t); - PlayerControl.LocalPlayer.RpcMurderPlayer(t, true); - } - } - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(op); - } - - private void KickAll() - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) - { - foreach (var pc in PlayerControl.AllPlayerControls) - if (pc != null && pc != PlayerControl.LocalPlayer && !pc.Data.Disconnected) - AmongUsClient.Instance.KickPlayer((int)pc.OwnerId, false); - } - } - - // ========================================== - // === МЕТОДЫ ЛОББИ (HOST ONLY) === - // ========================================== - private void DespawnLobby() - { - try - { - if (LobbyBehaviour.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - LobbyBehaviour.Instance.Cast().Despawn(); - } - } - catch { } - } - - private void SpawnLobby() - { - try - { - if (GameStartManager.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - LobbyBehaviour newLobby = UnityEngine.Object.Instantiate(GameStartManager.Instance.LobbyPrefab); - AmongUsClient.Instance.Spawn(newLobby.Cast(), -2, SpawnFlags.None); - } - } - catch { } - } - - // ========================================== - // === ФУНКЦИИ САБОТАЖЕЙ === - // ========================================== - private void ToggleReactor(bool state) { if (ShipStatus.Instance == null) return; byte flag = (byte)(state ? 128 : 16); try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Reactor, flag); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Laboratory, flag); if (state) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)128); else { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)16); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)17); } } catch { } } - private void ToggleO2(bool state) { if (ShipStatus.Instance == null) return; try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.LifeSupp, (byte)(state ? 128 : 16)); } catch { } } - private void ToggleComms(bool state) { if (ShipStatus.Instance == null) return; try { if (state) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)128); else { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)16); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)17); } } catch { } } - private void ToggleLights(bool state) - { - if (ShipStatus.Instance == null) return; - try - { - if (state) - { - byte b = 4; - for (int i = 0; i < 5; i++) if (UnityEngine.Random.value > 0.5f) b |= (byte)(1 << i); - ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, (byte)(b | 128)); - } - else - { - var sys = ShipStatus.Instance.Systems[SystemTypes.Electrical].Cast(); - if (sys != null) - { - for (int i = 0; i < 5; i++) - { - bool expected = (sys.ExpectedSwitches & (1 << i)) != 0; - bool actual = (sys.ActualSwitches & (1 << i)) != 0; - if (expected != actual) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, (byte)i); - } - } - } - } - catch { } - } - private void SabotageMushroom() { if (ShipStatus.Instance == null) return; try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.MushroomMixupSabotage, (byte)1); } catch { } } - - // ========================================== - // === МЕТОДЫ АНЛОКА И ГЛОБАЛА === - // ========================================== - public static void UnlockCosmetics() - { - if (HatManager.Instance == null) return; - try - { - foreach (var h in HatManager.Instance.allHats) h.Free = true; - foreach (var s in HatManager.Instance.allSkins) s.Free = true; - foreach (var v in HatManager.Instance.allVisors) v.Free = true; - foreach (var p in HatManager.Instance.allPets) p.Free = true; - foreach (var n in HatManager.Instance.allNamePlates) n.Free = true; - } - catch { } - } - - public static void ChangeNameGlobalHost(PlayerControl target, string newName) - { - if (target == null) return; - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; - try - { - target.RpcSetName(newName); - var netObj = GameData.Instance.GetComponent(); - if (netObj != null) netObj.SetDirtyBit(1U << (int)target.PlayerId); - } - catch { } - } - - // ========================================== - // === СОХРАНЕНИЕ НАСТРОЕК (Spoof Level & Platform) === - // ========================================== - private void SaveConfig() - { - try - { - PlayerPrefs.SetString("M_SpoofLevel", spoofLevelString); - PlayerPrefs.SetInt("M_PlatSpoof", enablePlatformSpoof ? 1 : 0); - PlayerPrefs.SetInt("M_PlatIdx", currentPlatformIndex); - - // СОХРАНЯЕМ КНОПКУ МЕНЮ - if (keyBinds.ContainsKey("Toggle Menu")) - { - PlayerPrefs.SetInt("M_ToggleKey", (int)keyBinds["Toggle Menu"]); - } - - PlayerPrefs.Save(); - } - catch { } - } - - private void LoadConfig() - { - try - { - if (PlayerPrefs.HasKey("M_SpoofLevel")) spoofLevelString = PlayerPrefs.GetString("M_SpoofLevel"); - if (PlayerPrefs.HasKey("M_PlatSpoof")) enablePlatformSpoof = PlayerPrefs.GetInt("M_PlatSpoof") == 1; - if (PlayerPrefs.HasKey("M_PlatIdx")) currentPlatformIndex = PlayerPrefs.GetInt("M_PlatIdx"); - - // ЗАГРУЖАЕМ КНОПКУ МЕНЮ - if (PlayerPrefs.HasKey("M_ToggleKey")) - { - keyBinds["Toggle Menu"] = (KeyCode)PlayerPrefs.GetInt("M_ToggleKey"); - } - else - { - keyBinds["Toggle Menu"] = KeyCode.Insert; - } - } - catch { } - } - // ========================================== - // === СТИЛИ === - // ========================================== - private Texture2D MakeRoundedTex(int size, Color col, float radius) - { - Texture2D result = new Texture2D(size, size, TextureFormat.RGBA32, false); - result.hideFlags = HideFlags.HideAndDontSave; - Color[] pix = new Color[size * size]; - float center = size / 2f; - for (int y = 0; y < size; y++) - { - for (int x = 0; x < size; x++) - { - float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); - float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); - float dist = Mathf.Sqrt(dx * dx + dy * dy); - float alpha = Mathf.Clamp01(radius - dist + 0.5f); - Color c = col; - c.a = col.a * alpha; - pix[y * size + x] = c; - } - } - result.SetPixels(pix); result.Apply(); - return result; - } - - private RectOffset CreateRectOffset(int left, int right, int top, int bottom) - { - return new RectOffset { left = left, right = right, top = top, bottom = bottom }; - } - - private void UpdateSwitchTex(Texture2D tex, bool isOn, Color accentColor) - { - int width = tex.width; int height = tex.height; - Color transparent = new Color(0, 0, 0, 0); - Color offBg = new Color(0.23f, 0.23f, 0.23f, 1f); - Color offKnob = new Color(0.6f, 0.6f, 0.6f, 1f); - Color bgColor = isOn ? accentColor : offBg; - Color knobColor = isOn ? Color.white : offKnob; - float r = height / 2f; - float cx1 = r; float cx2 = width - r; float cy = r; - float knobRadius = r - 2f; - float knobCx = isOn ? cx2 : cx1; - Color[] pixels = new Color[width * height]; - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - float dLeft = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx1, cy)); - float dRight = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx2, cy)); - float dRect = (x + 0.5f >= cx1 && x + 0.5f <= cx2) ? Mathf.Abs((y + 0.5f) - cy) : 9999f; - float distBg = Mathf.Min(dLeft, Mathf.Min(dRight, dRect)); - float alphaBg = Mathf.Clamp01(r - distBg + 0.5f); - float distKnob = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(knobCx, cy)); - float alphaKnob = Mathf.Clamp01(knobRadius - distKnob + 0.5f); - if (alphaBg > 0) - { - Color finalCol = Color.Lerp(bgColor, knobColor, alphaKnob); - finalCol.a = alphaBg; - pixels[y * width + x] = finalCol; - } - else pixels[y * width + x] = transparent; - } - } - tex.SetPixels(pixels); tex.Apply(); - } - - private void UpdateAccentColor(Color color) - { - currentAccentColor = color; - if (texAccent != null) - { - int size = texAccent.width; - Color[] pix = new Color[size * size]; - float center = size / 2f; - float radius = 6f; - for (int y = 0; y < size; y++) - { - for (int x = 0; x < size; x++) - { - float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); - float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); - float dist = Mathf.Sqrt(dx * dx + dy * dy); - float alpha = Mathf.Clamp01(radius - dist + 0.5f); - Color c = color; c.a = alpha; - pix[y * size + x] = c; - } - } - texAccent.SetPixels(pix); texAccent.Apply(); - } - if (texSliderThumb != null) - { - int size = texSliderThumb.width; - Color[] pix = new Color[size * size]; - float center = size / 2f; - float radius = 10f; - for (int y = 0; y < size; y++) - { - for (int x = 0; x < size; x++) - { - float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); - float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); - float dist = Mathf.Sqrt(dx * dx + dy * dy); - float alpha = Mathf.Clamp01(radius - dist + 0.5f); - Color c = color; c.a = alpha; - pix[y * size + x] = c; - } - } - texSliderThumb.SetPixels(pix); texSliderThumb.Apply(); - } - if (texToggleOn != null) UpdateSwitchTex(texToggleOn, true, color); - if (windowStyle != null) windowStyle.normal.textColor = color; - if (headerStyle != null) headerStyle.normal.textColor = color; - if (activeSidebarBtnStyle != null) { activeSidebarBtnStyle.normal.textColor = color; activeSidebarBtnStyle.hover.textColor = color; } - if (activeTabStyle != null) activeTabStyle.normal.background = texAccent; - if (activeSubTabStyle != null) activeSubTabStyle.normal.background = texAccent; - if (btnStyle != null) btnStyle.active.background = texAccent; - if (inputBlockStyle != null) inputBlockStyle.normal.textColor = color; - } - - private void InitStyles() - { - Color darkBg = new Color(0.12f, 0.12f, 0.12f, 0.90f); - Color sidebarBg = new Color(0.0f, 0.0f, 0.0f, 0.0f); - Color boxBg = new Color(0f, 0f, 0f, 0f); - Color btnCol = new Color(0.23f, 0.23f, 0.23f, 1f); - Color sliderBgCol = new Color(0.08f, 0.08f, 0.08f, 1f); - - texWindowBg = MakeRoundedTex(64, darkBg, 12f); - texSidebarBg = MakeRoundedTex(64, sidebarBg, 0f); - texBoxBg = MakeRoundedTex(64, boxBg, 0f); - texBtnBg = MakeRoundedTex(64, btnCol, 6f); - texAccent = MakeRoundedTex(64, currentAccentColor, 6f); - texSliderBg = MakeRoundedTex(64, sliderBgCol, 4f); - texSliderThumb = MakeRoundedTex(20, currentAccentColor, 10f); - texInputBg = MakeRoundedTex(64, new Color(0.08f, 0.08f, 0.08f, 0.85f), 6f); - - texToggleOff = new Texture2D(30, 16, TextureFormat.RGBA32, false); texToggleOff.hideFlags = HideFlags.HideAndDontSave; - texToggleOn = new Texture2D(30, 16, TextureFormat.RGBA32, false); texToggleOn.hideFlags = HideFlags.HideAndDontSave; - UpdateSwitchTex(texToggleOff, false, Color.white); - UpdateSwitchTex(texToggleOn, true, currentAccentColor); - - safeLineStyle = new GUIStyle(); safeLineStyle.normal.background = Texture2D.whiteTexture; - - windowStyle = new GUIStyle(); - windowStyle.normal.background = texWindowBg; - windowStyle.normal.textColor = currentAccentColor; - windowStyle.fontStyle = FontStyle.Bold; - windowStyle.fontSize = 14; - windowStyle.padding = CreateRectOffset(0, 0, 0, 0); - windowStyle.border = CreateRectOffset(12, 12, 12, 12); - - boxStyle = new GUIStyle(); - boxStyle.normal.background = texBoxBg; - boxStyle.padding = CreateRectOffset(0, 0, 0, 0); - boxStyle.margin = CreateRectOffset(0, 0, 4, 8); - - btnStyle = new GUIStyle(GUI.skin.button); - btnStyle.normal.background = texBtnBg; - btnStyle.normal.textColor = new Color(0.78f, 0.78f, 0.78f); - btnStyle.active.background = texAccent; - btnStyle.active.textColor = Color.black; - btnStyle.alignment = TextAnchor.MiddleCenter; - btnStyle.border = CreateRectOffset(6, 6, 6, 6); - btnStyle.fontSize = 12; - btnStyle.fontStyle = FontStyle.Bold; - - activeTabStyle = new GUIStyle(btnStyle); - activeTabStyle.normal.background = texAccent; - activeTabStyle.normal.textColor = Color.black; - - subTabStyle = new GUIStyle(btnStyle); - subTabStyle.padding = CreateRectOffset(6, 6, 2, 2); - activeSubTabStyle = new GUIStyle(activeTabStyle); - activeSubTabStyle.padding = CreateRectOffset(6, 6, 2, 2); - - inputBlockStyle = new GUIStyle(btnStyle); - inputBlockStyle.normal.background = texInputBg; - inputBlockStyle.hover.background = texInputBg; - inputBlockStyle.active.background = texAccent; - inputBlockStyle.normal.textColor = currentAccentColor; - inputBlockStyle.alignment = TextAnchor.MiddleCenter; - inputBlockStyle.fontStyle = FontStyle.Bold; - - headerStyle = new GUIStyle(); - headerStyle.normal.background = texBtnBg; - headerStyle.normal.textColor = currentAccentColor; - headerStyle.fontStyle = FontStyle.Bold; - headerStyle.alignment = TextAnchor.MiddleLeft; - headerStyle.padding = CreateRectOffset(6, 6, 4, 4); - headerStyle.margin = CreateRectOffset(0, 0, 4, 4); - headerStyle.fontSize = 13; - - sidebarStyle = new GUIStyle(); - sidebarStyle.normal.background = texSidebarBg; - sidebarStyle.padding = CreateRectOffset(0, 0, 5, 0); - - sidebarBtnStyle = new GUIStyle(); - sidebarBtnStyle.normal.textColor = new Color(0.6f, 0.6f, 0.6f); - sidebarBtnStyle.hover.textColor = Color.white; - sidebarBtnStyle.padding = CreateRectOffset(12, 0, 6, 6); - sidebarBtnStyle.alignment = TextAnchor.MiddleLeft; - sidebarBtnStyle.fontSize = 13; - sidebarBtnStyle.fontStyle = FontStyle.Bold; - - activeSidebarBtnStyle = new GUIStyle(sidebarBtnStyle); - activeSidebarBtnStyle.normal.textColor = currentAccentColor; - activeSidebarBtnStyle.hover.textColor = currentAccentColor; - - toggleOffStyle = new GUIStyle(); toggleOffStyle.normal.background = texToggleOff; - toggleOnStyle = new GUIStyle(); toggleOnStyle.normal.background = texToggleOn; - - toggleLabelStyle = new GUIStyle(); - toggleLabelStyle.normal.textColor = new Color(0.78f, 0.78f, 0.78f); - toggleLabelStyle.alignment = TextAnchor.MiddleLeft; - toggleLabelStyle.padding = CreateRectOffset(4, 0, 0, 0); - toggleLabelStyle.fontSize = 12; - toggleLabelStyle.fontStyle = FontStyle.Bold; - - sliderStyle = new GUIStyle(); - sliderStyle.normal.background = texSliderBg; - sliderStyle.border = CreateRectOffset(6, 6, 6, 6); - sliderStyle.fixedHeight = 10f; - sliderStyle.margin = CreateRectOffset(0, 0, 8, 8); - - sliderThumbStyle = new GUIStyle(); - sliderThumbStyle.normal.background = texSliderThumb; - sliderThumbStyle.fixedWidth = 18f; - sliderThumbStyle.fixedHeight = 18f; - sliderThumbStyle.margin = CreateRectOffset(0, 0, -4, 0); - - titleStyle = new GUIStyle(); - titleStyle.normal.textColor = currentAccentColor; - titleStyle.fontStyle = FontStyle.Bold; - titleStyle.fontSize = 14; - titleStyle.padding = CreateRectOffset(10, 0, 8, 0); - - stylesInited = true; - } - - private void LoadBackgroundImage() - { - try - { - string bgPath = System.IO.Path.Combine(BepInEx.Paths.ConfigPath, "MenuBG.png"); - if (!System.IO.File.Exists(bgPath)) bgPath = System.IO.Path.Combine(BepInEx.Paths.ConfigPath, "MenuBG.jpg"); - if (System.IO.File.Exists(bgPath)) - { - byte[] fileData = System.IO.File.ReadAllBytes(bgPath); - Texture2D tempTex = new Texture2D(2, 2); - ImageConversion.LoadImage(tempTex, fileData); - customMenuBg = new Texture2D(tempTex.width, tempTex.height, TextureFormat.RGBA32, false); - customMenuBg.hideFlags = HideFlags.HideAndDontSave; - Color[] pix = tempTex.GetPixels(); - UnityEngine.Object.Destroy(tempTex); - int w = customMenuBg.width, h = customMenuBg.height; - float targetRadius = 12f, rx = targetRadius * (w / windowRect.width), ry = targetRadius * (h / windowRect.height); - for (int y = 0; y < h; y++) - for (int x = 0; x < w; x++) - { - float dx = 0f, dy = 0f; - if (x < rx) dx = rx - x; - else if (x > w - rx) dx = x - (w - rx); - if (y < ry) dy = ry - y; - else if (y > h - ry) dy = y - (h - ry); - if (dx > 0 && dy > 0) - { - float nx = dx / rx, ny = dy / ry; - float dist = Mathf.Sqrt(nx * nx + ny * ny); - if (dist > 1f) { Color c = pix[y * w + x]; c.a = 0f; pix[y * w + x] = c; } - else - { - float alphaMult = Mathf.Clamp01((1f - dist) * Mathf.Max(rx, ry)); - Color c = pix[y * w + x]; c.a *= alphaMult; pix[y * w + x] = c; - } - } - } - customMenuBg.SetPixels(pix); customMenuBg.Apply(); - } - else enableBackground = false; - } - catch { enableBackground = false; } - } - - public static string ApplyMenuShimmer(string text) - { - string result = ""; - Color baseColor = currentAccentColor, glowColor = Color.white; - for (int i = 0; i < text.Length; i++) - { - if (text[i] == ' ') { result += " "; continue; } - float wave = Mathf.Sin(Time.unscaledTime * 6f - (i * 0.4f)) * 0.5f + 0.5f; - Color c = Color.Lerp(baseColor, glowColor, wave); - result += $"{text[i]}"; - } - return result; - } - - private bool DrawToggle(bool value, string text, int width = 0) - { - GUILayout.BeginHorizontal(GUILayout.Width(width > 0 ? width : 200)); - bool clickedBox = GUILayout.Button("", value ? toggleOnStyle : toggleOffStyle, GUILayout.Width(30), GUILayout.Height(16)); - GUILayout.Space(6); - bool clickedText = GUILayout.Button(text, toggleLabelStyle); - GUILayout.EndHorizontal(); - return (clickedBox || clickedText) ? !value : value; - } - - private bool DrawBindableButton(string label, string bindKey, float width) - { - bool clicked = false; - GUILayout.BeginVertical(GUILayout.Width(width)); - if (GUILayout.Button(label, btnStyle, GUILayout.Height(25), GUILayout.Width(width))) clicked = true; - string bindTxt = bindingAction == bindKey ? "Press Key..." : (keyBinds.ContainsKey(bindKey) ? $"[{keyBinds[bindKey]}]" : "[Bind Key]"); - GUIStyle bindStyle = new GUIStyle(btnStyle) { fontSize = 10, normal = { textColor = new Color(0.6f, 0.6f, 0.6f) } }; - if (bindingAction == bindKey) bindStyle.normal.textColor = currentAccentColor; - if (GUILayout.Button(bindTxt, bindStyle, GUILayout.Height(15), GUILayout.Width(width))) bindingAction = bindKey; - GUILayout.EndVertical(); - return clicked; - } - - private bool DrawHostToggle(bool value, string text, float totalWidth = 250f) - { - GUILayout.BeginHorizontal(GUILayout.Width(totalWidth), GUILayout.Height(20)); - bool clickedBox = GUILayout.Button("", value ? toggleOnStyle : toggleOffStyle, GUILayout.Width(30), GUILayout.Height(16)); - GUILayout.Space(6); - bool clickedText = GUILayout.Button(text, toggleLabelStyle, GUILayout.Width(totalWidth - 36f), GUILayout.Height(16)); - GUILayout.EndHorizontal(); - return (clickedBox || clickedText) ? !value : value; - } - - // ========================================== - // === GUI ВКЛАДКИ === - // ========================================== - private void DrawGeneralTab() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("INFORMATION", headerStyle); - GUILayout.Space(10); - GUIStyle textStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 13 }; - string title = ApplyMenuShimmer("Welcome to NjordMenu!"); - string respect = ApplyMenuShimmer("Respect other players:"); - string useWisely = ApplyMenuShimmer("Use wisely:"); - string risks = ApplyMenuShimmer("Risks:"); - string infoText = $"{title}\n\n" + - $"NjordMenu is a multi-functional tool created by Meowchelo to enhance the Among Us experience.\n\n" + - $"{respect} Remember that there are real people behind those screens who just want to relax.\n\n" + - $"{useWisely} Use aggressive features only in private lobbies with friends.\n\n" + - $"{risks} Abusing these features in public games can lead to reports and banning of your account."; - GUILayout.Label(infoText, textStyle); - GUILayout.FlexibleSpace(); - GUILayout.EndVertical(); - } - - private void DrawSelfTab() - { - GUILayout.BeginHorizontal(); - for (int i = 0; i < selfSubTabs.Length; i++) - if (GUILayout.Button(selfSubTabs[i], currentSelfSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) - { currentSelfSubTab = i; scrollPosition = Vector2.zero; } - GUILayout.EndHorizontal(); - GUILayout.Space(8); - // Найди это в DrawSelfTab - if (currentSelfSubTab == 0) DrawSelfSpoof(); - else if (currentSelfSubTab == 1) DrawPlayerMovement(); // Было DrawMovement, исправлено на DrawPlayerMovement - } - - private void DrawPlayerMovement() - { - GUILayout.BeginVertical(boxStyle); - try - { - GUILayout.Label("MOVEMENT & TELEPORT", headerStyle); - - GUILayout.BeginHorizontal(); - try - { - GUILayout.Label($"Engine Speed: {Mathf.Round(engineSpeed)}x", GUILayout.Width(130)); - engineSpeed = GUILayout.HorizontalSlider(engineSpeed, 1f, 555f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); - GUILayout.Space(10); - if (GUILayout.Button("Reset", btnStyle, GUILayout.Width(50), GUILayout.Height(20))) engineSpeed = 1f; - } - finally { GUILayout.EndHorizontal(); } - - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - try - { - GUILayout.Label($"Walk Speed: {Mathf.Round(walkSpeed)}x", GUILayout.Width(130)); - walkSpeed = GUILayout.HorizontalSlider(walkSpeed, 1f, 30f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); - GUILayout.Space(10); - if (GUILayout.Button("Reset", btnStyle, GUILayout.Width(50), GUILayout.Height(20))) walkSpeed = 1f; - } - finally { GUILayout.EndHorizontal(); } - - // --- УВЕЛИЧЕН ОТСТУП --- - GUILayout.Space(15); - - GUILayout.BeginHorizontal(); - try - { - // Указали ширину 160, чтобы чекбоксы не наезжали друг на друга - tpToCursor = DrawToggle(tpToCursor, "TP To Cursor", 160); - dragToCursor = DrawToggle(dragToCursor, "Drag To Cursor", 160); - } - finally { GUILayout.EndHorizontal(); } - - // --- УВЕЛИЧЕН ОТСТУП --- - GUILayout.Space(10); - - GUILayout.BeginHorizontal(); - try - { - autoFollowCursor = DrawToggle(autoFollowCursor, "Magnet Cursor [F9]", 160); - noClip = DrawToggle(noClip, "True NoClip", 160); - } - finally { GUILayout.EndHorizontal(); } - } - finally { GUILayout.EndVertical(); } - } - - private void DrawSelfSpoof() - { - GUILayout.BeginVertical(boxStyle); - GUIStyle greenHeader = new GUIStyle(headerStyle); - greenHeader.normal.textColor = currentAccentColor; - GUILayout.Label("ACCOUNT SPOOFER", greenHeader); - - // Fake Level - GUILayout.BeginHorizontal(); - GUILayout.Label("Fake Level", btnStyle, GUILayout.Width(120), GUILayout.Height(28)); - string lvlDisp = isEditingLevel ? spoofLevelString + "_" : spoofLevelString; - if (GUILayout.Button(lvlDisp, isEditingLevel ? activeTabStyle : inputBlockStyle, GUILayout.Width(160), GUILayout.Height(28))) { isEditingLevel = !isEditingLevel; isEditingName = false; } - if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(75), GUILayout.Height(28))) - { - isEditingLevel = false; - if (uint.TryParse(spoofLevelString, out uint parsedLvl)) - { - try { AmongUs.Data.DataManager.Player.stats.level = parsedLvl > 0 ? parsedLvl - 1 : 0; AmongUs.Data.DataManager.Player.Save(); } - catch { try { AmongUs.Data.DataManager.Player.Stats.Level = parsedLvl > 0 ? parsedLvl - 1 : 0; AmongUs.Data.DataManager.Player.Save(); } catch { } } - } - SaveConfig(); - } - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.Space(2); - // Name - GUILayout.BeginHorizontal(); - GUILayout.Label("Name", btnStyle, GUILayout.Width(120), GUILayout.Height(28)); - string nmDisp = isEditingName ? customNameInput + "_" : customNameInput; - if (GUILayout.Button(nmDisp, isEditingName ? activeTabStyle : inputBlockStyle, GUILayout.Width(160), GUILayout.Height(28))) { isEditingName = !isEditingName; isEditingLevel = false; } - if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(75), GUILayout.Height(28))) - { - isEditingName = false; - try { AmongUs.Data.DataManager.Player.Customization.Name = customNameInput; AmongUs.Data.DataManager.Player.Save(); } catch { } - ChangeNameGlobalHost(PlayerControl.LocalPlayer, customNameInput); - SaveConfig(); - } - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.Space(5); - // Platform Spoof - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Spoof Platform", enablePlatformSpoof ? activeTabStyle : btnStyle, GUILayout.Width(120), GUILayout.Height(28))) - { - enablePlatformSpoof = !enablePlatformSpoof; - SaveConfig(); - } - GUILayout.Space(10); - GUILayout.Label($"Platform: {platformValues[currentPlatformIndex].ToString()}", new GUIStyle(toggleLabelStyle) { fontSize = 13 }, GUILayout.Height(28)); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.BeginHorizontal(); - int newPlatIdx = (int)GUILayout.HorizontalSlider(currentPlatformIndex, 0, platformNames.Length - 1, sliderStyle, sliderThumbStyle, GUILayout.Width(290)); - if (newPlatIdx != currentPlatformIndex) - { - currentPlatformIndex = newPlatIdx; - SaveConfig(); - } - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.Space(15); - GUILayout.Label("TASKS", headerStyle); - if (GUILayout.Button("Complete My Tasks", btnStyle, GUILayout.Height(30))) - { - if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.myTasks != null) - foreach (var task in PlayerControl.LocalPlayer.myTasks) - if (task != null && !task.IsComplete) PlayerControl.LocalPlayer.RpcCompleteTask((uint)task.Id); - } - GUILayout.EndVertical(); - } - - private void DrawVisualsTab() - { - GUILayout.BeginHorizontal(); - for (int i = 0; i < visualsSubTabs.Length; i++) - if (GUILayout.Button(visualsSubTabs[i], currentVisualsSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) - { currentVisualsSubTab = i; scrollPosition = Vector2.zero; } - GUILayout.EndHorizontal(); - GUILayout.Space(8); - if (currentVisualsSubTab == 0) DrawVisualsInGame(); - } - - private void DrawVisualsInGame() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("VISIBILITY", headerStyle); - GUILayout.BeginHorizontal(); - seeGhosts = DrawToggle(seeGhosts, "See Ghosts", 210); - seeRoles = DrawToggle(seeRoles, "See Roles", 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - showPlayerInfo = DrawToggle(showPlayerInfo, "Show Player Info (ESP)", 210); - revealMeetingRoles = DrawToggle(revealMeetingRoles, "Reveal Roles (Meeting)", 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - showTracers = DrawToggle(showTracers, "Show Tracers", 210); - fullBright = DrawToggle(fullBright, "Full Bright (No Shadows)", 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - alwaysChat = DrawToggle(alwaysChat, "Always Show Chat", 210); - readGhostChat = DrawToggle(readGhostChat, "Read Ghost Chat", 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - freecam = DrawToggle(freecam, "Freecam (WASD)", 210); - cameraZoom = DrawToggle(cameraZoom, "Camera Zoom (Scroll)", 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - RevealVotesEnabled = DrawToggle(RevealVotesEnabled, "Reveal Votes (Meeting)", 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - GUILayout.Label("Light Radius", toggleLabelStyle); - customLightRadius = GUILayout.HorizontalSlider(customLightRadius, 0.5f, 20f, sliderStyle, sliderThumbStyle, GUILayout.Width(200)); - GUILayout.EndVertical(); - } - - private void DrawPlayersTab() - { - GUILayout.BeginHorizontal(); - // Левая панель - список - GUILayout.BeginVertical(boxStyle, GUILayout.Width(200)); - playerListScrollPos = GUILayout.BeginScrollView(playerListScrollPos); - if (lockedPlayersList.Count > 0) - { - foreach (var pc in lockedPlayersList) - { - if (pc == null || pc.Data == null || pc.PlayerId >= 100) continue; - string pName = pc.Data.PlayerName ?? "Unknown"; - if (forcedPreGameRoles.ContainsKey(pc.PlayerId)) pName += " [*]"; - else if (forcedImpostors.Contains(pc.PlayerId)) pName += " [Imp]"; - bool isSelected = selectedHydraPlayerId == pc.PlayerId; - GUI.contentColor = Color.white; - try { GUI.contentColor = Palette.PlayerColors[pc.Data.DefaultOutfit.ColorId]; } catch { } - if (GUILayout.Button(pName, isSelected ? activeTabStyle : btnStyle, GUILayout.Height(30))) selectedHydraPlayerId = pc.PlayerId; - GUI.contentColor = Color.white; - } - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - - // Правая панель - действия - GUILayout.BeginVertical(boxStyle, GUILayout.ExpandWidth(true)); - playerActionScrollPos = GUILayout.BeginScrollView(playerActionScrollPos); - PlayerControl target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedHydraPlayerId); - if (target != null && target.Data != null) - { - GUILayout.Label($"Selected: {target.Data.PlayerName}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 14 }); - GUILayout.Space(10); - GUILayout.BeginHorizontal(); - GUI.backgroundColor = new Color(0.8f, 0.2f, 0.2f, 1f); - if (GUILayout.Button("KILL", btnStyle, GUILayout.Height(30))) - { - Vector3 op = PlayerControl.LocalPlayer.transform.position; - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(target.transform.position); - PlayerControl.LocalPlayer.CmdCheckMurder(target); - PlayerControl.LocalPlayer.RpcMurderPlayer(target, true); - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(op); - } - GUI.backgroundColor = Color.white; - GUILayout.Space(5); - // НОВАЯ КНОПКА FORCE MEETING (использует фикс) - if (GUILayout.Button("Force Meeting", btnStyle, GUILayout.Height(30))) ForceMeetingAsPlayer(target); - GUILayout.Space(5); - bool hr = rainbowPlayers.Contains(target.PlayerId); - if (GUILayout.Button(hr ? "RGB: ON" : "RGB: OFF", hr ? activeTabStyle : btnStyle, GUILayout.Height(30))) - { if (!hr) rainbowPlayers.Add(target.PlayerId); else rainbowPlayers.Remove(target.PlayerId); } - GUILayout.EndHorizontal(); - GUILayout.Space(5); - // КНОПКА FRAME SHAPE - запуск корутины - if (DrawBindableButton("Frame Shape", "Frame Shape", 90f)) - this.StartCoroutine(AttemptShapeshiftFrame(target).WrapToIl2Cpp()); - - GUILayout.Space(15); - GUILayout.Label("SET PLAYER COLOR", headerStyle); - GUILayout.BeginVertical(boxStyle); - GUIStyle brightColorStyle = new GUIStyle(btnStyle) { normal = { background = Texture2D.whiteTexture } }; - int colorsPerRow = 7; - for (int i = 0; i < Palette.PlayerColors.Length; i++) - { - if (i % colorsPerRow == 0) GUILayout.BeginHorizontal(); - GUI.backgroundColor = Palette.PlayerColors[i]; - if (GUILayout.Button("", brightColorStyle, GUILayout.Width(35), GUILayout.Height(30))) target.RpcSetColor((byte)i); - if (i % colorsPerRow == colorsPerRow - 1 || i == Palette.PlayerColors.Length - 1) GUILayout.EndHorizontal(); - } - GUI.backgroundColor = Color.white; - GUILayout.EndVertical(); - - GUILayout.Space(15); - GUILayout.Label("PRE-GAME ROLE (HOST)", headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Impostor", btnStyle, GUILayout.Height(25))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Add(target.PlayerId); enablePreGameRoleForce = true; } - if (GUILayout.Button("Crewmate", btnStyle, GUILayout.Height(25))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Crewmate; enablePreGameRoleForce = true; } - if (GUILayout.Button("Shapeshifter", btnStyle, GUILayout.Height(25))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Shapeshifter; enablePreGameRoleForce = true; } - GUILayout.EndHorizontal(); - GUILayout.Space(5); - if (GUILayout.Button("REMOVE FORCED ROLE", activeTabStyle, GUILayout.Height(25))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Remove(target.PlayerId); } - } - else - { - GUILayout.FlexibleSpace(); - GUILayout.Label("Select a player...", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); - GUILayout.FlexibleSpace(); - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - } - - private void DrawRolesTab() - { - GUILayout.BeginHorizontal(); - - // ЛЕВАЯ КОЛОНКА - GUILayout.BeginVertical(GUILayout.Width(280)); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Roles", headerStyle); - GUILayout.BeginHorizontal(); - GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = currentAccentColor } }; - if (GUILayout.Button("<", btnStyle, GUILayout.Width(25), GUILayout.Height(22))) { fakeRoleIdx--; if (fakeRoleIdx < 0) fakeRoleIdx = forceRoleOptions.Length - 1; } - GUILayout.Label(forceRoleOptions[fakeRoleIdx].ToString(), middleLabelStyle, GUILayout.Width(100), GUILayout.Height(22)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(25), GUILayout.Height(22))) { fakeRoleIdx++; if (fakeRoleIdx >= forceRoleOptions.Length) fakeRoleIdx = 0; } - GUILayout.Space(15); - if (GUILayout.Button("Set", activeTabStyle, GUILayout.Width(45), GUILayout.Height(22))) RoleManager.Instance?.SetRole(PlayerControl.LocalPlayer, forceRoleOptions[fakeRoleIdx]); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.Space(5); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Impostor", headerStyle); - killReach = DrawToggle(killReach, "Kill Reach", 160); - GUILayout.Space(5); // <-- ДОБАВЛЕН ОТСТУП - killAnyone = DrawToggle(killAnyone, "Kill Anyone", 160); - GUILayout.EndVertical(); - - GUILayout.Space(5); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Shapeshifter", headerStyle); - NoShapeshiftAnim = DrawToggle(NoShapeshiftAnim, "No Ss Animation", 160); - GUILayout.Space(5); // <-- ДОБАВЛЕН ОТСТУП - endlessSsDuration = DrawToggle(endlessSsDuration, "Endless Ss Duration", 160); - GUILayout.EndVertical(); - - GUILayout.Space(5); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Tracker", headerStyle); - EndlessTracking = DrawToggle(EndlessTracking, "Endless Tracking", 160); - GUILayout.Space(5); // <-- ДОБАВЛЕН ОТСТУП - NoTrackingCooldown = DrawToggle(NoTrackingCooldown, "No Track Cooldown", 160); - GUILayout.EndVertical(); - - GUILayout.EndVertical(); // Конец левой колонки - - GUILayout.Space(10); - - // ПРАВАЯ КОЛОНКА - GUILayout.BeginVertical(GUILayout.Width(280)); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Engineer", headerStyle); - endlessVentTime = DrawToggle(endlessVentTime, "Endless Vent Time", 160); - GUILayout.Space(5); // <-- ДОБАВЛЕН ОТСТУП - noVentCooldown = DrawToggle(noVentCooldown, "No Vent Cooldown", 160); - GUILayout.EndVertical(); - - GUILayout.Space(5); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Scientist", headerStyle); - endlessBattery = DrawToggle(endlessBattery, "Endless Battery", 160); - GUILayout.Space(5); // <-- ДОБАВЛЕН ОТСТУП - noVitalsCooldown = DrawToggle(noVitalsCooldown, "No Vitals Cooldown", 160); - GUILayout.EndVertical(); - - GUILayout.Space(5); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Detective", headerStyle); - UnlimitedInterrogateRange = DrawToggle(UnlimitedInterrogateRange, "Interrogate Reach", 160); - GUILayout.EndVertical(); - - GUILayout.EndVertical(); // Конец правой колонки - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - } - - private void DrawSabotagesTab() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("CRITICAL SABOTAGES", headerStyle); - - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Reactor / Lab", reactorSab ? activeTabStyle : btnStyle, GUILayout.Height(35))) - { - reactorSab = !reactorSab; - ToggleReactor(reactorSab); - } - if (GUILayout.Button("Oxygen", oxygenSab ? activeTabStyle : btnStyle, GUILayout.Height(35))) - { - oxygenSab = !oxygenSab; - ToggleO2(oxygenSab); - } - GUILayout.EndHorizontal(); - - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Comms", commsSab ? activeTabStyle : btnStyle, GUILayout.Height(35))) - { - commsSab = !commsSab; - ToggleComms(commsSab); - } - if (GUILayout.Button("Lights", elecSab ? activeTabStyle : btnStyle, GUILayout.Height(35))) - { - elecSab = !elecSab; - ToggleLights(elecSab); - } - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - if (GUILayout.Button("Trigger Mushroom (Fungle)", btnStyle, GUILayout.Height(35))) - { - SabotageMushroom(); - } - - GUILayout.EndVertical(); - } - - private void DrawHostOnlyTab() - { - GUILayout.BeginHorizontal(); - for (int i = 0; i < hostOnlySubTabs.Length; i++) - if (GUILayout.Button(hostOnlySubTabs[i], currentHostOnlySubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) - { currentHostOnlySubTab = i; scrollPosition = Vector2.zero; } - GUILayout.EndHorizontal(); - GUILayout.Space(8); - if (currentHostOnlySubTab == 0) DrawLobbyControls(); - else if (currentHostOnlySubTab == 1) DrawPlayersRoles(); - } - - private void DrawLobbyControls() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("LOBBY CONTROLS", headerStyle); - - GUILayout.BeginHorizontal(); - - // --- Левая колонка --- - GUILayout.BeginVertical(GUILayout.Width(280)); - neverEndGame = DrawToggle(neverEndGame, "Unlimited Game", 250); - GUILayout.Space(5); - noSettingLimit = DrawToggle(noSettingLimit, "No Setting Limit", 250); - GUILayout.Space(5); - noTaskMode = DrawToggle(noTaskMode, "No Task Mode", 250); - GUILayout.Space(5); - enableColorCommand = DrawToggle(enableColorCommand, "Enable /c command (Public)", 250); - GUILayout.EndVertical(); - - GUILayout.Space(10); - - // --- Правая колонка --- - GUILayout.BeginVertical(GUILayout.Width(280)); - bool prevTroll = fakeStartCounterTroll; - fakeStartCounterTroll = DrawToggle(fakeStartCounterTroll, "Fuck start (Random)", 250); - if (fakeStartCounterTroll && !prevTroll) fakeStartCounterCustom = false; - - GUILayout.Space(5); - bool prevCustom = fakeStartCounterCustom; - fakeStartCounterCustom = DrawToggle(fakeStartCounterCustom, "Fuck start (Custom)", 250); - if (fakeStartCounterCustom && !prevCustom) fakeStartCounterTroll = false; - - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - GUILayout.Space(36); - string fsDisp = isEditingFakeStart ? fakeStartInput + "_" : fakeStartInput; - if (GUILayout.Button(fsDisp, isEditingFakeStart ? activeTabStyle : inputBlockStyle, GUILayout.Width(130), GUILayout.Height(22))) { isEditingFakeStart = !isEditingFakeStart; isEditingName = false; isEditingLevel = false; } - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.Space(15); - GUILayout.Label("HOST ACTIONS", headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Insta Start", btnStyle, GUILayout.Height(25), GUILayout.Width(140)) && GameStartManager.Instance != null && AmongUsClient.Instance.AmHost) - { GameStartManager.Instance.startState = GameStartManager.StartingStates.Countdown; GameStartManager.Instance.countDownTimer = 0f; } - GUILayout.Space(10); - if (GUILayout.Button("Close Meeting", btnStyle, GUILayout.Height(25), GUILayout.Width(140)) && MeetingHud.Instance != null && AmongUsClient.Instance.AmHost) MeetingHud.Instance.RpcClose(); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - // === КНОПКИ ЛОББИ === - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Spawn Lobby", activeTabStyle, GUILayout.Height(25), GUILayout.Width(140))) SpawnLobby(); - GUILayout.Space(10); - if (GUILayout.Button("Despawn Lobby", btnStyle, GUILayout.Height(25), GUILayout.Width(140))) DespawnLobby(); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Kill All", btnStyle, GUILayout.Width(93f), GUILayout.Height(30))) KillAll(); - if (GUILayout.Button("Kick All", btnStyle, GUILayout.Width(93f), GUILayout.Height(30))) KickAll(); - // НОВАЯ КНОПКА FRAME ALL - запуск корутины - if (GUILayout.Button("Frame All", btnStyle, GUILayout.Width(93f), GUILayout.Height(30))) this.StartCoroutine(FrameAllCoroutine().WrapToIl2Cpp()); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.Space(10); - if (GUILayout.Button("Force End (Impostor Disconnect)", btnStyle, GUILayout.Height(25), GUILayout.Width(290)) && GameManager.Instance != null && AmongUsClient.Instance.AmHost) - { bool tempNeverEnd = neverEndGame; neverEndGame = false; GameManager.Instance.RpcEndGame((GameOverReason)4, false); neverEndGame = tempNeverEnd; } - GUILayout.EndVertical(); - } - - private void DrawPlayersRoles() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("PRE-GAME ROLE MANAGER", headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button(enablePreGameRoleForce ? "Role Forcing: ON" : "Role Forcing: OFF", enablePreGameRoleForce ? activeTabStyle : btnStyle, GUILayout.Height(25))) enablePreGameRoleForce = !enablePreGameRoleForce; - if (GUILayout.Button("Random 2 Imps", btnStyle, GUILayout.Width(110), GUILayout.Height(25))) - { - forcedPreGameRoles.Clear(); forcedImpostors.Clear(); - var activePlayers = PlayerControl.AllPlayerControls.ToArray().Where(p => p != null && !p.Data.Disconnected).ToList(); - if (activePlayers.Count >= 2) - { - for (int i = activePlayers.Count - 1; i > 0; i--) { int swapIndex = UnityEngine.Random.Range(0, i + 1); var temp = activePlayers[i]; activePlayers[i] = activePlayers[swapIndex]; activePlayers[swapIndex] = temp; } - forcedImpostors.Add(activePlayers[0].PlayerId); forcedImpostors.Add(activePlayers[1].PlayerId); - enablePreGameRoleForce = true; - } - } - if (GUILayout.Button("Clear All Roles", btnStyle, GUILayout.Width(110), GUILayout.Height(25))) { forcedPreGameRoles.Clear(); forcedImpostors.Clear(); } - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.Space(10); - GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(boxStyle, GUILayout.Width(200)); - preRolesListScrollPos = GUILayout.BeginScrollView(preRolesListScrollPos); - foreach (var pc in lockedPlayersList) - { - if (pc == null || pc.Data == null || pc.PlayerId >= 100) continue; - string pName = pc.Data.PlayerName ?? "Unknown"; - if (forcedPreGameRoles.ContainsKey(pc.PlayerId)) { string rShort = forcedPreGameRoles[pc.PlayerId].ToString().Replace("9", "Pha").Replace("10", "Tra").Replace("8", "Noi").Replace("12", "Det").Replace("18", "Vip"); if (rShort.Length > 3) rShort = rShort.Substring(0, 3); pName += $" [{rShort}]"; } - else if (forcedImpostors.Contains(pc.PlayerId)) pName += " [Imp]"; - bool isSelected = selectedPreRoleId == pc.PlayerId; - try { GUI.contentColor = Palette.PlayerColors[pc.Data.DefaultOutfit.ColorId]; } catch { } - if (GUILayout.Button(pName, isSelected ? activeTabStyle : btnStyle, GUILayout.Height(30))) selectedPreRoleId = pc.PlayerId; - GUI.contentColor = Color.white; - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - - GUILayout.BeginVertical(boxStyle, GUILayout.ExpandWidth(true)); - preRolesActionScrollPos = GUILayout.BeginScrollView(preRolesActionScrollPos); - PlayerControl target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedPreRoleId); - if (target != null && target.Data != null) - { - GUIStyle infoStyle = new GUIStyle(GUI.skin.label) { richText = true, fontSize = 14 }; - GUILayout.Label($"Selecting role for: {target.Data.PlayerName}", infoStyle); - RoleTypes currentForced = forcedPreGameRoles.ContainsKey(target.PlayerId) ? forcedPreGameRoles[target.PlayerId] : RoleTypes.Crewmate; - bool isForced = forcedPreGameRoles.ContainsKey(target.PlayerId) || forcedImpostors.Contains(target.PlayerId); - string roleNameStr = currentForced.ToString().Replace("9", "Phantom").Replace("10", "Tracker").Replace("8", "Noisemaker").Replace("12", "Detective").Replace("18", "Viper"); - if (forcedImpostors.Contains(target.PlayerId)) roleNameStr = "Impostor"; - GUILayout.Label($"Status: {(isForced ? $"Forced ({roleNameStr})" : "Not Forced (Random)")}", infoStyle); - GUILayout.Space(15); - GUILayout.Label("IMPOSTOR ROLES (Red Team)", headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Impostor", btnStyle, GUILayout.Height(30))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Add(target.PlayerId); } - if (GUILayout.Button("Shapeshifter", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Shapeshifter; } - if (GUILayout.Button("Phantom", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)9; } - if (GUILayout.Button("Viper", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)18; } - GUILayout.EndHorizontal(); - GUILayout.Space(10); - GUILayout.Label("CREWMATE ROLES (Blue Team)", headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Crewmate", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Crewmate; } - if (GUILayout.Button("Engineer", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Engineer; } - if (GUILayout.Button("Scientist", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Scientist; } - if (GUILayout.Button("Tracker", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)10; } - GUILayout.EndHorizontal(); - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Noisemaker", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)8; } - if (GUILayout.Button("Guardian Angel", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.GuardianAngel; } - if (GUILayout.Button("Detective", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)12; } - GUILayout.EndHorizontal(); - GUILayout.Space(15); - if (GUILayout.Button("REMOVE FORCED ROLE", activeTabStyle, GUILayout.Height(35))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Remove(target.PlayerId); } - GUILayout.Space(20); - GUILayout.Label("Hide & Seek Notice:\nВыбор Impostor/Shapeshifter/Phantom/Viper расширит лимит маньяков (Seekers) в Прятках!", new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true }); - } - else - { - GUILayout.FlexibleSpace(); - GUILayout.Label("Select a player to set their role", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); - GUILayout.FlexibleSpace(); - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - } - - private void DrawMenuTab() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("MENU CUSTOMIZATION", headerStyle); - - GUILayout.BeginHorizontal(); - // Вставили плитку вместо текста! - if (DrawBindableButton("Menu Toggle Key", "Toggle Menu", 200f)) { } - GUILayout.EndHorizontal(); - - GUILayout.Space(10); // Отступ чтобы не слипалось - - bool prevRgb = rgbMenuMode; - rgbMenuMode = DrawToggle(rgbMenuMode, "RGB Menu Mode"); - if (prevRgb && !rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); - - GUILayout.Space(5); // Отступ между тумблерами - - bool prevBg = enableBackground; - enableBackground = DrawToggle(enableBackground, "Enable Image Background"); - if (enableBackground && !prevBg) LoadBackgroundImage(); - - GUILayout.Space(5); - GUILayout.Label("Put 'MenuBG.png' or .jpg in BepInEx/config to add a background image.", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11 }); - - GUILayout.Space(10); - - GUILayout.BeginHorizontal(); - GUIStyle middleColorStyle = new GUIStyle(btnStyle) { normal = { background = null, textColor = currentAccentColor }, fontStyle = FontStyle.Bold }; - GUI.enabled = !rgbMenuMode; - if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex--; if (currentMenuColorIndex < 0) currentMenuColorIndex = menuColors.Length - 1; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); } - GUILayout.Label(menuColorNames[currentMenuColorIndex], middleColorStyle, GUILayout.Width(110), GUILayout.Height(25)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex++; if (currentMenuColorIndex >= menuColors.Length) currentMenuColorIndex = 0; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); } - GUI.enabled = true; - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.Space(10); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("SPOOF MENU IDENTITY", headerStyle); - SpoofMenuEnabled = DrawToggle(SpoofMenuEnabled, "Enable Fake RPC"); - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = currentAccentColor } }; - if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex--; if (selectedSpoofMenuIndex < 0) selectedSpoofMenuIndex = spoofMenuNames.Length - 1; } - GUILayout.Label($"{spoofMenuNames[selectedSpoofMenuIndex]}", middleLabelStyle, GUILayout.Width(110), GUILayout.Height(25)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex++; if (selectedSpoofMenuIndex >= spoofMenuNames.Length) selectedSpoofMenuIndex = 0; } - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - } - private Vector2 outfitsScrollPos = Vector2.zero; - - private void DrawOutfitsTab() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("COPY SPECIFIC PLAYER", headerStyle); - - outfitsScrollPos = GUILayout.BeginScrollView(outfitsScrollPos); - if (lockedPlayersList.Count > 0) - { - foreach (var pc in lockedPlayersList) - { - // Пропускаем себя и пустых игроков - if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null) continue; - - GUILayout.BeginHorizontal(boxStyle); - try - { - string pName = pc.Data.PlayerName ?? "Unknown"; - GUILayout.Label(pName, GUILayout.Width(150)); - - if (GUILayout.Button("Copy Outfit", btnStyle, GUILayout.Height(25))) - { - try - { - // Копируем всё, КРОМЕ ColorId - PlayerControl.LocalPlayer.RpcSetSkin(pc.Data.DefaultOutfit.SkinId); - PlayerControl.LocalPlayer.RpcSetHat(pc.Data.DefaultOutfit.HatId); - PlayerControl.LocalPlayer.RpcSetVisor(pc.Data.DefaultOutfit.VisorId); - PlayerControl.LocalPlayer.RpcSetNamePlate(pc.Data.DefaultOutfit.NamePlateId); - PlayerControl.LocalPlayer.RpcSetPet(pc.Data.DefaultOutfit.PetId); - } - catch { } - } - } - finally { GUILayout.EndHorizontal(); } - GUILayout.Space(2); - } - } - else - { - GUILayout.Label("Нет игроков для копирования."); - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - } - // ========================================== - // === UPDATE И ONGUI === - // ========================================== - public void Start() - { - if (enableBackground) LoadBackgroundImage(); - UnlockCosmetics(); - LoadConfig(); // загружаем сохранённые настройки спуфера - } - - public void Update() - { - KeyCode toggleKey = keyBinds.ContainsKey("Toggle Menu") ? keyBinds["Toggle Menu"] : KeyCode.Insert; - if (Input.GetKeyDown(toggleKey) || Input.GetKeyDown(KeyCode.RightShift)) showMenu = !showMenu; - - if (stylesInited && rgbMenuMode) - { - rgbMenuHue += Time.deltaTime * 0.2f; - if (rgbMenuHue > 1f) rgbMenuHue -= 1f; - UpdateAccentColor(Color.HSVToRGB(rgbMenuHue, 1f, 1f)); - } - - if (PlayerControl.LocalPlayer != null) - { - // === ТЕЛЕПОРТ К КУРСОРУ (ПКМ) === - if (tpToCursor && Input.GetMouseButtonDown(1)) - { - if (Camera.main != null) - { - Vector3 mPos = Camera.main.ScreenToWorldPoint(Input.mousePosition); - mPos.z = PlayerControl.LocalPlayer.transform.position.z; - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(mPos); - } - } - - // === МУВМЕНТ ТОЛЬКО ДЛЯ NOCLIP === - try - { - // Если включен NoClip, двигаемся сквозь стены - if (noClip && PlayerControl.LocalPlayer.CanMove) - { - float moveX = Input.GetAxis("Horizontal") * (invertControls ? -1 : 1); - float moveY = Input.GetAxis("Vertical") * (invertControls ? -1 : 1); - - if (moveX != 0 || moveY != 0) - { - Vector3 moveDir = new Vector3(moveX, moveY, 0).normalized; - PlayerControl.LocalPlayer.transform.position += moveDir * (walkSpeed * 5f * Time.deltaTime); - } - } - } - catch { } - - // === ОБЕСПЕЧЕНИЕ НОУКЛИПА ЧЕРЕЗ ТРИГГЕР === - if (PlayerControl.LocalPlayer.Collider != null) - { - PlayerControl.LocalPlayer.Collider.isTrigger = noClip; - } - } - - // Сохранение и прочее - if (wasShowMenu && !showMenu) SaveConfig(); - wasShowMenu = showMenu; - - - // Spoof RPC - if (SpoofMenuEnabled && PlayerControl.LocalPlayer != null) - { - uiSpoofTimer += Time.deltaTime; - if (uiSpoofTimer >= 2f) - { - uiSpoofTimer = 0f; - byte rpc = spoofMenuRPCs[selectedSpoofMenuIndex]; - try - { - MessageWriter msg = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, rpc, SendOption.None, -1); - AmongUsClient.Instance.FinishRpcImmediately(msg); - } - catch { } - } - } - - // Freecam - if (freecam) - { - if (!_freecamActive && Camera.main != null) - { - var cam = Camera.main.gameObject.GetComponent(); - if (cam != null) { cam.enabled = false; cam.Target = null; } - _freecamActive = true; - } - if (PlayerControl.LocalPlayer != null) PlayerControl.LocalPlayer.moveable = false; - Vector3 movement = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0.0f); - if (Camera.main != null) Camera.main.transform.position += movement * 15f * Time.deltaTime; - } - else if (_freecamActive) - { - if (PlayerControl.LocalPlayer != null) PlayerControl.LocalPlayer.moveable = true; - if (Camera.main != null) - { - var cam = Camera.main.gameObject.GetComponent(); - if (cam != null && PlayerControl.LocalPlayer != null) { cam.enabled = true; cam.SetTarget(PlayerControl.LocalPlayer); } - } - _freecamActive = false; - } - - // Camera zoom - try - { - if (cameraZoom && Camera.main != null && Input.GetAxis("Mouse ScrollWheel") != 0f) - { - if (Input.GetAxis("Mouse ScrollWheel") < 0f) Camera.main.orthographicSize += 0.5f; - else if (Input.GetAxis("Mouse ScrollWheel") > 0f && Camera.main.orthographicSize > 3f) Camera.main.orthographicSize -= 0.5f; - } - } - catch { } - - // Rainbow target - try - { - if (rainbowPlayers.Count > 0 && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) - { - colorTimer += Time.deltaTime; - if (colorTimer > 0.15f) - { - colorTimer = 0f; - currentColorId++; - if (currentColorId > 17) currentColorId = 0; - foreach (var p in PlayerControl.AllPlayerControls) - if (p != null && p.Data != null && !p.Data.Disconnected && rainbowPlayers.Contains(p.PlayerId)) - p.RpcSetColor(currentColorId); - } - } - } - catch { } - - // Tracers - try - { - if (showTracers && PlayerControl.AllPlayerControls != null) - foreach (var pc in PlayerControl.AllPlayerControls) - if (pc != null) HandleTracer(pc, showTracers); - } - catch { } - - // Input editing - if (isEditingName || isEditingLevel || isEditingFakeStart) - { - foreach (char c in Input.inputString) - { - if (c == '\b') - { - if (isEditingName && customNameInput.Length > 0) customNameInput = customNameInput.Substring(0, customNameInput.Length - 1); - if (isEditingLevel && spoofLevelString.Length > 0) spoofLevelString = spoofLevelString.Substring(0, spoofLevelString.Length - 1); - if (isEditingFakeStart && fakeStartInput.Length > 0) fakeStartInput = fakeStartInput.Substring(0, fakeStartInput.Length - 1); - } - else if (c == '\n' || c == '\r') { isEditingName = false; isEditingLevel = false; isEditingFakeStart = false; } - else - { - if (isEditingName && customNameInput.Length < 15) customNameInput += c; - if (isEditingLevel && char.IsDigit(c) && spoofLevelString.Length < 6) spoofLevelString += c; - if (isEditingFakeStart && (char.IsDigit(c) || (c == '-' && fakeStartInput.Length == 0)) && fakeStartInput.Length < 8) fakeStartInput += c; - } - } - } - - // Auto unlock cosmetics - if (unlockFeatures && Time.frameCount % 300 == 0) UnlockCosmetics(); - - // Keybinds - if (bindingAction != "") - { - foreach (KeyCode vKey in System.Enum.GetValues(typeof(KeyCode))) - if (Input.GetKeyDown(vKey)) - { - if (vKey == KeyCode.Escape) keyBinds.Remove(bindingAction); - else keyBinds[bindingAction] = vKey; - bindingAction = ""; - break; - } - } - else - { - if (keyBinds.ContainsKey("Frame Shape") && Input.GetKeyDown(keyBinds["Frame Shape"])) - { - var target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedHydraPlayerId); - if (target != null) this.StartCoroutine(AttemptShapeshiftFrame(target).WrapToIl2Cpp()); - } - } - } - - - public void OnGUI() - { - if (Event.current.type == EventType.Layout) - { - lockedPlayersList.Clear(); - if (PlayerControl.AllPlayerControls != null) - foreach (var p in PlayerControl.AllPlayerControls) - if (p != null && p.Data != null && !p.Data.Disconnected && p.PlayerId < 100) - lockedPlayersList.Add(p); - } - - if (!stylesInited) InitStyles(); - - if (showMenu) - { - windowRect = GUI.Window(0, windowRect, (Action)DrawNjordMenu, "", windowStyle); - } - } - - private void DrawNjordMenu(int windowID) - { - if (Event.current.type == EventType.Repaint && tabTransitionProgress < 1f) - { - tabTransitionProgress += Time.unscaledDeltaTime * 8f; - if (tabTransitionProgress >= 1f) { tabTransitionProgress = 1f; currentTab = targetTabIndex; } - } - - if (enableBackground && customMenuBg != null) - { - GUI.color = new Color(0.6f, 0.6f, 0.6f, 0.8f); - GUIStyle bgStyle = new GUIStyle() { normal = { background = customMenuBg } }; - GUI.Box(new Rect(0, 0, windowRect.width, windowRect.height), GUIContent.none, bgStyle); - GUI.color = Color.white; - } - - GUILayout.BeginHorizontal(); - GUILayout.Label(ApplyMenuShimmer("NjordMenu v1.0"), titleStyle); - GUILayout.FlexibleSpace(); - if (GUILayout.Button("-", new GUIStyle(btnStyle) { fixedWidth = 20, fixedHeight = 18, margin = CreateRectOffset(0, 8, 6, 0) })) showMenu = false; - GUILayout.EndHorizontal(); - - GUI.color = new Color(1f, 1f, 1f, 0.1f); - GUI.Box(new Rect(0, 30, windowRect.width, 1), "", safeLineStyle); - GUI.color = Color.white; - - GUILayout.BeginArea(new Rect(0f, 31f, 130f, windowRect.height - 31f)); - GUILayout.BeginVertical(sidebarStyle, GUILayout.ExpandHeight(true)); - GUILayout.Space(5); - for (int i = 0; i < tabNames.Length; i++) - if (GUILayout.Button(tabNames[i], i == targetTabIndex ? activeSidebarBtnStyle : sidebarBtnStyle, GUILayout.Height(24))) - if (targetTabIndex != i) { targetTabIndex = i; tabTransitionProgress = 0f; scrollPosition = Vector2.zero; } - GUILayout.EndVertical(); - GUILayout.EndArea(); - - GUI.color = new Color(1f, 1f, 1f, 0.1f); - GUI.Box(new Rect(130, 31, 1, windowRect.height), "", safeLineStyle); - GUI.color = new Color(1f, 1f, 1f, tabTransitionProgress); - - GUILayout.BeginArea(new Rect(140f, 36f + ((1f - tabTransitionProgress) * 10f), windowRect.width - 150f, windowRect.height - 46f)); - scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, false, GUIStyle.none, GUI.skin.verticalScrollbar); - // Найди этот кусок внутри DrawNjordMenu и обнови его: - int tabToDraw = (tabTransitionProgress < 1f) ? targetTabIndex : currentTab; - if (tabToDraw == 0) DrawGeneralTab(); - else if (tabToDraw == 1) DrawSelfTab(); - else if (tabToDraw == 2) DrawVisualsTab(); - else if (tabToDraw == 3) DrawPlayersTab(); - else if (tabToDraw == 4) DrawRolesTab(); - else if (tabToDraw == 5) DrawSabotagesTab(); - else if (tabToDraw == 6) DrawHostOnlyTab(); - else if (tabToDraw == 7) DrawOutfitsTab(); // <--- НОВАЯ ВКЛАДКА - else if (tabToDraw == 8) DrawMenuTab(); // <--- Сдвинулось на 8 - GUILayout.EndScrollView(); - GUILayout.EndArea(); - - GUI.color = Color.white; - GUI.DragWindow(new Rect(0, 0, 10000, 30)); - } - - // ========================================== - // === ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ДЛЯ ВИЗУАЛОВ === - // ========================================== - public static string GetPlatform(ClientData client) - { - if (client == null || client.PlatformData == null) return "Unknown"; - Platforms platform = client.PlatformData.Platform; - switch ((int)platform) - { - case 1: return "Epic Games"; - case 2: return "Steam"; - case 3: return "MacOS"; - case 4: return "Microsoft Store"; - case 5: return "itch.io"; - case 6: return "iOS/iPadOS"; - case 7: return "Android"; - case 8: return "Nintendo Switch"; - case 9: return "Xbox"; - case 10: return "Playstation"; - default: return "Unknown"; - } - } - - public static Color GetRoleColor(int roleId, Color fallbackColor) - { - switch (roleId) - { - case 1: return new Color32(255, 0, 0, 255); - case 2: return new Color32(0, 0, 128, 255); - case 3: return new Color32(127, 255, 212, 255); - case 4: return new Color32(176, 196, 222, 255); - case 5: return new Color32(255, 140, 0, 255); - case 8: return new Color32(255, 105, 180, 255); - case 9: return new Color32(139, 0, 0, 255); - case 10: return new Color32(106, 90, 205, 255); - case 12: return new Color32(189, 183, 107, 255); - case 18: return new Color32(173, 255, 47, 255); - default: return fallbackColor; - } - } - - public static void HandleTracer(PlayerControl target, bool enable) - { - try - { - if (target == null || target.gameObject == null) return; - LineRenderer lr = target.GetComponent(); - if (!enable || PlayerControl.LocalPlayer == null || target == PlayerControl.LocalPlayer || target.Data == null || target.Data.Disconnected) - { if (lr != null) lr.enabled = false; return; } - if (target.Data.IsDead && !seeGhosts && !PlayerControl.LocalPlayer.Data.IsDead) - { if (lr != null) lr.enabled = false; return; } - if (lr == null) - { - lr = target.gameObject.AddComponent(); - lr.SetVertexCount(2); lr.SetWidth(0.02f, 0.02f); - try { if (HatManager.Instance != null) lr.material = HatManager.Instance.PlayerMaterial; } catch { } - } - lr.enabled = true; - Color tColor = target.Data.IsDead ? Color.gray : (target.Data.Role != null ? GetRoleColor((int)target.Data.Role.Role, target.Data.Role.TeamColor) : Color.white); - lr.SetColors(tColor, tColor); - lr.SetPosition(0, PlayerControl.LocalPlayer.transform.position); - lr.SetPosition(1, target.transform.position); - } - catch { } - } - - public static string GetESPNameTag(NetworkedPlayerInfo info, string originalName) - { - if (info == null) return originalName; - string newName = originalName; - if (seeRoles && info.Role != null) - { - string roleName = info.Role.Role.ToString(); - int roleId = (int)info.Role.Role; - if (roleId == 8) roleName = "Noisemaker"; - else if (roleId == 9) roleName = "Phantom"; - else if (roleId == 10) roleName = "Tracker"; - else if (roleId == 12) roleName = "Detective"; - else if (roleId == 18) roleName = "Viper"; - else if (roleName == "GuardianAngel") roleName = "Guardian Angel"; - Color customColor = GetRoleColor(roleId, info.Role.TeamColor); - string roleColor = ColorUtility.ToHtmlStringRGB(customColor); - newName = $"{roleName}\n{newName}"; - } - if (showPlayerInfo) - { - int level = 0; string platform = "Unknown"; string hostStr = ""; - try { level = (int)info.PlayerLevel + 1; } catch { } - try - { - var client = AmongUsClient.Instance.GetClientFromPlayerInfo(info); - if (client != null) { platform = GetPlatform(client); if (AmongUsClient.Instance.GetHost() == client) hostStr = "Host - "; } - } - catch { } - newName = $"{hostStr}Lv:{level} - {platform}\n{newName}"; - } - return newName; - } - } - - // ========================================== - // === ПАТЧИ === - // ========================================== - - [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] - public static class VersionShower_Start_Patch - { - public static void Postfix(VersionShower __instance) { if (__instance != null && __instance.text != null) __instance.text.text = NjordMenuGUI.ApplyMenuShimmer("NjordMenu By Meowchelo"); } - } - - [HarmonyPatch(typeof(PingTracker), nameof(PingTracker.Update))] - public static class PingTracker_Watermark_Patch - { - private static float _smoothFps = 0f; - private static int _smoothPing = 0; - private static float _updateTimer = 0f; - public static void Postfix(PingTracker __instance) - { - try - { - _updateTimer += Time.deltaTime; - if (_updateTimer >= 0.5f) { _smoothFps = 1f / Time.deltaTime; if (AmongUsClient.Instance != null) _smoothPing = AmongUsClient.Instance.Ping; _updateTimer = 0f; } - int num = Mathf.RoundToInt(_smoothFps); - string pingColor = ((_smoothPing < 80) ? "#00FF00" : ((_smoothPing < 400) ? "#FFFF00" : "#FF0000")); - string shimmerTitle = NjordMenuGUI.ApplyMenuShimmer("NjordMenu By Meowchelo "); - string finalString = $"{shimmerTitle} • PING: {_smoothPing} ms • FPS: {num}"; - if (AmongUsClient.Instance != null) - { - ClientData host = AmongUsClient.Instance.GetHost(); - if (host != null && host.Character != null) - { - string hostName = host.Character.Data.PlayerName ?? "Unknown"; - string shimmerHostName = NjordMenuGUI.ApplyMenuShimmer(hostName); - finalString += $" • Host: {shimmerHostName}"; - if (AmongUsClient.Instance.AmHost) finalString += " (You)"; - } - } - __instance.text.text = finalString; - __instance.text.alignment = TMPro.TextAlignmentOptions.Center; - __instance.aspectPosition.enabled = false; - float zPos = MeetingHud.Instance != null && MeetingHud.Instance.gameObject.activeInHierarchy ? -100f : -10f; - __instance.transform.localPosition = new Vector3(0f, -2.3f, zPos); - } - catch { } - } - } - - [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Update))] - public static class GameStartManager_Update_Patch - { - public static void Postfix(GameStartManager __instance) - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; - if (NjordMenuGUI.fakeStartCounterTroll) - { - try { sbyte[] arr = { -123, -100, -69, -42, 0, 42, 69, 100, 123 }; sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; PlayerControl.LocalPlayer.RpcSetStartCounter(b); __instance.SetStartCounter(b); } catch { } - } - else if (NjordMenuGUI.fakeStartCounterCustom && int.TryParse(NjordMenuGUI.fakeStartInput, out int custom)) - { - try { PlayerControl.LocalPlayer.RpcSetStartCounter(custom); __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); } catch { } - } - } - } - - [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] - public static class InfiniteGamePatch { public static bool Prefix() { try { if (NjordMenuGUI.neverEndGame && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) return false; } catch { } return true; } } - - [HarmonyPatch(typeof(IntroCutscene), "CoBegin")] - public static class IntroCutscene_CoBegin_Patch - { - public static void Prefix() - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; - if (NjordMenuGUI.enablePreGameRoleForce) - { - foreach (var kvp in NjordMenuGUI.forcedPreGameRoles) - { var target = GameData.Instance.GetPlayerById(kvp.Key)?.Object; if (target != null && target.Data.RoleType != kvp.Value) target.RpcSetRole(kvp.Value); } - foreach (byte impId in NjordMenuGUI.forcedImpostors) - { var target = GameData.Instance.GetPlayerById(impId)?.Object; if (target != null && target.Data.Role != null && !target.Data.Role.IsImpostor) target.RpcSetRole(RoleTypes.Impostor); } - } - } - } - - [HarmonyPatch(typeof(LogicRoleSelectionNormal), "AssignRolesForTeam")] - public static class RoleSelectionNormal_Patch - { - public static bool Prefix(Il2CppSystem.Collections.Generic.List players, IGameOptions opts, RoleTeamTypes team, ref int teamMax) - { - if (!NjordMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; - try - { - if ((int)team == 1) - { - int numImps = opts.GetInt((Int32OptionNames)1); - var impRoleTypes = new HashSet { 1, 5, 9, 18 }; - List allForced = new List(NjordMenuGUI.forcedImpostors); - foreach (var kvp in NjordMenuGUI.forcedPreGameRoles) if (impRoleTypes.Contains((int)kvp.Value) && !allForced.Contains(kvp.Key)) allForced.Add(kvp.Key); - if (allForced.Count > 0) numImps = allForced.Count; - else { if (numImps >= players.Count) numImps = players.Count - 1; if (numImps < 1) numImps = 1; } - int assigned = 0; - foreach (byte impId in allForced) - { - if (players.Count == 0 || assigned >= numImps) break; - var targetInfo = players.ToArray().FirstOrDefault(p => p.PlayerId == impId); - if (targetInfo != null && targetInfo.Object != null) - { - RoleTypes role = NjordMenuGUI.forcedPreGameRoles.ContainsKey(impId) ? NjordMenuGUI.forcedPreGameRoles[impId] : RoleTypes.Impostor; - targetInfo.Object.RpcSetRole(role, false); - players.Remove(targetInfo); - assigned++; - } - } - while (assigned < numImps && players.Count > 0) - { - int idx = UnityEngine.Random.Range(0, players.Count); - players[idx].Object.RpcSetRole(RoleTypes.Impostor, false); - players.RemoveAt(idx); - assigned++; - } - return false; - } - else if ((int)team == 0) - { - var crewRoleTypes = new HashSet { 0, 2, 3, 4, 8, 10, 12 }; - for (int i = players.Count - 1; i >= 0; i--) - { - var p = players[i]; - if (p != null && p.Object != null) - { - RoleTypes role = RoleTypes.Crewmate; - if (NjordMenuGUI.forcedPreGameRoles.ContainsKey(p.PlayerId) && crewRoleTypes.Contains((int)NjordMenuGUI.forcedPreGameRoles[p.PlayerId])) - role = NjordMenuGUI.forcedPreGameRoles[p.PlayerId]; - p.Object.RpcSetRole(role, false); - players.RemoveAt(i); - } - } - return false; - } - return true; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(LogicRoleSelectionHnS), "AssignRolesForTeam")] - public static class RoleSelectionHnS_Patch - { - public static bool Prefix(Il2CppSystem.Collections.Generic.List players, IGameOptions opts, RoleTeamTypes team, ref int teamMax) - { - if (!NjordMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; - if ((int)team != 1) return true; - try - { - int numImps = opts.GetInt((Int32OptionNames)1); - var impRoleTypes = new HashSet { 1, 5, 9, 18 }; - List allForced = new List(NjordMenuGUI.forcedImpostors); - foreach (var kvp in NjordMenuGUI.forcedPreGameRoles) if (impRoleTypes.Contains((int)kvp.Value) && !allForced.Contains(kvp.Key)) allForced.Add(kvp.Key); - if (allForced.Count > 0) numImps = allForced.Count; - else { if (numImps >= players.Count) numImps = players.Count - 1; if (numImps < 1) numImps = 1; } - int assigned = 0; - foreach (byte impId in allForced) - { - if (players.Count == 0 || assigned >= numImps) break; - var targetInfo = players.ToArray().FirstOrDefault(p => p.PlayerId == impId); - if (targetInfo != null) { targetInfo.Object.RpcSetRole((RoleTypes)1, false); players.Remove(targetInfo); assigned++; } - } - while (assigned < numImps && players.Count > 0) - { - int idx = UnityEngine.Random.Range(0, players.Count); - players[idx].Object.RpcSetRole((RoleTypes)1, false); - players.RemoveAt(idx); - assigned++; - } - return false; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SelectRoles))] - public static class RoleManager_SelectRoles_Patch - { - public static bool Prefix(RoleManager __instance) - { - if (!NjordMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; - try - { - var allPlayers = PlayerControl.AllPlayerControls.ToArray().Where(p => p != null && p.Data != null && !p.Data.Disconnected && !p.Data.IsDead).ToList(); - int numImps = 1; - try { numImps = GameOptionsManager.Instance.CurrentGameOptions.GetInt((Int32OptionNames)1); } catch { } - var impRoleTypes = new HashSet { 1, 5, 9, 18 }; - List impostors = new List(); - foreach (var p in allPlayers) - if (NjordMenuGUI.forcedImpostors.Contains(p.PlayerId) || (NjordMenuGUI.forcedPreGameRoles.ContainsKey(p.PlayerId) && impRoleTypes.Contains((int)NjordMenuGUI.forcedPreGameRoles[p.PlayerId]))) - impostors.Add(p); - if (impostors.Count > 0) numImps = impostors.Count; - else { if (numImps >= allPlayers.Count) numImps = allPlayers.Count - 1; if (numImps < 1) numImps = 1; } - System.Random rand = new System.Random(); - while (impostors.Count < numImps && allPlayers.Count > impostors.Count) - { - var available = allPlayers.Where(p => !impostors.Contains(p)).ToList(); - impostors.Add(available[rand.Next(available.Count)]); - } - List crewmates = allPlayers.Where(p => !impostors.Contains(p)).ToList(); - var impData = new Il2CppSystem.Collections.Generic.List(); - foreach (var i in impostors) impData.Add(i.Data); - var crewData = new Il2CppSystem.Collections.Generic.List(); - foreach (var c in crewmates) crewData.Add(c.Data); - IGameOptions opts = GameOptionsManager.Instance.CurrentGameOptions; - GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(impData, opts, (RoleTeamTypes)1, int.MaxValue, new Il2CppSystem.Nullable()); - GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(crewData, opts, (RoleTeamTypes)0, int.MaxValue, new Il2CppSystem.Nullable((RoleTypes)0)); - foreach (var kvp in NjordMenuGUI.forcedPreGameRoles) - { - if (kvp.Value != RoleTypes.Crewmate && kvp.Value != RoleTypes.Impostor) - { - var pc = allPlayers.FirstOrDefault(p => p.PlayerId == kvp.Key); - if (pc != null) RoleManager.Instance.SetRole(pc, kvp.Value); - } - } - foreach (var pc in allPlayers) if (pc.Data.Role != null) pc.Data.Role.Initialize(pc); - return false; - } - catch { return true; } - } - } - - // Фикс See Ghosts (уже есть в оригинале, оставляем) - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.TurnOnProtection))] - public static class PlayerControl_TurnOnProtection_Patch { public static void Prefix(ref bool visible) { if (NjordMenuGUI.seeGhosts) visible = true; } } - - [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.LateUpdate))] - public static class PlayerVisuals_LateUpdate_Patch - { - public static void Postfix(PlayerPhysics __instance) - { - if (__instance == null || __instance.myPlayer == null || __instance.myPlayer.Data == null) return; - try - { - if (NjordMenuGUI.seeGhosts && __instance.myPlayer.Data.IsDead && PlayerControl.LocalPlayer != null && !PlayerControl.LocalPlayer.Data.IsDead) - { - __instance.myPlayer.Visible = true; - var rend = __instance.myPlayer.GetComponent(); - if (rend != null) { Color c = rend.color; rend.color = new Color(c.r, c.g, c.b, 0.4f); } - } - var cosmetics = __instance.myPlayer.cosmetics; - var outfit = __instance.myPlayer.CurrentOutfit; - if (cosmetics != null && cosmetics.nameText != null && outfit != null) - { - cosmetics.SetName(NjordMenuGUI.GetESPNameTag(__instance.myPlayer.Data, outfit.PlayerName)); - if (NjordMenuGUI.seeRoles && NjordMenuGUI.showPlayerInfo) cosmetics.nameText.transform.localPosition = new Vector3(0f, 0.186f, 0f); - else if (NjordMenuGUI.seeRoles || NjordMenuGUI.showPlayerInfo) cosmetics.nameText.transform.localPosition = new Vector3(0f, 0.093f, 0f); - else cosmetics.nameText.transform.localPosition = new Vector3(0f, 0f, 0f); - } - } - catch { } - } - } - - [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Update))] - public static class ESP_MeetingHud - { - public static void Postfix(MeetingHud __instance) - { - try - { - if (__instance.playerStates == null) return; - foreach (var state in __instance.playerStates) - { - if (state == null) continue; - var data = GameData.Instance.GetPlayerById(state.TargetPlayerId); - if (data != null && !data.Disconnected && data.DefaultOutfit != null && state.NameText != null) - { - string espName = NjordMenuGUI.GetESPNameTag(data, data.DefaultOutfit.PlayerName ?? "???"); - if (!NjordMenuGUI.seeRoles && NjordMenuGUI.revealMeetingRoles && data.Role != null) - { - string roleName = data.Role.Role.ToString(); - int roleId = (int)data.Role.Role; - if (roleId == 8) roleName = "Noisemaker"; - else if (roleId == 9) roleName = "Phantom"; - else if (roleId == 10) roleName = "Tracker"; - else if (roleId == 12) roleName = "Detective"; - else if (roleId == 18) roleName = "Viper"; - else if (roleName == "GuardianAngel") roleName = "Guardian Angel"; - Color customColor = NjordMenuGUI.GetRoleColor(roleId, data.Role.TeamColor); - string roleColor = ColorUtility.ToHtmlStringRGB(customColor); - espName = $"{roleName}\n{espName}"; - } - state.NameText.text = espName; - bool showingExtra = NjordMenuGUI.seeRoles || NjordMenuGUI.revealMeetingRoles; - if (showingExtra && NjordMenuGUI.showPlayerInfo) { state.NameText.transform.localPosition = new Vector3(0.33f, 0.08f, 0f); state.NameText.transform.localScale = new Vector3(0.75f, 0.75f, 0.75f); } - else if (showingExtra || NjordMenuGUI.showPlayerInfo) { state.NameText.transform.localPosition = new Vector3(0.3384f, 0.1125f, -0.1f); state.NameText.transform.localScale = new Vector3(0.9f, 1f, 1f); } - else { state.NameText.transform.localPosition = new Vector3(0.3384f, 0.0311f, -0.1f); state.NameText.transform.localScale = new Vector3(0.9f, 1f, 1f); } - } - } - } - catch { } - } - } - - [HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetName))] - public static class ChatBubble_SetName_Patch - { - public static void Postfix(ChatBubble __instance) - { - if (!NjordMenuGUI.showPlayerInfo || __instance.playerInfo == null) return; - try - { - int level = 0; string platform = "Unknown"; string hostStr = ""; - try { level = (int)__instance.playerInfo.PlayerLevel + 1; } catch { } - try - { - var client = AmongUsClient.Instance.GetClientFromPlayerInfo(__instance.playerInfo); - if (client != null) { platform = NjordMenuGUI.GetPlatform(client); if (AmongUsClient.Instance.GetHost() == client) hostStr = "Host - "; } - } - catch { } - string extra = $" {hostStr}Lv:{level} - {platform}"; - if (!__instance.NameText.text.Contains("Lv:")) __instance.NameText.text += extra; - } - catch { } - } - } - - [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] - public static class FullBright_Patch - { - public static void Postfix(HudManager __instance) { if (__instance.ShadowQuad != null) __instance.ShadowQuad.gameObject.SetActive(!NjordMenuGUI.fullBright); } - } - - // НОВЫЙ ПАТЧ ДЛЯ ALWAYS CHAT (принудительное открытие чата) - [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] - public static class HudManager_Update_Patch - { - public static void Postfix(HudManager __instance) - { - try - { - if (NjordMenuGUI.alwaysChat && __instance.Chat != null) - __instance.Chat.gameObject.SetActive(true); - } - catch { } - } - } - - [HarmonyPatch(typeof(PlatformSpecificData), nameof(PlatformSpecificData.Serialize))] - public static class PlatformSpooferPatch { public static void Prefix(PlatformSpecificData __instance) { try { if (NjordMenuGUI.enablePlatformSpoof && __instance != null) __instance.Platform = NjordMenuGUI.platformValues[NjordMenuGUI.currentPlatformIndex]; } catch { } } } - - [HarmonyPatch(typeof(FullAccount), nameof(FullAccount.CanSetCustomName))] - public static class FullAccount_CanSetCustomName_Patch { public static void Prefix(ref bool canSetName) { try { if (NjordMenuGUI.unlockFeatures) canSetName = true; } catch { } } } - - [HarmonyPatch(typeof(AccountManager), nameof(AccountManager.CanPlayOnline))] - public static class AccountManager_CanPlayOnline_Patch { public static void Postfix(ref bool __result) { try { if (NjordMenuGUI.unlockFeatures) __result = true; } catch { } } } - - // Роль баффы - [HarmonyPatch(typeof(EngineerRole), "FixedUpdate")] - public static class EngineerCheatsPatch - { - public static void Postfix(EngineerRole __instance) - { - if (__instance.Player != PlayerControl.LocalPlayer) return; - if (NjordMenuGUI.endlessVentTime) __instance.inVentTimeRemaining = float.MaxValue; - if (NjordMenuGUI.noVentCooldown && __instance.cooldownSecondsRemaining > 0f) - { - __instance.cooldownSecondsRemaining = 0f; - var btn = DestroyableSingleton.Instance?.AbilityButton; - if (btn != null) { btn.ResetCoolDown(); btn.SetCooldownFill(0f); } - } - } - } - - [HarmonyPatch(typeof(ScientistRole), "Update")] - public static class ScientistCheatsPatch - { - public static void Postfix(ScientistRole __instance) - { - if (__instance.Player != PlayerControl.LocalPlayer) return; - if (NjordMenuGUI.noVitalsCooldown) __instance.currentCooldown = 0f; - if (NjordMenuGUI.endlessBattery) __instance.currentCharge = float.MaxValue; - } - } - - [HarmonyPatch(typeof(ShapeshifterRole), "FixedUpdate")] - public static class ShapeshifterDurationPatch - { - public static void Postfix(ShapeshifterRole __instance) { if (__instance.Player == PlayerControl.LocalPlayer && NjordMenuGUI.endlessSsDuration) __instance.durationSecondsRemaining = float.MaxValue; } - } - - [HarmonyPatch(typeof(ImpostorRole), "FindClosestTarget")] - public static class ImpostorRangePatch - { - public static bool Prefix(ImpostorRole __instance, ref PlayerControl __result) - { - if (!NjordMenuGUI.killReach) return true; - try - { - var target = PlayerControl.AllPlayerControls.ToArray() - .Where(p => p != null && __instance.IsValidTarget(p.Data) && !p.Data.IsDead && !p.Data.Disconnected) - .OrderBy(p => Vector2.Distance(p.transform.position, PlayerControl.LocalPlayer.transform.position)) - .FirstOrDefault(); - if (target != null) __result = target; - return false; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(ImpostorRole), "IsValidTarget")] - public static class ImpostorKillAnyonePatch - { - public static void Postfix(NetworkedPlayerInfo target, ref bool __result) { try { if (NjordMenuGUI.killAnyone && target != null && target.PlayerId != PlayerControl.LocalPlayer.PlayerId && !target.IsDead) __result = true; } catch { } } - } - - [HarmonyPatch(typeof(PlayerControl), "CmdCheckShapeshift")] - public static class ShapeshiftAnimPatch1 { public static void Prefix(ref bool shouldAnimate) { if (NjordMenuGUI.NoShapeshiftAnim) shouldAnimate = false; } } - [HarmonyPatch(typeof(PlayerControl), "CmdCheckRevertShapeshift")] - public static class ShapeshiftAnimPatch2 { public static void Prefix(ref bool shouldAnimate) { if (NjordMenuGUI.NoShapeshiftAnim) shouldAnimate = false; } } - - [HarmonyPatch(typeof(TrackerRole), "FixedUpdate")] - public static class TrackerCheatsPatch - { - public static void Postfix(TrackerRole __instance) - { - if (__instance.Player != PlayerControl.LocalPlayer) return; - if (NjordMenuGUI.EndlessTracking) __instance.durationSecondsRemaining = float.MaxValue; - if (NjordMenuGUI.NoTrackingCooldown && __instance.cooldownSecondsRemaining > 0f) - { - __instance.cooldownSecondsRemaining = 0f; - __instance.delaySecondsRemaining = 0f; - var btn = DestroyableSingleton.Instance?.AbilityButton; - if (btn != null) { btn.ResetCoolDown(); btn.SetCooldownFill(0f); } - } - } - } - - [HarmonyPatch(typeof(DetectiveRole), "FindClosestTarget")] - public static class DetectiveRangePatch - { - public static bool Prefix(DetectiveRole __instance, ref PlayerControl __result) - { - if (!NjordMenuGUI.UnlimitedInterrogateRange) return true; - try - { - var target = PlayerControl.AllPlayerControls.ToArray() - .Where(p => p != null && __instance.IsValidTarget(p.Data) && !p.Data.IsDead && !p.Data.Disconnected) - .OrderBy(p => Vector2.Distance(p.transform.position, PlayerControl.LocalPlayer.transform.position)) - .FirstOrDefault(); - if (target != null) __result = target; - return false; - } - catch { return true; } - } - } - - // Авто-открытие дверей - [HarmonyPatch(typeof(DoorBreakerGame), nameof(DoorBreakerGame.Start))] - public static class DoorBreakerGame_Start_Patch - { - public static bool Prefix(DoorBreakerGame __instance) - { - if (!NjordMenuGUI.autoOpenDoors) return true; - try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(__instance.MyDoor.Id | 64)); } catch { } - __instance.MyDoor.SetDoorway(true); __instance.Close(); - return false; - } - } - [HarmonyPatch(typeof(DoorCardSwipeGame), nameof(DoorCardSwipeGame.Begin))] - public static class DoorCardSwipeGame_Begin_Patch - { - public static bool Prefix(DoorCardSwipeGame __instance) - { - if (!NjordMenuGUI.autoOpenDoors) return true; - try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(__instance.MyDoor.Id | 64)); } catch { } - __instance.MyDoor.SetDoorway(true); __instance.Close(); - return false; - } - } - [HarmonyPatch(typeof(MushroomDoorSabotageMinigame), nameof(MushroomDoorSabotageMinigame.Begin))] - public static class MushroomDoorSabotageMinigame_Begin_Patch - { - public static bool Prefix(MushroomDoorSabotageMinigame __instance) { if (NjordMenuGUI.autoOpenDoors) { __instance.FixDoorAndCloseMinigame(); return false; } return true; } - } - - // No Task Mode - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetTasks))] - public static class NoTaskMode_Patch { public static bool Prefix(PlayerControl __instance) { if (NjordMenuGUI.noTaskMode) return false; return true; } } - - // ========================================== - // === ФИКСЫ CHAT: КОМАНДЫ /color, /w И GHOST CHAT === - // ========================================== - [HarmonyPatch(typeof(ChatController), nameof(ChatController.SendChat))] - public static class ChatController_SendChat_Patch - { - public static bool Prefix(ChatController __instance) - { - if (__instance.freeChatField == null || __instance.freeChatField.textArea == null) return true; - string text = __instance.freeChatField.textArea.text; - if (string.IsNullOrWhiteSpace(text)) return true; - - string lowerChat = text.ToLower().Trim(); - - // === 1. ЛОКАЛЬНАЯ СМЕНА СВОЕГО ЦВЕТА (/color или /c) === - if (lowerChat.StartsWith("/color ") || lowerChat.StartsWith("/c ") || lowerChat.StartsWith("/col ")) - { - string arg = lowerChat.Substring(lowerChat.IndexOf(' ') + 1).Trim(); - int colorId = -1; - if (int.TryParse(arg, out int parsed)) colorId = parsed; - else colorId = NjordMenuGUI.GetColorIdByName(arg); - - if (colorId >= 0 && colorId <= 18 && PlayerControl.LocalPlayer != null) - { - PlayerControl.LocalPlayer.RpcSetColor((byte)colorId); - } - else if (PlayerControl.LocalPlayer != null && HudManager.Instance?.Chat != null) - { - HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "Неверный цвет. Используйте ID (0-18) или названия (red, blue...)"); - } - __instance.freeChatField.textArea.SetText("", ""); - return false; - } - - // === 2. КОМАНДА ШЕПОТА (/w или /pm) === - if (lowerChat.StartsWith("/w ") || lowerChat.StartsWith("/pm ")) - { - string[] parts = text.Split(new char[] { ' ' }, 3); - if (parts.Length >= 3) - { - string targetInput = parts[1].ToLower().Trim(); - string message = parts[2]; - PlayerControl target = null; - string[] colorNames = { "red", "blue", "green", "pink", "orange", "yellow", "black", "white", "purple", "brown", "cyan", "lime", "maroon", "rose", "banana", "gray", "tan", "coral", "fortegreen" }; - - if (byte.TryParse(targetInput, out byte pid)) - { - target = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p.PlayerId == pid); - } - else if (PlayerControl.AllPlayerControls != null) - { - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc.Data == null || pc.Data.Disconnected || pc == PlayerControl.LocalPlayer) continue; - string rawName = Regex.Replace(pc.Data.PlayerName, "<.*?>", string.Empty).ToLower().Trim(); - int cId = (int)pc.Data.DefaultOutfit.ColorId; - string cName = (cId >= 0 && cId < colorNames.Length) ? colorNames[cId] : ""; - if (rawName == targetInput || rawName.StartsWith(targetInput) || cName == targetInput) - { - target = pc; - break; - } - } - } - - if (target != null && target != PlayerControl.LocalPlayer) - { - string safeMessage = Regex.Replace(message, "<.*?>", string.Empty).Replace("<", "").Replace(">", ""); - string networkMsg = $"шепчет вам:\n{safeMessage}"; - - if (AmongUsClient.Instance != null && PlayerControl.LocalPlayer != null) - { - MessageWriter msgWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, 13, Hazel.SendOption.Reliable, target.OwnerId); - msgWriter.Write(networkMsg); - AmongUsClient.Instance.FinishRpcImmediately(msgWriter); - } - - string targetClean = Regex.Replace(target.Data.PlayerName, "<.*?>", string.Empty); - if (HudManager.Instance?.Chat != null) - HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, $"Вы шепчете {targetClean}:\n{safeMessage}"); - } - else if (HudManager.Instance?.Chat != null) - { - HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Игрок не найден!"); - } - } - __instance.freeChatField.textArea.SetText("", ""); - return false; - } - - return true; - } - } - - [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] - public static class ChatController_AddChat_Patch - { - public static bool Prefix(PlayerControl sourcePlayer, ref string chatText, ChatController __instance) - { - if (string.IsNullOrEmpty(chatText)) return true; - string lowerText = chatText.ToLower().Trim(); - - // === ПУБЛИЧНАЯ КОМАНДА СМЕНЫ ЦВЕТА ОТ ДРУГИХ ИГРОКОВ === - if (NjordMenuGUI.enableColorCommand && sourcePlayer != null) - { - string[] colorCommands = { "/color ", "!color ", "/col ", "!col ", "/c ", "!c " }; - string usedCmd = colorCommands.FirstOrDefault(cmd => lowerText.StartsWith(cmd)); - if (usedCmd != null) - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - string colorInput = lowerText.Substring(usedCmd.Length).Trim(); - int colorId = -1; - if (int.TryParse(colorInput, out int parsedId)) - { - if (parsedId >= 0 && parsedId <= 18) colorId = parsedId; - } - else colorId = NjordMenuGUI.GetColorIdByName(colorInput); - if (colorId != -1) sourcePlayer.RpcSetColor((byte)colorId); - } - return false; // скрываем команду - } - } - - // === GHOST CHAT (отображение сообщений мёртвых) === - if (sourcePlayer != null && sourcePlayer.Data != null && PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.Data != null) - { - bool weAreAlive = !PlayerControl.LocalPlayer.Data.IsDead; - bool senderIsDead = sourcePlayer.Data.IsDead; - - if (senderIsDead && weAreAlive && NjordMenuGUI.readGhostChat) - { - if (!chatText.StartsWith("[GHOST]")) - chatText = "[GHOST] " + chatText; - - ChatBubble pooledBubble = __instance.GetPooledBubble(); - try - { - pooledBubble.transform.SetParent(__instance.scroller.Inner); - pooledBubble.transform.localScale = Vector3.one; - pooledBubble.SetLeft(); - - bool didVote = MeetingHud.Instance && MeetingHud.Instance.DidVote(sourcePlayer.PlayerId); - pooledBubble.SetCosmetics(sourcePlayer.Data); - __instance.SetChatBubbleName(pooledBubble, sourcePlayer.Data, true, didVote, PlayerNameColor.Get(sourcePlayer.Data), null); - - if (AmongUs.Data.DataManager.Settings.Multiplayer.CensorChat) - chatText = BlockedWords.CensorWords(chatText, false); - - pooledBubble.SetText(chatText); - pooledBubble.AlignChildren(); - __instance.AlignAllBubbles(); - - if (!__instance.IsOpenOrOpening && __instance.notificationRoutine == null) - __instance.notificationRoutine = __instance.StartCoroutine(__instance.BounceDot()); - - if (!__instance.IsOpenOrOpening) - { - SoundManager.Instance.PlaySound(__instance.messageSound, false).pitch = 0.5f + (sourcePlayer.PlayerId / 15f); - __instance.chatNotification.SetUp(sourcePlayer, chatText); - } - } - catch (Exception) { __instance.chatBubblePool.Reclaim(pooledBubble); } - return false; - } - } - return true; - } - } - - [HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] - public static class ChatController_Update_Patch - { - public static void Postfix(ChatController __instance) - { - try - { - if (__instance.freeChatField != null && __instance.freeChatField.background != null) - { - __instance.freeChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); - if (__instance.freeChatField.textArea != null && __instance.freeChatField.textArea.outputText != null) - __instance.freeChatField.textArea.outputText.color = Color.white; - } - if (__instance.quickChatField != null && __instance.quickChatField.background != null) - { - __instance.quickChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); - if (__instance.quickChatField.text != null) - __instance.quickChatField.text.color = Color.white; - } - } - catch { } - } - } - - [HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetText))] - public static class DarkMode_ChatBubblePatch - { - public static void Postfix(ChatBubble __instance) - { - try - { - Transform bg = __instance.transform.Find("Background"); - if (bg != null) - { - var sr = bg.GetComponent(); - if (sr != null) sr.color = new Color32(0, 0, 0, 128); - } - if (__instance.TextArea != null) - __instance.TextArea.color = Color.white; - } - catch { } - } - } - - [HarmonyPatch(typeof(GameManager), nameof(GameManager.CheckTaskCompletion))] - public static class GameManager_CheckTaskCompletion_Patch - { - public static bool Prefix(ref bool __result) - { - try - { - if (!NjordMenuGUI.neverEndGame) return true; - __result = false; return false; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(ChatController), nameof(ChatController.SetVisible))] - public static class ChatController_SetVisible_Patch - { - public static void Prefix(ref bool visible) - { - if (NjordMenuGUI.alwaysChat) visible = true; - } - } - - // Reveal Votes - [HarmonyPatch(typeof(MeetingHud), "Update")] - public static class RevealVotesPatch - { - internal static List _votedPlayers = new List(); - public static void Prefix(MeetingHud __instance) - { - if (!NjordMenuGUI.RevealVotesEnabled) return; - try - { - if ((int)__instance.state >= 4) return; - foreach (var item in __instance.playerStates) - { - if (item == null) continue; - var playerById = GameData.Instance.GetPlayerById(item.TargetPlayerId); - if (playerById == null || playerById.Disconnected || item.VotedFor == PlayerVoteArea.HasNotVoted || - item.VotedFor == PlayerVoteArea.MissedVote || item.VotedFor == PlayerVoteArea.DeadVote || _votedPlayers.Contains(item.TargetPlayerId)) continue; - _votedPlayers.Add(item.TargetPlayerId); - if (item.VotedFor != PlayerVoteArea.SkippedVote) - { - foreach (var item2 in __instance.playerStates) if (item2.TargetPlayerId == item.VotedFor) { __instance.BloopAVoteIcon(playerById, 0, item2.transform); break; } - } - else if (__instance.SkippedVoting != null) __instance.BloopAVoteIcon(playerById, 0, __instance.SkippedVoting.transform); - } - foreach (var item3 in __instance.playerStates) - { - if (item3 == null) continue; - var component = item3.transform.GetComponent(); - if (component != null) foreach (var sprite in component.Votes) sprite.gameObject.SetActive(true); - } - if (__instance.SkippedVoting != null) __instance.SkippedVoting.SetActive(true); - } - catch { } - } - } - [HarmonyPatch(typeof(MeetingHud), "PopulateResults")] - public static class RevealVotesCleanupPatch - { - public static void Prefix(MeetingHud __instance) - { - if (!NjordMenuGUI.RevealVotesEnabled) return; - try - { - foreach (var item in __instance.playerStates) - { - if (item == null) continue; - var component = item.transform.GetComponent(); - if (component != null && component.Votes.Count != 0) - { - foreach (var sprite in component.Votes) Object.DestroyImmediate(sprite.gameObject); - component.Votes.Clear(); - } - } - RevealVotesPatch._votedPlayers.Clear(); - } - catch { } - } - } - - // ========================================== - // === НОВЫЕ ПАТЧИ: NO SETTING LIMITS === - // ========================================== - [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increase))] - public static class NumberOption_Increase_Patch - { - public static bool Prefix(NumberOption __instance) - { - try - { - if (!NjordMenuGUI.noSettingLimit) return true; - if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && - (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) - return true; - __instance.Value += __instance.Increment; - __instance.UpdateValue(); - __instance.OnValueChanged.Invoke(__instance); - __instance.AdjustButtonsActiveState(); - return false; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Decrease))] - public static class NumberOption_Decrease_Patch - { - public static bool Prefix(NumberOption __instance) - { - try - { - if (!NjordMenuGUI.noSettingLimit) return true; - if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && - (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) - return true; - __instance.Value -= __instance.Increment; - __instance.UpdateValue(); - __instance.OnValueChanged.Invoke(__instance); - __instance.AdjustButtonsActiveState(); - return false; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Initialize))] - public static class NumberOption_Initialize_Patch - { - public static void Postfix(NumberOption __instance) - { - try - { - if (!NjordMenuGUI.noSettingLimit) return; - if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && - (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) - return; - __instance.ValidRange = new FloatRange(-999f, 999f); - } - catch { } - } - } - - [HarmonyPatch(typeof(IGameOptionsExtensions), nameof(IGameOptionsExtensions.GetAdjustedNumImpostors))] - public static class IGameOptionsExtensions_GetAdjustedNumImpostors_Patch - { - public static bool Prefix(IGameOptions __instance, ref int __result) - { - try - { - if (!NjordMenuGUI.noSettingLimit) return true; - __result = GameOptionsManager.Instance.CurrentGameOptions.NumImpostors; - return false; - } - catch { return true; } - } - } - - // ========================================== - // === НОВЫЕ ПАТЧИ: EXTENDED LOBBY (15 слотов) === - // ========================================== - [HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.Start))] - public static class ExtendedLobbyListPatch - { - public static Scroller scroller; - - public static bool Prefix(FindAGameManager __instance) - { - if (!NjordMenuGUI.extendedLobby) return true; - try - { - if (__instance.gameContainers == null || __instance.gameContainers.Count == 0) return true; - if (__instance.gameContainers.Count > 10) return true; - - GameContainer prefab = __instance.gameContainers[0]; - GameObject holder = new GameObject("ExtendedLobbyScroller"); - holder.transform.SetParent(prefab.transform.parent); - - scroller = holder.AddComponent(); - scroller.Inner = holder.transform; - scroller.MouseMustBeOverToScroll = true; - scroller.allowY = true; - scroller.ScrollWheelSpeed = 0.4f; - scroller.SetYBoundsMin(0f); - scroller.SetYBoundsMax(4f); - - BoxCollider2D collider = prefab.transform.parent.gameObject.AddComponent(); - collider.size = new Vector2(100f, 100f); - scroller.ClickMask = collider; - - var list = new System.Collections.Generic.List(); - foreach (var gc in __instance.gameContainers) - { - gc.transform.SetParent(holder.transform); - gc.transform.localPosition = new Vector3(gc.transform.localPosition.x, gc.transform.localPosition.y, 25f); - list.Add(gc); - } - - for (int i = 0; i < 15; i++) - { - GameContainer newGc = UnityEngine.Object.Instantiate(prefab, holder.transform); - newGc.transform.localPosition = new Vector3(newGc.transform.localPosition.x, newGc.transform.localPosition.y - 0.75f * list.Count, 25f); - list.Add(newGc); - } - - __instance.gameContainers = new Il2CppReferenceArray(list.ToArray()); - return true; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.RefreshList))] - public static class ExtendedLobbyRefreshPatch - { - public static void Postfix() - { - try { if (NjordMenuGUI.extendedLobby && ExtendedLobbyListPatch.scroller != null) ExtendedLobbyListPatch.scroller.ScrollRelative(new Vector2(0f, -100f)); } catch { } - } - } -} -[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CanMove), MethodType.Getter)] -public static class NoClipMove_Patch -{ - public static void Postfix(ref bool __result) - { - // Исправлено: NGUI заменено на NjordMenuGUI - if (NjordMenuGUI.noClip) __result = true; - } -} - -[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.FixedUpdate))] -public static class SafeMovement_Patch -{ - public static void Postfix(PlayerPhysics __instance) - { - // Проверяем, что это наш персонаж и сейчас НЕ включен NoClip - if (__instance.AmOwner && !NjordMenuGUI.noClip && __instance.body != null) - { - // Берем чистую скорость, которую только что посчитала сама игра (учитывая стены) - Vector2 currentVelocity = __instance.body.velocity; - - // Если включена инверсия управления - if (NjordMenuGUI.invertControls) - { - currentVelocity = -currentVelocity; - } - - // Если включен спидхак, умножаем физическую скорость - if (NjordMenuGUI.walkSpeed > 1f) - { - currentVelocity *= NjordMenuGUI.walkSpeed; - - // Включаем непрерывную коллизию, чтобы на дикой скорости не пролетать сквозь тонкие стены - __instance.body.collisionDetectionMode = CollisionDetectionMode2D.Continuous; - } - - // Отдаем просчитанную скорость обратно движку - __instance.body.velocity = currentVelocity; - } - } -} \ No newline at end of file diff --git a/ClassLibrary10.slnx b/ClassLibrary10.slnx deleted file mode 100644 index 1ce94a5..0000000 --- a/ClassLibrary10.slnx +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs new file mode 100644 index 0000000..e7d85fb --- /dev/null +++ b/ElysiumModMenu.cs @@ -0,0 +1,8830 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219 + +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using ElysiumModMenu; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; +using System.Runtime.CompilerServices; +namespace ElysiumModMenu +{ + [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.0")] + public class Plugin : BasePlugin + { + public static Plugin Instance { get; private set; } = null!; + public static string ElysiumFolder = ""; + public static ConfigFile MenuConfig; + public static ConfigEntry RpcSpoofDelayConfig; + public static ConfigEntry MenuKeybind; + public static ConfigEntry SpoofedLevel; + public static ConfigEntry EnableFriendCodeSpoofConfig; + public static ConfigEntry SpoofFriendCodeConfig; + public static ConfigEntry EnablePlatformSpoof; + public static ConfigEntry AutoBanBrokenFriendCodeConfig; + public static ConfigEntry PlatformIndex; + public static ConfigEntry ShowWatermarkConfig; + public static ConfigEntry MenuColorIndexConfig; + public static ConfigEntry RgbMenuModeConfig; + public static ConfigEntry UnlockCosmeticsConfig; + public static ConfigEntry MoreLobbyInfoConfig; + public static ConfigEntry EnableChatDarkModeConfig; + + public override void Load() + { + Instance = this; + + ElysiumFolder = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu"); + if (!System.IO.Directory.Exists(ElysiumFolder)) + { + System.IO.Directory.CreateDirectory(ElysiumFolder); + } + + string banFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumModMenuBanList.txt"); + if (!System.IO.File.Exists(banFile)) + { + System.IO.File.Create(banFile).Dispose(); + } + + MenuConfig = new ConfigFile(System.IO.Path.Combine(ElysiumFolder, "ElysiumModMenu.cfg"), true); + RpcSpoofDelayConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "RpcDelay", 4f, ""); + MenuKeybind = MenuConfig.Bind("ElysiumModMenu.GUI", "Keybind", KeyCode.Insert, ""); + SpoofedLevel = MenuConfig.Bind("ElysiumModMenu.Spoofing", "Level", "100", ""); + EnableFriendCodeSpoofConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "EnableFriendCodeSpoof", false, ""); + SpoofFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "FriendCode", "crewmate01", ""); + EnablePlatformSpoof = MenuConfig.Bind("ElysiumModMenu.Spoofing", "EnablePlatformSpoof", true, ""); + AutoBanBrokenFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Anticheat", "AutoBanBrokenFriendCode", false, ""); + PlatformIndex = MenuConfig.Bind("ElysiumModMenu.Spoofing", "PlatformIndex", 1, ""); + ShowWatermarkConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "ShowWatermark", true, ""); + MenuColorIndexConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "MenuColorIndex", 10, ""); + RgbMenuModeConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "RgbMenuMode", false, ""); + UnlockCosmeticsConfig = MenuConfig.Bind("ElysiumModMenu.General", "UnlockCosmetics", true, ""); + MoreLobbyInfoConfig = MenuConfig.Bind("ElysiumModMenu.Visuals", "MoreLobbyInfo", true, ""); + EnableChatDarkModeConfig = MenuConfig.Bind("ElysiumModMenu.Chat", "EnableChatDarkMode", true, "Turns the custom dark chat input and bubble colors on/off."); + + ClassInjector.RegisterTypeInIl2Cpp(); + + var guiObject = new GameObject("ElysiumModMenu_Object"); + UnityEngine.Object.DontDestroyOnLoad(guiObject); + guiObject.hideFlags = HideFlags.HideAndDontSave; + guiObject.AddComponent(); + + var harmony = new Harmony("com.elysiummodmenu.harmony"); + harmony.PatchAll(); + } + } + public class ElysiumModMenuGUI : MonoBehaviour + { + public static string[] spoofMenuNames = { "ElysiumModMenu", "HostGuard/TOH", "Polar", "BanMod", "Better Among Us", "Sicko Menu", "GNC", "KillNetwork (V1)", "KillNetwork (V2)", "KNM" }; + public static byte[] spoofMenuRPCs = { 89, 176, 204, 212, 151, 164, 154, 85, 150, 162 }; + public static float rpcSpoofDelay = 4f; + + public static byte selectedMorphTargetId = 255; + public static bool unlockCosmetics = true; + public static bool moreLobbyInfo = true; + + public static Dictionary keyBinds = new Dictionary(); + public static string bindingAction = ""; + + public static string L(string eng, string rus) + { + try + { + if (DestroyableSingleton.InstanceExists) + { + string currentLang = DestroyableSingleton.Instance.currentLanguage.ToString().ToLower(); + if (currentLang.Contains("russian") || currentLang.Contains("ru")) + return rus; + } + } + catch { } + return eng; + } + + private int currentGeneralSubTab = 0; + private int currentGeneralInfoSubTab = 0; + private string[] generalSubTabs => new string[] { L("INFORMATION", "ИНФОРМАЦИЯ"), L("KEYBINDS", "БИНДЫ") }; + private string[] generalInfoSubTabs => new string[] { L("WELCOME", "WELCOME"), L("CREDITS", "АВТОРЫ") }; + + public static KeyCode menuToggleKey = KeyCode.Insert; + public static KeyCode bindMassMorph = KeyCode.None; + public static KeyCode bindSpawnLobby = KeyCode.None; + public static KeyCode bindDespawnLobby = KeyCode.None; + public static KeyCode bindCloseMeeting = KeyCode.None; + public static KeyCode bindInstaStart = KeyCode.None; + public static KeyCode bindEndCrew = KeyCode.None; + public static KeyCode bindEndImp = KeyCode.None; + public static KeyCode bindEndImpDC = KeyCode.None; + public static KeyCode bindEndHnsDC = KeyCode.None; + + private bool isScannerActiveFlag = false; + private bool isCamsActiveFlag = false; + public static bool isWaitingForBind = false; + public static bool isWaitBindMassMorph = false; + public static bool isWaitBindSpawnLobby = false; + public static bool isWaitBindDespawnLobby = false; + public static bool isWaitBindCloseMeeting = false; + public static bool isWaitBindInstaStart = false; + public static bool isWaitBindEndCrew = false; + public static bool isWaitBindEndImp = false; + public static bool isWaitBindEndImpDC = false; + public static bool isWaitBindEndHnsDC = false; + public static bool SpoofMenuEnabled = false; + public static int selectedSpoofMenuIndex = 0; + private float uiSpoofTimer = 0f; + public static bool noClip = false; + public static bool tpToCursor = false; + public static bool dragToCursor = false; + public static float walkSpeed = 1f; + + public static bool DetailedJoinInfo = true; + private static List lastPlayerIds = new List(); + private static Dictionary pendingJoinTimers = new Dictionary(); + public class PlayerHistoryEntry + { + public string Name; + public string FriendCode; + public string Puid; + public string Platform; + public int Level; + public DateTime FirstSeenUtc; + public DateTime LastSeenUtc; + } + private static List playerHistoryEntries = new List(); + private Vector2 playersHistoryScroll = Vector2.zero; + private int currentPlayersSubTab = 0; + private string[] playersSubTabs = { "ACTIONS", "HISTORY" }; + + public static float engineSpeed = 1f; + public static bool invertControls = false; + public static bool autoFollowCursor = false; + + public static int fakeRoleIdx = 0; + public static RoleTypes[] forceRoleOptions = { RoleTypes.Crewmate, RoleTypes.Impostor, RoleTypes.Engineer, RoleTypes.Scientist, RoleTypes.Shapeshifter, RoleTypes.GuardianAngel }; + public static RoleTypes[] roleAssignOptions = { + RoleTypes.Crewmate, RoleTypes.Impostor, RoleTypes.Engineer, RoleTypes.Scientist, RoleTypes.Shapeshifter, RoleTypes.GuardianAngel, + (RoleTypes)8, (RoleTypes)9, (RoleTypes)10, (RoleTypes)12, (RoleTypes)18 + }; + public static string[] roleAssignNames = { + "Crewmate", "Impostor", "Engineer", "Scientist", "Shapeshifter", "Guardian Angel", + "Noisemaker", "Phantom", "Tracker", "Detective", "Viper" + }; + private int targetRoleAssignIdx = 0; + private int allPlayersRoleAssignIdx = 0; + public static bool NoShapeshiftAnim = false; + public static bool EndlessTracking = false; + public static bool NoTrackingCooldown = false; + public static bool UnlimitedInterrogateRange = false; + public static bool noTaskMode = false; + public static bool killAuraHostOnly = false; + public static bool noKillCooldownHostOnly = false; + public static bool spamReportBodies = false; + private float killAuraTimer = 0f; + + public static bool enableColorCommand = false; + public static bool hostChatColor = false; + public static Color hostChatColorValue = new Color32(0, 128, 128, 255); + + public static bool showMenu = false; + public static Rect windowRect = new Rect(100, 100, 750, 480); + public static bool freecam = false; + private static bool _freecamActive = false; + public static bool cameraZoom = false; + public static bool RevealVotesEnabled = false; + + public static Color currentAccentColor = new Color(1f, 0.549f, 0f, 1f); + public static bool rgbMenuMode = false; + private float rgbMenuHue = 0f; + public static bool enableBackground = false; + public static Texture2D customMenuBg = null; + private bool wasShowMenu = false; + private int currentMenuColorIndex = 10; + private string[] menuColorNames = { + "Elysium Blue", "Dark Forest", "Green", "Sea Green", "Mint", "Chartreuse", + "Sun Yellow", "Marigold", "Old Gold", + "Bright Amber", "Vivid Orange", "Dark Orange", + "Blood Red", + "Hot Pink", "Pale Mauve", "Lilac", + "Lavender", "Deep Indigo", "Indigo", + "Med Slate Blue", "Slate Blue", "Navy", "Slate Grey", + "Arctic Cyan", "Neon Lime", "Royal Violet", "Crimson Glow", "Ocean Teal", + "Sunset Orange", "Rose Quartz", "Electric Blue", "Gold Ember", "Emerald Pulse", + "Midnight Steel", "Soft Lavender" + }; + + private Color[] menuColors = { + new Color32(51, 51, 255, 255), new Color(0.192f, 0.290f, 0.196f, 1f), new Color(0f, 0.502f, 0f, 1f), new Color(0.235f, 0.702f, 0.443f, 1f), new Color(0.243f, 0.706f, 0.537f, 1f), new Color(0.498f, 1f, 0f, 1f), + new Color(0.996f, 0.718f, 0.082f, 1f), new Color(0.812f, 0.651f, 0.004f, 1f), + new Color(0.996f, 0.612f, 0.063f, 1f), new Color(0.957f, 0.455f, 0.004f, 1f), new Color(1f, 0.549f, 0f, 1f), + new Color(0.871f, 0.071f, 0.149f, 1f), + new Color(0.992f, 0.529f, 0.859f, 1f), new Color(0.882f, 0.678f, 0.800f, 1f), new Color(0.784f, 0.635f, 0.784f, 1f), + new Color(0.925f, 0.686f, 0.996f, 1f), new Color(0.314f, 0.267f, 0.675f, 1f), new Color(0.294f, 0f, 0.51f, 1f), + new Color(0.482f, 0.408f, 0.933f, 1f), new Color(0.416f, 0.353f, 0.804f, 1f), new Color(0f, 0f, 0.502f, 1f), new Color(0.439f, 0.502f, 0.565f, 1f), + new Color32(72, 219, 251, 255), new Color32(163, 230, 53, 255), new Color32(124, 58, 237, 255), new Color32(239, 68, 68, 255), + new Color32(20, 184, 166, 255), new Color32(249, 115, 22, 255), new Color32(244, 114, 182, 255), new Color32(59, 130, 246, 255), + new Color32(245, 158, 11, 255), new Color32(16, 185, 129, 255), new Color32(51, 65, 85, 255), new Color32(196, 181, 253, 255) + }; + + public static float autoChatEveryoneDelay = 2.5f; + public static string customChatMessage = "test"; + public static bool customChatSpamEnabled = false; + public static float customChatSpamDelay = 2.1f; + public static bool customChatInputFocused = false; + private float customChatSpamTimer = 0f; + + public static float autoMeetingTimer = 0f; + private string[] tabNames => new string[] { L("GENERAL", "ОБЩИЕ"), L("SELF", "ИГРОК"), L("VISUALS", "ВИЗУАЛ"), L("PLAYERS", "ИГРОКИ"), L("SABOTAGES", "САБОТАЖИ"), L("HOST ONLY", "ХОСТ"), L("OUTFITS", "ОДЕЖДА"), L("VOTEKICK", "КИК"), L("MENU", "МЕНЮ"), L("MAPS", "КАРТЫ"), L("ANIMATIONS", "АНИМАЦИИ") }; + public static float speedMultiplier = 1f; + public static bool noSettingLimit = false; + public static float globalRoomColorId = 0f; + + private int currentHostOnlySubTab = 0; + private string[] hostOnlySubTabs => new string[] { L("LOBBY CONTROLS", "КОНТРОЛЬ ЛОББИ"), L("ROLE MANAGER", "МЕНЕДЖЕР РОЛЕЙ"), L("ANTI CHEAT", "АНТИ-ЧИТ"), L("AUTO HOST", "АВТО ХОСТ") }; + public static bool UseSnapToRPC = true; + private static bool isSkeldFlipped = false; + public static float selectedMapSpawnIdx = 0f; + public static string[] mapSpawnNames = { "The Skeld", "Mira HQ", "Polus", "The Airship", "The Fungle" }; + + public static bool FlippedSkeld + { + get { return isSkeldFlipped; } + set + { + if (AmongUsClient.Instance == null || isSkeldFlipped == value) return; + var temp = AmongUsClient.Instance.ShipPrefabs[3]; + AmongUsClient.Instance.ShipPrefabs[3] = AmongUsClient.Instance.ShipPrefabs[0]; + AmongUsClient.Instance.ShipPrefabs[0] = temp; + isSkeldFlipped = value; + } + } + + [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.Start))] + public static class AllowSymbols_TextBoxTMP_Start_Patch + { + public static void Postfix(TextBoxTMP __instance) + { + __instance.allowAllCharacters = true; + __instance.AllowSymbols = true; + + __instance.AllowEmail = true; + } + } + [HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] + public static class ChatJailbreak_ChatController_Update_Postfix + { + public static void Postfix(ChatController __instance) + { + if (__instance == null || __instance.freeChatField == null || __instance.freeChatField.textArea == null) return; + + if (ElysiumModMenuGUI.enableFastChat && __instance.timeSinceLastMessage < 0.9f) + { + __instance.timeSinceLastMessage = 0.9f; + } + + __instance.freeChatField.textArea.allowAllCharacters = true; + __instance.freeChatField.textArea.AllowSymbols = true; + __instance.freeChatField.textArea.AllowEmail = true; + + __instance.freeChatField.textArea.characterLimit = ElysiumModMenuGUI.enableExtendedChat ? 120 : 100; + } + } + [HarmonyPatch(typeof(ChatController), nameof(ChatController.SendFreeChat))] + public static class AllowURLS_ChatController_SendFreeChat_Patch + { + public static bool Prefix(ChatController __instance) + { + if (!ElysiumModMenuGUI.allowLinksAndSymbols) return true; + + string text = __instance.freeChatField.Text; + + if (!string.IsNullOrWhiteSpace(text)) + { + PlayerControl.LocalPlayer.RpcSendChat(text); + __instance.freeChatField.textArea.SetText(string.Empty, string.Empty); + } + + return false; + } + } + public static bool autoKickBugs = false; + public static float autoKickTimer = 5f; + public static Dictionary fortegreenTimer = new Dictionary(); + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetColor))] + public static class AutoKickBugs_Patch + { + public static void Postfix(PlayerControl __instance, byte bodyColor) + { + if (!ElysiumModMenuGUI.autoKickBugs || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + + try + { + if (__instance != null && __instance != PlayerControl.LocalPlayer && __instance.Data != null && !__instance.Data.Disconnected) + { + byte pid = __instance.PlayerId; + string colorName = Palette.GetColorName((int)bodyColor); + + if (bodyColor == 18 || colorName == "???" || bodyColor >= Palette.PlayerColors.Length) + { + if (!ElysiumModMenuGUI.fortegreenTimer.ContainsKey(pid)) + { + ElysiumModMenuGUI.fortegreenTimer[pid] = Time.time + ElysiumModMenuGUI.autoKickTimer; + } + } + else + { + if (ElysiumModMenuGUI.fortegreenTimer.ContainsKey(pid)) + { + ElysiumModMenuGUI.fortegreenTimer.Remove(pid); + } + } + } + } + catch { } + } + } + + [HarmonyPatch(typeof(VoteBanSystem), nameof(VoteBanSystem.HandleRpc))] + public static class VoteBanSystemPatch + { + public static bool Prefix(VoteBanSystem __instance, byte callId, Hazel.MessageReader reader) + { + if (!AmongUsClient.Instance.AmHost || !ElysiumModMenuGUI.disableVoteKicks) + return true; + + if (callId == 26) + { + reader.ReadInt32(); + reader.ReadInt32(); + + ElysiumModMenuGUI.ShowNotification("[SHIELD] Заблокирована попытка Vote-Kick'а!"); + + return false; + } + + return true; + } + } + public static bool disableVoteKicks = false; + + + [HarmonyPatch(typeof(ShhhBehaviour), nameof(ShhhBehaviour.PlayAnimation))] + public static class SkipShhh_Perfect_Patch + { + public static bool Prefix(ShhhBehaviour __instance, ref Il2CppSystem.Collections.IEnumerator __result) + { + if (!ElysiumModMenuGUI.skipShhhAnim || __instance == null) return true; + + __instance.gameObject.SetActive(false); + + __result = FastSkip().WrapToIl2Cpp(); + return false; + } + + private static System.Collections.IEnumerator FastSkip() { yield break; } + } + private void SpawnMap(int mapId) + { + try + { + if ((UnityEngine.Object)(object)AmongUsClient.Instance == (UnityEngine.Object)null || AmongUsClient.Instance.ShipPrefabs == null) + { + System.Console.WriteLine("[MAP] AmongUsClient or ShipPrefabs is null"); + return; + } + + int realMapId = mapId; + if (mapId == 3) realMapId = 4; + if (mapId == 4) realMapId = 5; + + if (realMapId >= AmongUsClient.Instance.ShipPrefabs.Count) + { + System.Console.WriteLine("[MAP] Invalid map ID"); + return; + } + + BepInEx.Unity.IL2CPP.Utils.MonoBehaviourExtensions.StartCoroutine(this, CoSpawnMap(realMapId)); + } + catch (Exception ex) + { + System.Console.WriteLine("[MAP ERROR] Failed to spawn map: " + ex.Message); + } + } + + [HideFromIl2Cpp] + private System.Collections.IEnumerator CoSpawnMap(int mapId) + { + AmongUsClient.Instance.ShipLoadingAsyncHandle = AmongUsClient.Instance.ShipPrefabs[mapId].InstantiateAsync((Transform)null, false); + yield return AmongUsClient.Instance.ShipLoadingAsyncHandle; + + ShipStatus.Instance = AmongUsClient.Instance.ShipLoadingAsyncHandle.Result.GetComponent(); + ((InnerNetClient)AmongUsClient.Instance).Spawn(((Component)ShipStatus.Instance).GetComponent(), -2, (SpawnFlags)0); + + System.Console.WriteLine($"[MAP] Map ID: {mapId} spawned successfully"); + } + + private void DespawnMap() + { + try + { + if (ShipStatus.Instance != null) + { + ShipStatus.Instance.Despawn(); + System.Console.WriteLine("[MAP] Map despawned successfully"); + } + } + catch (Exception ex) + { + System.Console.WriteLine("[MAP ERROR] Failed to despawn map: " + ex.Message); + } + } + + private void DespawnCurrentMap() + { + DespawnMap(); + } + + [HideFromIl2Cpp] + private System.Collections.IEnumerator CoSpawnOverlappedMap(int mapId) + { + yield return CoSpawnMap(mapId); + } + public static Dictionary skeldTeleportLocations = new Dictionary() +{ + { "Cafeteria", new Vector2(-0.78f, 2.48f) }, + { "Weapons", new Vector2(8.04f, 1.24f) }, + { "Navigation", new Vector2(16.59f, -2.33f) }, + { "O2", new Vector2(5.15f, -3.12f) }, + { "Shields", new Vector2(10.15f, -7.64f) }, + { "Communications", new Vector2(3.87f, -11.08f) }, + { "Storage", new Vector2(-1.92f, -6.14f) }, + { "Admin", new Vector2(5.31f, -7.42f) }, + { "Electrical", new Vector2(-3.37f, -4.84f) }, + { "Security", new Vector2(-5.69f, -3.07f) }, + { "Medbay", new Vector2(-8.61f, -4.30f) }, + { "Reactor", new Vector2(-20.19f, -2.48f) }, + { "Upper Engine", new Vector2(-16.84f, 2.47f) }, + { "Lower Engine", new Vector2(-16.48f, -7.53f) } +}; + + public static Dictionary miraTeleportLocations = new Dictionary() +{ + { "Launchpad", new Vector2(0.12f, -1.5f) }, + { "Medbay", new Vector2(10.2f, 15.1f) }, + { "Locker Room", new Vector2(12.5f, 18.5f) }, + { "Decontamination", new Vector2(14.8f, 22.0f) }, + { "Reactor", new Vector2(20.5f, 25.0f) }, + { "Laboratory", new Vector2(26.2f, 22.1f) }, + { "Office", new Vector2(24.5f, 15.2f) }, + { "Greenhouse", new Vector2(22.1f, 8.5f) }, + { "Admin", new Vector2(18.2f, 3.1f) }, + { "Cafeteria", new Vector2(14.5f, -2.1f) }, + { "Storage", new Vector2(9.8f, -6.5f) } +}; + + public static Dictionary polusTeleportLocations = new Dictionary() +{ + { "Dropship", new Vector2(0f, 0f) }, + { "Electrical", new Vector2(5.2f, 12.1f) }, + { "O2", new Vector2(-12.4f, 8.5f) }, + { "Security", new Vector2(-18.5f, 2.2f) }, + { "Decontamination", new Vector2(-25.2f, 1.5f) }, + { "Specimen Room", new Vector2(-30.1f, -5.2f) }, + { "Laboratory", new Vector2(-20.5f, -12.1f) }, + { "Medbay", new Vector2(-8.2f, -15.4f) }, + { "Communications", new Vector2(8.5f, -12.1f) }, + { "Weapons", new Vector2(15.2f, -2.5f) } +}; + + public static Dictionary airshipTeleportLocations = new Dictionary() +{ + { "Cockpit", new Vector2(-30f, 15f) }, + { "Vault", new Vector2(-15f, 15f) }, + { "Brig", new Vector2(-5f, 10f) }, + { "Meeting Room", new Vector2(10f, 12f) }, + { "Records", new Vector2(25f, 12f) }, + { "Lounge", new Vector2(35f, 8f) }, + { "Kitchen", new Vector2(25f, -5f) } +}; + + public static Dictionary fungleTeleportLocations = new Dictionary() +{ + { "Beach", new Vector2(0f, -20f) }, + { "Jungle", new Vector2(15f, 10f) }, + { "Lookout", new Vector2(-10f, 25f) }, + { "Laboratory", new Vector2(-25f, 0f) }, + { "Storage", new Vector2(5f, -5f) } +}; + public static int GetCurrentMapId() + { + if (AmongUsClient.Instance == null) return 0; + if (AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay) + { + return AmongUsClient.Instance.TutorialMapId; + } + else + { + if (GameOptionsManager.Instance == null || GameOptionsManager.Instance.CurrentGameOptions == null) return 0; + return GameOptionsManager.Instance.CurrentGameOptions.MapId; + } + } + private Vector2 mapsScrollPos = Vector2.zero; + public static Dictionary GetTeleportLocations() + { + switch (GetCurrentMapId()) + { + case 0: return skeldTeleportLocations; + case 1: return miraTeleportLocations; + case 2: return polusTeleportLocations; + case 3: return skeldTeleportLocations; + case 4: return airshipTeleportLocations; + case 5: return fungleTeleportLocations; + default: return skeldTeleportLocations; + } + } + + public static void TeleportTo(Vector2 position) + { + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.NetTransform == null) return; + if (UseSnapToRPC) + { + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(position); + } + else + { + PlayerControl.LocalPlayer.NetTransform.SnapTo(position); + } + } + + private int currentTab = 0; + private int targetTabIndex = 0; + private float tabTransitionProgress = 1f; + private Vector2 scrollPosition = Vector2.zero; + private void DrawAutoHostMainTab() + { + GUILayout.BeginHorizontal(); + for (int i = 0; i < autoHostSubTabs.Length; i++) + { + if (GUILayout.Button(autoHostSubTabs[i], currentAutoHostSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) + { + currentAutoHostSubTab = i; + scrollPosition = Vector2.zero; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(8); + + if (currentAutoHostSubTab == 0) DrawLobbyControls(); + else if (currentAutoHostSubTab == 1) DrawPlayersRoles(); + else if (currentAutoHostSubTab == 2) DrawAntiCheatTab(); + else if (currentAutoHostSubTab == 3) DrawAutoHostTab(); + } + + private void DrawMapsTab() + { + GUILayout.BeginVertical(boxStyle); + + GUILayout.Label(L("LOBBY CONTROL", "КОНТРОЛЬ ЛОББИ"), headerStyle); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(L("Spawn Lobby", "Создать лобби"), btnStyle, GUILayout.Height(30))) SpawnLobby(); + if (GUILayout.Button(L("Despawn Lobby", "Удалить лобби"), btnStyle, GUILayout.Height(30))) DespawnLobby(); + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + + GUILayout.Label(L("MAP CONTROL", "КОНТРОЛЬ КАРТЫ"), headerStyle); + isManualMapSpawn = DrawToggle(isManualMapSpawn, L("Manual Map Spawn Mode", "Ручной спавн карты"), 250); + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + GUILayout.Label(L("Select Map:", "Выбор карты:"), GUILayout.Width(100)); + selectedMapSpawnIdx = (int)GUILayout.HorizontalSlider((int)selectedMapSpawnIdx, 0, mapSpawnNames.Length - 1, sliderStyle, sliderThumbStyle, GUILayout.Width(200)); + GUILayout.Label($"{mapSpawnNames[(int)selectedMapSpawnIdx]}", new GUIStyle(GUI.skin.label) { richText = true }); + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button(L("Spawn Map", "Создать карту"), activeTabStyle, GUILayout.Height(30))) SpawnMap((int)selectedMapSpawnIdx); + if (GUILayout.Button(L("Despawn Map", "Удалить карту"), btnStyle, GUILayout.Height(30))) DespawnCurrentMap(); + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + + GUILayout.Label(L("ROOM TELEPORTS (IN-GAME)", "ТЕЛЕПОРТЫ ПО КОМНАТАМ (В ИГРЕ)"), headerStyle); + if (ShipStatus.Instance != null && PlayerControl.LocalPlayer != null) + { + mapsScrollPos = GUILayout.BeginScrollView(mapsScrollPos, GUILayout.Height(160)); + var locations = GetTeleportLocations(); + int columns = 3; + int count = 0; + + GUILayout.BeginHorizontal(); + foreach (var loc in locations) + { + if (GUILayout.Button(loc.Key, btnStyle, GUILayout.Width(135), GUILayout.Height(30))) + { + TeleportTo(loc.Value); + ShowNotification($"[TELEPORT] {L("Moved to:", "Перемещен в:")} {loc.Key}"); + } + + count++; + if (count % columns == 0) + { + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + } + } + GUILayout.EndHorizontal(); + GUILayout.EndScrollView(); + } + else + { + GUILayout.Label($"{L("Teleports are only available when you are on a map.", "Телепорты доступны только когда вы находитесь на карте.")}", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); + } + + GUILayout.EndVertical(); + } + + private void DrawChatSettingsTab() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label(L("CHAT SETTINGS & LOGS", "НАСТРОЙКИ ЧАТА И ЛОГИ"), headerStyle); + GUILayout.Space(10); + + string hexColor = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(GUILayout.Width(300)); + GUILayout.Label($"{L("LOCAL FEATURES", "ЛОКАЛЬНЫЕ ФУНКЦИИ")}", toggleLabelStyle); + GUILayout.Space(6); + alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 280); + GUILayout.Space(2); + readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 280); + GUILayout.Space(2); + enableExtendedChat = DrawToggle(enableExtendedChat, L("Extended Chat (120 chars)", "Длинный чат (120 симв.)"), 280); + GUILayout.Space(2); + enableFastChat = DrawToggle(enableFastChat, L("Fast Chat (3.1 to 2.1", "Быстрый чат (c 3.1 до 2.1)"), 280); + GUILayout.Space(2); + allowLinksAndSymbols = DrawToggle(allowLinksAndSymbols, L("Allow Links & Symbols", "Разрешить ссылки и символы"), 280); + GUILayout.Space(2); + enableSpellCheck = DrawToggle(enableSpellCheck, L("Spell Check (Basic)", "Проверка орфографии (Базовая)"), 280); + GUILayout.EndVertical(); + + GUILayout.BeginVertical(GUILayout.ExpandWidth(true)); + GUILayout.Label($"{L("UTILITY OPTIONS", "УТИЛИТЫ")}", toggleLabelStyle); + GUILayout.Space(6); + enableChatHistory = DrawToggle(enableChatHistory, L("Chat History (Up/Down)", "История чата (Стрелочки)"), 280); + GUILayout.Space(2); + enableClipboard = DrawToggle(enableClipboard, L("Clipboard (Ctrl+C/V)", "Буфер обмена (Ctrl+C/V)"), 280); + GUILayout.Space(2); + enableChatLog = DrawToggle(enableChatLog, L("Save Chat Log to File", "Сохранять лог чата в файл"), 280); + GUILayout.Space(2); + enableChatDarkMode = DrawToggle(enableChatDarkMode, L("Dark Chat Theme", "Темная тема чата"), 280); + + GUILayout.Space(8); + + GUILayout.Label($"{L("HOST LOBBY OPTIONS", "НАСТРОЙКИ ХОСТА")}", toggleLabelStyle); + GUILayout.Space(6); + enableColorCommand = DrawToggle(enableColorCommand, L("Enable /color command", "Разрешить команду /color"), 280); + GUILayout.Space(2); + blockFortegreenChat = DrawToggle(blockFortegreenChat, L("Block Fortegreen Chat", "Запрет чата Fortegreen"), 280); + GUILayout.Space(2); + blockRainbowChat = DrawToggle(blockRainbowChat, L("Block Rainbow Chat", "Запрет радужного чата"), 280); + GUILayout.EndVertical(); + + GUILayout.EndHorizontal(); + + GUILayout.Space(12); + + GUILayout.Label($"{L("CHAT SENDER", "ОТПРАВКА ЧАТА")}", toggleLabelStyle); + GUILayout.Space(6); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Space(6); + + GUIStyle macFieldStyle = new GUIStyle(GUI.skin.textField) + { + fontSize = 12, + alignment = TextAnchor.MiddleLeft + }; + macFieldStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); + macFieldStyle.padding = new RectOffset(); + macFieldStyle.padding.left = 12; + macFieldStyle.padding.right = 12; + macFieldStyle.padding.top = 8; + macFieldStyle.padding.bottom = 8; + macFieldStyle.margin = new RectOffset(); + macFieldStyle.margin.left = 4; + macFieldStyle.margin.right = 4; + macFieldStyle.margin.top = 4; + macFieldStyle.margin.bottom = 4; + + Rect chatInputRect = GUILayoutUtility.GetRect(10f, 34f, GUILayout.ExpandWidth(true), GUILayout.Height(34)); + GUI.Box(chatInputRect, string.Empty, macFieldStyle); + + string drawText = string.IsNullOrEmpty(customChatMessage) + ? L("Type a message...", "Введите сообщение...") + : customChatMessage; + + if (customChatInputFocused && (Time.unscaledTime % 1f) < 0.5f) + drawText += "|"; + + GUIStyle chatInputTextStyle = new GUIStyle(GUI.skin.label) + { + alignment = TextAnchor.MiddleLeft, + clipping = TextClipping.Clip, + richText = false, + fontSize = 12 + }; + chatInputTextStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); + + Rect textRect = new Rect(chatInputRect.x + 12f, chatInputRect.y + 4f, chatInputRect.width - 24f, chatInputRect.height - 8f); + GUI.Label(textRect, drawText, chatInputTextStyle); + + Event e = Event.current; + if (e != null) + { + if (e.type == EventType.MouseDown) + { + customChatInputFocused = chatInputRect.Contains(e.mousePosition); + if (customChatInputFocused) e.Use(); + } + else if (customChatInputFocused && e.type == EventType.KeyDown) + { + if (HandleClipboardShortcut(e, ref customChatMessage, 120)) + { + } + else if (e.keyCode == KeyCode.Backspace) + { + if (!string.IsNullOrEmpty(customChatMessage)) + customChatMessage = customChatMessage.Substring(0, customChatMessage.Length - 1); + e.Use(); + } + else if (e.keyCode == KeyCode.Escape) + { + customChatInputFocused = false; + e.Use(); + } + else if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) + { + TrySendCustomChatMessage(customChatMessage); + e.Use(); + } + else if (!char.IsControl(e.character)) + { + if (customChatMessage == null) customChatMessage = string.Empty; + if (customChatMessage.Length < 120) + customChatMessage += e.character; + e.Use(); + } + } + } + + GUILayout.Space(10); + + GUILayout.BeginHorizontal(GUILayout.Height(30)); + if (GUILayout.Button(L("Send Chat", "Отправить"), btnStyle, GUILayout.Width(150), GUILayout.Height(30))) + TrySendCustomChatMessage(customChatMessage); + + GUILayout.Space(10); + string spamBtnText = customChatSpamEnabled ? L("Spam: ON", "Спам: ВКЛ") : L("Spam: OFF", "Спам: ВЫКЛ"); + if (GUILayout.Button(spamBtnText, customChatSpamEnabled ? activeTabStyle : btnStyle, GUILayout.Width(150), GUILayout.Height(30))) + customChatSpamEnabled = !customChatSpamEnabled; + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.Space(12); + + GUILayout.BeginHorizontal(GUILayout.Height(24)); + GUILayout.Label($"{L("Delay:", "Задержка:")} {Mathf.Round(customChatSpamDelay * 10f) / 10f}s", new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(122)); + customChatSpamDelay = GUILayout.HorizontalSlider(customChatSpamDelay, 0.5f, 10f, sliderStyle, sliderThumbStyle, GUILayout.Width(300)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.Label($"{L("COMMANDS & INFO", "КОМАНДЫ И ИНФОРМАЦИЯ")}", toggleLabelStyle); + GUILayout.Space(4); + + GUILayout.Label($"{L("Whisper:", "Шепот:")} /w, /pm, /msg [Name/ID/Color] [Text]", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 12 }); + GUILayout.Label($"{L("Sends a private message to a player on your screen only.", "Отправляет личное сообщение выбранному игроку (видит только он и вы).")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); + + GUILayout.Space(6); + + GUILayout.Label($"Log Info: {L("ChatLog.txt clears every 3 game restarts.", "Файл ChatLog.txt очищается каждые 3 запуска игры.")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); + + GUILayout.EndVertical(); + } + + private void TrySendCustomChatMessage(string rawText) + { + if (string.IsNullOrWhiteSpace(rawText)) return; + if (PlayerControl.LocalPlayer == null) return; + + try + { + PlayerControl.LocalPlayer.RpcSendChat(rawText.Trim()); + } + catch { } + } + + private static readonly HashSet BasicSpellDictionary = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "hello","hi","gg","wp","yes","no","ok","pls","please","thanks","thx","go","come","start","skip","vote","report","body","kill","who","where","why", + "привет","да","нет","ок","пж","пожалуйста","спасибо","го","старт","скип","голос","репорт","труп","килл","кто","где","почему","лол" + }; + + private static void TrySpellCheckNotify(string text) + { + if (!enableSpellCheck || string.IsNullOrWhiteSpace(text)) return; + if (text.StartsWith("/") || text.StartsWith("!")) return; + + try + { + var words = Regex.Matches(text.ToLower(), @"[a-zа-яё]{3,}"); + List suspicious = new List(); + foreach (Match m in words) + { + string w = m.Value; + if (w.Length < 3) continue; + if (BasicSpellDictionary.Contains(w)) continue; + if (suspicious.Contains(w)) continue; + suspicious.Add(w); + if (suspicious.Count >= 4) break; + } + + if (suspicious.Count > 0) + { + string joined = string.Join(", ", suspicious); + ShowNotification($"[SPELL] Проверь слова: {joined}"); + } + } + catch { } + } + + private static void UpsertPlayerHistory(PlayerControl pc) + { + try + { + if (pc == null || pc.Data == null || pc.Data.Disconnected) return; + string name = string.IsNullOrEmpty(pc.Data.PlayerName) ? "Unknown" : pc.Data.PlayerName; + string fc = GetDisplayedFriendCode(pc.Data); + string puid = "Unknown"; + string platform = "Unknown"; + int level = 1; + + try + { + uint rawLevel = pc.Data.PlayerLevel; + if (rawLevel != uint.MaxValue && rawLevel < 10000) level = (int)rawLevel + 1; + } + catch { } + + try + { + var client = AmongUsClient.Instance?.GetClientFromCharacter(pc); + if (client != null) + { + platform = GetPlatform(client); + puid = client.Id.ToString(); + } + } + catch { } + + string key = $"{fc}|{puid}|{name}"; + var item = playerHistoryEntries.FirstOrDefault(x => $"{x.FriendCode}|{x.Puid}|{x.Name}" == key); + if (item == null) + { + playerHistoryEntries.Add(new PlayerHistoryEntry + { + Name = name, + FriendCode = fc, + Puid = puid, + Platform = platform, + Level = level, + FirstSeenUtc = DateTime.UtcNow, + LastSeenUtc = DateTime.UtcNow + }); + } + else + { + item.Name = name; + item.Platform = platform; + item.Level = level; + item.LastSeenUtc = DateTime.UtcNow; + } + } + catch { } + } + + private void TryHostOnlyKillAuraTick() + { + if (!killAuraHostOnly) + { + killAuraTimer = 0f; + return; + } + + if (AmongUsClient.Instance == null) return; + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) return; + if (PlayerControl.LocalPlayer.Data.IsDead) return; + if (!RoleManager.IsImpostorRole(PlayerControl.LocalPlayer.Data.RoleType)) return; + if (MeetingHud.Instance != null) return; + if (PlayerControl.LocalPlayer.inVent || PlayerControl.LocalPlayer.onLadder) return; + if (!noKillCooldownHostOnly && GetRemainingKillCooldown(PlayerControl.LocalPlayer.PlayerId) > 0.05f) return; + + killAuraTimer += Time.deltaTime; + if (killAuraTimer < 0.05f) return; + + if (PlayerControl.AllPlayerControls == null) return; + + PlayerControl nearestTarget = null; + float nearestDistance = float.MaxValue; + Vector3 localPos = PlayerControl.LocalPlayer.transform.position; + Vector2 localPos2D = new Vector2(localPos.x, localPos.y); + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null) continue; + if (pc.Data.Disconnected || pc.Data.IsDead) continue; + if (pc.inVent || pc.onLadder) continue; + + Vector3 targetPos = pc.transform.position; + float dist = Vector2.Distance(localPos2D, new Vector2(targetPos.x, targetPos.y)); + if (dist <= 2.2f && dist < nearestDistance) + { + nearestDistance = dist; + nearestTarget = pc; + } + } + + if (nearestTarget == null) return; + + try + { + PlayerControl.LocalPlayer.CmdCheckMurder(nearestTarget); + PlayerControl.LocalPlayer.RpcMurderPlayer(nearestTarget, true); + + if (AmongUsClient.Instance.AmHost) + PlayerControl.LocalPlayer.SetKillTimer(noKillCooldownHostOnly ? 0f : GetConfiguredKillCooldown()); + + killAuraTimer = 0f; + } + catch { } + } + + private void DrawAntiCheatTab() + { + float antiCheatColumnWidth = (windowRect.width - 220f) / 2f; + if (antiCheatColumnWidth < 250f) antiCheatColumnWidth = 250f; + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(boxStyle, GUILayout.Width(antiCheatColumnWidth)); + + GUILayout.Label(L("PUNISHMENT SYSTEM", "СИСТЕМА НАКАЗАНИЙ"), headerStyle); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + GUILayout.Label(L("Mode:", "Режим:"), toggleLabelStyle, GUILayout.Width(60)); + + GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) } }; + + if (GUILayout.Button("<", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) + { + punishmentMode--; + if (punishmentMode < 0) punishmentMode = punishmentNames.Length - 1; + } + + GUILayout.Label(punishmentNames[punishmentMode], middleLabelStyle, GUILayout.ExpandWidth(true), GUILayout.Height(25)); + + if (GUILayout.Button(">", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) + { + punishmentMode++; + if (punishmentMode >= punishmentNames.Length) punishmentMode = 0; + } + GUILayout.EndHorizontal(); + + string modeDesc = punishmentMode switch + { + 0 => "Null: Пакеты блокируются без действий.", + 1 => "Warn: Блокировка + Уведомление на экран.", + 2 => "Kick: Игрок будет исключен из лобби.", + 3 => "Ban: Игрок будет забанен (Host Only).", + _ => "" + }; + GUILayout.Label(modeDesc, new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); + + GUILayout.Space(15); + GUILayout.Label(L("RPC PROTECTIONS", "ЗАЩИТА RPC"), headerStyle); + + blockSpoofRPC = DrawToggle(blockSpoofRPC, "Block Spoof RPC", 250); + GUILayout.Space(5); + blockSabotageRPC = DrawToggle(blockSabotageRPC, "Block Sabotage & Meetings", 250); + GUILayout.Space(5); + blockGameRpcInLobby = DrawToggle(blockGameRpcInLobby, "Block Game RPC in Lobby", 250); + GUILayout.Space(5); + blockMeetingFloodRpc = DrawToggle(blockMeetingFloodRpc, "Block Meeting RPC Flood", 250); + GUILayout.Space(5); + blockChatFloodRpc = DrawToggle(blockChatFloodRpc, "Block Chat RPC Flood", 250); + + GUILayout.Space(15); + GUILayout.Label(L("OTHER PROTECTIONS", "ПРОЧАЯ ЗАЩИТА"), headerStyle); + + disableVoteKicks = DrawToggle(disableVoteKicks, L("Disable Vote Kicks (Host)", "Запрет кика голосованием (Хост)"), 250); + GUILayout.Space(5); + enableLocalPetSpamDrop = DrawToggle(enableLocalPetSpamDrop, L("Block Pet Spam (Local)", "Блок спама питомцем (Локально)"), 250); + GUILayout.Space(5); + enableHostPetSpamBan = DrawToggle(enableHostPetSpamBan, L("Auto-Ban Pet Spammers", "Авто-бан за спам питомцем"), 250); + GUILayout.Space(5); + + autoKickBugs = DrawToggle(autoKickBugs, L("Auto-Kick Fortegreen", "Авто-кик багнутых игроков"), 250); + if (autoKickBugs) + { + GUILayout.BeginHorizontal(); + autoKickTimer = GUILayout.HorizontalSlider(autoKickTimer, 1f, 15f, sliderStyle, sliderThumbStyle, GUILayout.Width(170)); + GUILayout.EndHorizontal(); + } + GUILayout.Space(5); + autoBanBrokenFriendCode = DrawToggle(autoBanBrokenFriendCode, L("Auto-Ban Broken FriendCode (Host)", "Авто-бан сломанного FriendCode (Хост)"), 250); + + GUILayout.EndVertical(); + GUILayout.Space(10); + + GUILayout.BeginVertical(boxStyle, GUILayout.Width(antiCheatColumnWidth), GUILayout.ExpandHeight(true)); + GUILayout.Label(L("BAN LIST", "БАН ЛИСТ"), headerStyle); + autoBanEnabled = DrawToggle(autoBanEnabled, L("Auto-Ban Blacklisted Players", "Авто-бан игроков из списка"), 250); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + string defaultBanText = L("Enter Friend Code", "Введите Friend Code"); + string banValue = string.IsNullOrEmpty(banInput) && !isEditingBan ? defaultBanText : banInput; + + if (DrawPseudoInputButton(banValue, isEditingBan, 25f, 46)) + { + isEditingBan = !isEditingBan; + ResetAllBindWaits(); + } + + if (GUILayout.Button(L("ADD", "ДОБАВИТЬ"), btnStyle, GUILayout.Width(75f), GUILayout.Height(25f))) + { + if (!string.IsNullOrWhiteSpace(banInput)) + { + AddToBanList(banInput.Trim(), "Manual", "Unknown", "Manual ban"); + banInput = ""; + isEditingBan = false; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + banListScroll = GUILayout.BeginScrollView(banListScroll); + + if (bannedEntries.Count == 0) + { + GUILayout.FlexibleSpace(); + GUILayout.Label($"{L("Ban list is empty.", "Бан лист пуст.")}", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); + GUILayout.FlexibleSpace(); + } + else + { + for (int i = 0; i < bannedEntries.Count; i++) + { + string entry = bannedEntries[i]; + if (string.IsNullOrWhiteSpace(entry)) continue; + + string[] parts = entry.Split('|'); + string disp = parts.Length >= 3 ? $"{parts[2]} ({parts[0]})" : entry; + + GUILayout.BeginHorizontal(boxStyle); + GUILayout.Label(disp, new GUIStyle(GUI.skin.label) { fontSize = 12 }, GUILayout.Width(185)); + GUILayout.FlexibleSpace(); + + GUIStyle redCrossStyle = new GUIStyle(btnStyle); + redCrossStyle.normal.textColor = new Color(1f, 0.3f, 0.3f); + + if (GUILayout.Button("X", redCrossStyle, GUILayout.Width(25), GUILayout.Height(22))) + { + RemoveFromBanList(entry); + break; + } + GUILayout.EndHorizontal(); + } + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + + GUILayout.EndHorizontal(); + } + + public static class ElysiumAnticheat + { + public static void Flag(PlayerControl player, string reason) + { + if (player == null || player.Data == null || player == PlayerControl.LocalPlayer) return; + + string pName = player.Data.PlayerName ?? "Unknown"; + + int mode = ElysiumModMenuGUI.punishmentMode; + + if (mode >= 1) + { + ElysiumModMenuGUI.ShowNotification($"[ANTICHEAT] {pName}: {reason}"); + System.Console.WriteLine($"[Anticheat] {pName} flagged for: {reason}"); + } + + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + if (mode == 2) + { + AmongUsClient.Instance.KickPlayer(player.OwnerId, false); + } + else if (mode == 3) + { + string fc = string.IsNullOrEmpty(player.Data.FriendCode) ? "Unknown" : player.Data.FriendCode; + string puid = "Unknown"; + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(player); + if (client != null) puid = client.Id.ToString(); + } + catch { } + + ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Anticheat: {reason}"); + + AmongUsClient.Instance.KickPlayer(player.OwnerId, true); + } + } + } + } + + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] + public static class Anticheat_PlayerControl_RPC + { + private static readonly Dictionary> chatRpcTimes = new Dictionary>(); + private static readonly Dictionary> meetingRpcTimes = new Dictionary>(); + private static readonly HashSet lobbyGameRpcs = new HashSet + { + (byte)RpcCalls.MurderPlayer, + (byte)RpcCalls.ReportDeadBody, + (byte)RpcCalls.StartMeeting, + (byte)RpcCalls.EnterVent, + (byte)RpcCalls.ExitVent, + (byte)RpcCalls.Shapeshift, + (byte)RpcCalls.ProtectPlayer + }; + + private static bool IsFlooded(Dictionary> map, byte playerId, int maxCalls, float windowSeconds) + { + float now = Time.unscaledTime; + if (!map.TryGetValue(playerId, out Queue times)) + { + times = new Queue(); + map[playerId] = times; + } + + times.Enqueue(now); + while (times.Count > 0 && now - times.Peek() > windowSeconds) + times.Dequeue(); + + return times.Count > maxCalls; + } + + public static bool Prefix(PlayerControl __instance, byte callId, Hazel.MessageReader reader) + { + if (!ElysiumModMenuGUI.blockSpoofRPC && + !ElysiumModMenuGUI.blockSabotageRPC && + !ElysiumModMenuGUI.blockGameRpcInLobby && + !ElysiumModMenuGUI.blockChatFloodRpc && + !ElysiumModMenuGUI.blockMeetingFloodRpc) return true; + if (__instance == null || __instance == PlayerControl.LocalPlayer || __instance.Data == null) return true; + + int oldPos = reader.Position; + bool isCheat = false; + string cheatReason = ""; + + try + { + if (ElysiumModMenuGUI.blockGameRpcInLobby && + AmongUsClient.Instance != null && + !AmongUsClient.Instance.IsGameStarted && + lobbyGameRpcs.Contains(callId)) + { + isCheat = true; + cheatReason = $"Game RPC in lobby ({((RpcCalls)callId)})"; + } + + if (!isCheat && ElysiumModMenuGUI.blockChatFloodRpc && + (callId == (byte)RpcCalls.SendChat || callId == (byte)RpcCalls.SendQuickChat)) + { + if (IsFlooded(chatRpcTimes, __instance.PlayerId, ElysiumModMenuGUI.chatRpcLimit, ElysiumModMenuGUI.chatRpcWindow)) + { + isCheat = true; + cheatReason = "Chat RPC flood"; + } + } + + if (!isCheat && ElysiumModMenuGUI.blockMeetingFloodRpc && + (callId == (byte)RpcCalls.StartMeeting || callId == (byte)RpcCalls.ReportDeadBody)) + { + if (IsFlooded(meetingRpcTimes, __instance.PlayerId, ElysiumModMenuGUI.meetingRpcLimit, ElysiumModMenuGUI.meetingRpcWindow)) + { + isCheat = true; + cheatReason = "Meeting RPC flood"; + } + } + + if (!isCheat && ElysiumModMenuGUI.blockSpoofRPC) + { + if (callId == (byte)RpcCalls.SetColor) + { + uint netId = reader.ReadUInt32(); + byte color = reader.ReadByte(); + if (color >= Palette.PlayerColors.Length) { isCheat = true; cheatReason = $"Invalid Color ID ({color})"; } + } + else if (callId == (byte)RpcCalls.SetName || callId == (byte)RpcCalls.CheckName) + { + uint netId = callId == (byte)RpcCalls.SetName ? reader.ReadUInt32() : 0; + string reqName = reader.ReadString(); + if (reqName.Length > 12) { isCheat = true; cheatReason = "Name length too long"; } + if (reqName.Contains("<")) { isCheat = true; cheatReason = "HTML Tags in name"; } + } + else if (callId == (byte)RpcCalls.SetScanner) + { + bool scanning = reader.ReadBoolean(); + if (scanning && RoleManager.IsImpostorRole(__instance.Data.RoleType)) + { isCheat = true; cheatReason = "Scanner activated as Impostor"; } + } + else if (callId == (byte)RpcCalls.PlayAnimation) + { + byte anim = reader.ReadByte(); + if (RoleManager.IsImpostorRole(__instance.Data.RoleType)) + { isCheat = true; cheatReason = "Task Animation as Impostor"; } + } + else if (callId == (byte)RpcCalls.EnterVent || callId == (byte)RpcCalls.ExitVent) + { + if (!__instance.Data.IsDead && __instance.Data.Role != null && !__instance.Data.Role.CanVent) + { isCheat = true; cheatReason = "Vent without vent ability"; } + + if (GameManager.Instance != null && GameManager.Instance.IsHideAndSeek() && RoleManager.IsImpostorRole(__instance.Data.RoleType)) + { isCheat = true; cheatReason = "Venting as Seeker in H&S"; } + } + } + + if (!isCheat && ElysiumModMenuGUI.blockSabotageRPC) + { + if (callId == (byte)RpcCalls.ReportDeadBody) + { + if (GameManager.Instance != null && GameManager.Instance.IsHideAndSeek()) + { isCheat = true; cheatReason = "Reported body in H&S"; } + } + else if (callId == (byte)RpcCalls.SetStartCounter) + { + reader.ReadPackedInt32(); + sbyte counter = reader.ReadSByte(); + + if (__instance.OwnerId != AmongUsClient.Instance.HostId && counter != -1) + { isCheat = true; cheatReason = "Start counter changed by non-host"; } + } + } + } + catch { } + + reader.Position = oldPos; + + if (isCheat) + { + ElysiumAnticheat.Flag(__instance, cheatReason); + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.HandleRpc))] + public static class Anticheat_ShipStatus_RPC + { + public static bool Prefix(ShipStatus __instance, byte callId, Hazel.MessageReader reader) + { + if (!ElysiumModMenuGUI.blockSabotageRPC) return true; + + int oldPos = reader.Position; + bool isCheat = false; + string cheatReason = ""; + PlayerControl sender = null; + + try + { + if (callId == (byte)RpcCalls.UpdateSystem) + { + SystemTypes system = (SystemTypes)reader.ReadByte(); + sender = reader.ReadNetObject(); + + if (sender != null && !sender.AmOwner) + { + if (system == SystemTypes.Sabotage) + { + SystemTypes sabSystem = (SystemTypes)reader.ReadByte(); + if (sender.Data != null && !RoleManager.IsImpostorRole(sender.Data.RoleType)) + { isCheat = true; cheatReason = "Triggered Sabotage as Crewmate"; } + } + } + } + else if (callId == (byte)RpcCalls.CloseDoorsOfType) + { + if (GameManager.Instance != null && GameManager.Instance.IsHideAndSeek()) + { isCheat = true; cheatReason = "Closed doors in H&S"; } + } + } + catch { } + + reader.Position = oldPos; + + if (isCheat && sender != null && sender != PlayerControl.LocalPlayer) + { + ElysiumAnticheat.Flag(sender, cheatReason); + return false; + } + + return true; + } + } + public static bool autoChatEveryone = false; + public static bool pendingAutoMeeting = false; + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Start))] + public static class Anticheat_Platform_Check + { + public static void Postfix(PlayerControl __instance) + { + if (!ElysiumModMenuGUI.blockSpoofRPC || __instance == null || __instance == PlayerControl.LocalPlayer) return; + + try + { + var clientData = AmongUsClient.Instance.GetClientFromCharacter(__instance); + if (clientData == null || clientData.PlatformData == null) return; + + var platform = clientData.PlatformData; + string pName = platform.PlatformName; + ulong xuid = platform.XboxPlatformId; + ulong psid = platform.PsnPlatformId; + + bool isValid = true; + + switch (platform.Platform) + { + case Platforms.StandaloneEpicPC: + case Platforms.StandaloneSteamPC: + case Platforms.StandaloneMac: + case Platforms.StandaloneItch: + case Platforms.IPhone: + case Platforms.Android: + isValid = (pName == "TESTNAME" && xuid == 0 && psid == 0); + break; + case Platforms.StandaloneWin10: + isValid = (pName == "TESTNAME" && xuid != 0 && psid == 0); + break; + case Platforms.Xbox: + isValid = (pName != "TESTNAME" && pName.Length >= 3 && xuid != 0 && psid == 0); + break; + case Platforms.Playstation: + isValid = (pName != "TESTNAME" && xuid == 0 && psid != 0); + break; + case Platforms.Switch: + isValid = (pName != "TESTNAME" && xuid == 0 && psid == 0); + break; + } + + if (!isValid) + { + ElysiumAnticheat.Flag(__instance, $"Platform Spoof detected ({platform.Platform})"); + } + } + catch { } + } + } + public static class ElysiumAutoLobbyReturn + { + private const float AutoReturnDelaySeconds = 3f; + private const float AutoReturnRetrySeconds = 0.4f; + private const int AutoReturnMaxAttempts = 40; + + private static int trackedEndGameId; + private static int exhaustedEndGameId; + private static int attempt; + private static float nextAttemptAt; + private static bool pending; + + public static void UpdateLogic() + { + if (!ShouldAutoReturn()) + { + ResetState(); + return; + } + if (LobbyBehaviour.Instance != null) + { + ResetState(); + return; + } + + EndGameManager val = UnityEngine.Object.FindObjectOfType(); + if (val != null) + { + int instanceID = val.gameObject.GetInstanceID(); + if (trackedEndGameId != instanceID) + { + trackedEndGameId = instanceID; + exhaustedEndGameId = 0; + attempt = 0; + nextAttemptAt = Time.unscaledTime + AutoReturnDelaySeconds; + pending = true; + } + } + else if (trackedEndGameId == 0) return; + + if (!pending || exhaustedEndGameId == trackedEndGameId || Time.unscaledTime < nextAttemptAt) + return; + + bool flag = false; + if (val != null) + { + flag = TryInvokeEndGameAction(val); + flag = TryClickEndGameButtons(val) || flag; + } + flag = TryClickGlobalReturnButtons() || flag; + + if (LobbyBehaviour.Instance != null) + { + ResetState(); + return; + } + + attempt++; + if (attempt >= AutoReturnMaxAttempts) + pending = false; + else + nextAttemptAt = Time.unscaledTime + AutoReturnRetrySeconds; + } + + public static void ResetState() + { + trackedEndGameId = 0; + exhaustedEndGameId = 0; + attempt = 0; + nextAttemptAt = 0f; + pending = false; + } + + private static bool ShouldAutoReturn() + { + return ElysiumModMenuGUI.AutoReturnLobbyAfterMatch || ElysiumAutoHostService.ShouldReturnAfterMatch; + } + + private static bool TryInvokeEndGameAction(EndGameManager manager) + { + if (manager == null) return false; + string[] methods = new string[] { "Continue", "NextGame", "PlayAgain" }; + for (int i = 0; i < methods.Length; i++) + { + System.Reflection.MethodInfo methodInfo = FindMethodNoWarn(manager.GetType(), methods[i], Type.EmptyTypes); + if (methodInfo != null) + { + try { methodInfo.Invoke(manager, null); return true; } + catch { } + } + } + return false; + } + + private static System.Reflection.MethodInfo FindMethodNoWarn(Type type, string name, Type[] parameters) + { + if (type == null || string.IsNullOrWhiteSpace(name)) return null; + Type[] types = parameters ?? Type.EmptyTypes; + Type t = type; + while (t != null) + { + System.Reflection.MethodInfo method = t.GetMethod(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic, null, types, null); + if (method != null) return method; + t = t.BaseType; + } + return null; + } + + private static bool TryClickEndGameButtons(EndGameManager manager) + { + if (manager == null) return false; + if (TryClickPassiveButtons(manager.GetComponentsInChildren(true), true)) + return true; + return TryClickUnityButtons(manager.GetComponentsInChildren(true), true); + } + + private static bool TryClickGlobalReturnButtons() + { + if (TryClickPassiveButtons(UnityEngine.Object.FindObjectsOfType(), true)) + return true; + return TryClickUnityButtons(UnityEngine.Object.FindObjectsOfType(), true); + } + + private static bool TryClickPassiveButtons(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase buttons, bool onlyActive) + { + if (buttons == null) return false; + foreach (PassiveButton btn in buttons) + { + if (btn == null) continue; + if (onlyActive && (!btn.gameObject.activeInHierarchy || !btn.isActiveAndEnabled)) + continue; + if (!IsLobbyReturnButton(btn.name, btn.GetComponentsInChildren(true))) + continue; + try + { + if (btn.OnClick != null) + { + btn.OnClick.Invoke(); + return true; + } + } + catch { } + } + return false; + } + + private static bool TryClickUnityButtons(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase buttons, bool onlyActive) + { + if (buttons == null) return false; + foreach (UnityEngine.UI.Button btn in buttons) + { + if (btn == null) continue; + if (onlyActive && (!btn.gameObject.activeInHierarchy || !btn.isActiveAndEnabled || !btn.interactable)) + continue; + if (!IsLobbyReturnButton(btn.name, btn.GetComponentsInChildren(true))) + continue; + try + { + if (btn.onClick != null) + { + btn.onClick.Invoke(); + return true; + } + } + catch { } + } + return false; + } + + private static bool IsLobbyReturnButton(string objectName, Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase texts) + { + string input = (objectName ?? string.Empty).ToLowerInvariant(); + if (ContainsAny(input, "exit", "quit", "menu", "back", "leave", "вых", "выйт", "назад")) + return false; + if (ContainsAny(input, "continue", "nextgame", "playagain", "returntolobby", "tolobby", "lobby", "again", "продолж", "занов", "снов", "лобби", "играть", "вернут")) + return true; + if (texts == null) return false; + foreach (TMPro.TMP_Text txt in texts) + { + if (txt == null) continue; + string stripped = StripRichText(txt.text).ToLowerInvariant(); + if (ContainsAny(stripped, "exit", "quit", "menu", "back", "leave", "вых", "выйт", "назад")) + return false; + if (ContainsAny(stripped, "continue", "next game", "play again", "return to lobby", "lobby", "again", "продолж", "занов", "снов", "лобби", "играть", "вернут")) + return true; + } + return false; + } + + private static bool ContainsAny(string input, params string[] tokens) + { + if (string.IsNullOrEmpty(input)) return false; + foreach (string token in tokens) + if (!string.IsNullOrWhiteSpace(token) && input.Contains(token)) + return true; + return false; + } + + private static string StripRichText(string input) + { + if (string.IsNullOrEmpty(input)) return string.Empty; + char[] chars = new char[input.Length]; + int length = 0; + bool inTag = false; + foreach (char c in input) + { + switch (c) + { + case '<': inTag = true; continue; + case '>': inTag = false; continue; + } + if (!inTag) chars[length++] = c; + } + return new string(chars, 0, length); + } + } + + public static class ElysiumAutoHostService + { + public sealed class AutoHostStatusSnapshot + { + public bool Enabled; + public bool IsHost; + public bool IsLobby; + public bool IsInGame; + public string State = string.Empty; + public string LastReason = string.Empty; + public int ConnectedPlayers; + public int ReadyPlayers; + public int RequiredPlayers; + public float CountdownRemainingSeconds; + public float BackoffRemainingSeconds; + public float LobbyAgeSeconds; + public float LobbyLifeRemainingSeconds = -1f; + public bool WaitingForLoadedPlayers; + public bool AutoReturnAfterMatch; + public bool ForceLastMinute; + public string StartMode = string.Empty; + public float EffectiveStartDelaySeconds; + public float WarmupRemainingSeconds; + public float LoadGraceRemainingSeconds; + public bool FastStartActive; + public bool ForceStartActive; + } + + private enum AutoHostState + { + Disabled, Idle, Warmup, WaitingPlayers, WaitingLoad, + Countdown, Starting, InGame, Returning, Backoff, + } + + private const float TickIntervalSeconds = 0.2f; + private const float StartRequestGraceSeconds = 7f; + private const float LobbyLifetimeSeconds = 600f; + private const float LastMinuteStartSeconds = 60f; + private const float NotificationCooldownSeconds = 0.75f; + + private static AutoHostState state = AutoHostState.Disabled; + private static string lastReason = "disabled"; + private static float nextTickAt; + private static float countdownStartedAt = -1f; + private static float activeCountdownDelay = -1f; + private static float backoffUntil = -1f; + private static float lastStartIssuedAt = -1f; + private static float lobbyOpenedAt = -1f; + private static float loadWaitStartedAt = -1f; + private static float lastNotificationAt = -1f; + private static int lobbyGameId = -1; + private static int lastCountdownNotice = -1; + + public static void Tick() + { + float now = Time.unscaledTime; + if (now < nextTickAt) return; + nextTickAt = now + TickIntervalSeconds; + + if (!IsEnabled) + { + ResetLobbyFlow(true); + SetState(AutoHostState.Disabled, "Выключен"); + return; + } + + InnerNetClient client = TryGetClient(); + if (client == null) + { + ResetLobbyFlow(false); + SetState(AutoHostState.Idle, "Клиент недоступен"); + return; + } + + if (!client.AmHost) + { + ResetLobbyFlow(false); + SetState(AutoHostState.Idle, "Ожидаю хост-контекст"); + return; + } + + if (IsEndGameScreen()) + { + ResetLobbyFlow(false); + SetState(ShouldReturnAfterMatch ? AutoHostState.Returning : AutoHostState.InGame, + ShouldReturnAfterMatch ? "Возврат в лобби" : "Матч завершен"); + return; + } + + if (IsInMatch()) + { + ResetLobbyFlow(true); + SetState(AutoHostState.InGame, "Матч идет"); + return; + } + + if (LobbyBehaviour.Instance == null) + { + ResetLobbyFlow(false); + lobbyOpenedAt = -1f; + lobbyGameId = -1; + SetState(AutoHostState.Idle, "Вне лобби"); + return; + } + + TrackLobby(client, now); + TickHostedLobby(client, now); + } + + public static AutoHostStatusSnapshot GetStatusSnapshot() + { + AutoHostStatusSnapshot snapshot = new AutoHostStatusSnapshot + { + Enabled = IsEnabled, + State = FormatState(state), + LastReason = lastReason ?? string.Empty, + RequiredPlayers = RequiredPlayers, + CountdownRemainingSeconds = CountdownRemaining, + BackoffRemainingSeconds = BackoffRemaining, + LobbyAgeSeconds = lobbyOpenedAt > 0f ? Mathf.Max(0f, Time.unscaledTime - lobbyOpenedAt) : 0f, + LobbyLifeRemainingSeconds = LobbyLifeRemaining, + AutoReturnAfterMatch = ShouldReturnAfterMatch, + ForceLastMinute = ForceLastMinuteEnabled, + StartMode = ElysiumModMenuGUI.AutoHostInstantStart ? "Мгновенный" : "Обычный", + EffectiveStartDelaySeconds = EffectiveStartDelay(0), + WarmupRemainingSeconds = WarmupRemaining, + LoadGraceRemainingSeconds = LoadGraceRemaining, + }; + InnerNetClient client = TryGetClient(); + if (client != null) + { + snapshot.IsHost = client.AmHost; + snapshot.IsLobby = LobbyBehaviour.Instance != null; + snapshot.IsInGame = IsInMatch(); + snapshot.ConnectedPlayers = CountLobbyPlayers(client, out int readyPlayers, out _); + snapshot.ReadyPlayers = readyPlayers; + snapshot.WaitingForLoadedPlayers = snapshot.ConnectedPlayers > snapshot.ReadyPlayers; + snapshot.FastStartActive = IsFastStartActive(snapshot.ConnectedPlayers); + snapshot.ForceStartActive = ShouldForceStart(snapshot.ConnectedPlayers, out _); + snapshot.EffectiveStartDelaySeconds = EffectiveStartDelay(snapshot.ConnectedPlayers); + } + return snapshot; + } + + public static void ResetTransientState() + { + nextTickAt = 0f; + ResetLobbyFlow(true); + SetState(IsEnabled ? AutoHostState.Idle : AutoHostState.Disabled, IsEnabled ? "Сброшен" : "Выключен"); + } + + public static string TryStartNow() + { + if (!IsEnabled) return "Автохост выключен."; + InnerNetClient client = TryGetClient(); + if (client == null || !client.AmHost) return "Ручной старт доступен только хосту."; + if (LobbyBehaviour.Instance == null) return "Ручной старт доступен только в лобби."; + GameStartManager manager = TryGetGameStartManager(); + if (manager == null) return "Кнопка старта не найдена."; + + if (!TryConfiguredStart(manager)) + { + EnterBackoff("Ручной старт отклонен"); + return "Старт не сработал."; + } + lastStartIssuedAt = Time.unscaledTime; + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + backoffUntil = -1f; + SetState(AutoHostState.Starting, "Ручной старт"); + Notify("Автохост", "Матч запускается вручную."); + return "Старт отправлен."; + } + + private static void TickHostedLobby(InnerNetClient client, float now) + { + int connectedPlayers = CountLobbyPlayers(client, out int readyPlayers, out string loadingName); + bool forceStart = ShouldForceStart(connectedPlayers, out string forceReason); + float warmupRemaining = WarmupRemaining; + + if (!forceStart && warmupRemaining > 0.05f) + { + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + lastStartIssuedAt = -1f; + lastCountdownNotice = -1; + SetState(AutoHostState.Warmup, $"Прогрев лобби {Mathf.CeilToInt(warmupRemaining)}с"); + return; + } + + bool waitingForLoad = ElysiumModMenuGUI.AutoHostWaitLoadedPlayers && connectedPlayers > readyPlayers; + if (waitingForLoad && !forceStart && !CanBypassLoadWait(now, readyPlayers, connectedPlayers, loadingName)) + { + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + lastStartIssuedAt = -1f; + lastCountdownNotice = -1; + SetState(AutoHostState.WaitingLoad, $"Ожидаю прогрузку {readyPlayers}/{connectedPlayers}: {loadingName}"); + return; + } + if (!waitingForLoad) loadWaitStartedAt = -1f; + + if (lastStartIssuedAt > 0f) + { + if (now - lastStartIssuedAt < StartRequestGraceSeconds) + { + SetState(AutoHostState.Starting, "Старт отправлен"); + return; + } + lastStartIssuedAt = -1f; + EnterBackoff("Старт не подтвердился"); + return; + } + + if (backoffUntil > now) + { + SetState(AutoHostState.Backoff, "Пауза после попытки"); + return; + } + + int requiredPlayers = RequiredPlayers; + bool enoughPlayers = ElysiumModMenuGUI.AutoHostWaitLoadedPlayers ? readyPlayers >= requiredPlayers : connectedPlayers >= requiredPlayers; + bool continueBelowMin = !ElysiumModMenuGUI.AutoHostCancelBelowMin && countdownStartedAt >= 0f && connectedPlayers >= 2; + + if (!forceStart && !enoughPlayers && !continueBelowMin) + { + if (countdownStartedAt >= 0f) + Notify("Автохост", "Отсчет отменен: игроков стало меньше минимума."); + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + lastCountdownNotice = -1; + SetState(AutoHostState.WaitingPlayers, $"Игроки {connectedPlayers}/{requiredPlayers}"); + return; + } + + float delay = EffectiveStartDelay(connectedPlayers); + if (!forceStart && countdownStartedAt < 0f) + { + countdownStartedAt = now; + activeCountdownDelay = delay; + lastCountdownNotice = -1; + SetState(AutoHostState.Countdown, IsFastStartActive(connectedPlayers) ? "Быстрый старт" : "Минимум игроков набран"); + Notify("Автохост", $"Старт через {Mathf.CeilToInt(delay)} с."); + } + + if (!forceStart && now - countdownStartedAt < delay) + { + AnnounceCountdown(delay - (now - countdownStartedAt)); + SetState(AutoHostState.Countdown, "Отсчет"); + return; + } + + GameStartManager manager = TryGetGameStartManager(); + if (manager == null) + { + EnterBackoff("Кнопка старта не найдена"); + return; + } + if (!TryConfiguredStart(manager)) + { + EnterBackoff(forceStart ? "Форс-старт отклонен" : "Старт отклонен"); + return; + } + + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + backoffUntil = -1f; + lastStartIssuedAt = now; + lastCountdownNotice = -1; + SetState(AutoHostState.Starting, forceStart ? forceReason : "Старт матча"); + Notify("Автохост", forceStart ? forceReason : "Минимум набран, запускаю матч."); + } + + private static void TrackLobby(InnerNetClient client, float now) + { + int gameId; + try { gameId = client.GameId; } catch { gameId = 0; } + if (lobbyOpenedAt >= 0f && lobbyGameId == gameId) return; + lobbyOpenedAt = now; + lobbyGameId = gameId; + ResetLobbyFlow(true); + SetState(AutoHostState.WaitingPlayers, "Новое лобби"); + } + + private static void AnnounceCountdown(float remaining) + { + int whole = Mathf.CeilToInt(Mathf.Max(0f, remaining)); + if (whole == lastCountdownNotice) return; + if (whole == 60 || whole == 30 || whole == 15 || whole == 10 || whole == 5 || whole == 3 || whole == 2 || whole == 1) + { + lastCountdownNotice = whole; + Notify("Автохост", $"Старт через {whole} с."); + } + } + + private static bool TryConfiguredStart(GameStartManager manager) + { + if (manager == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || LobbyBehaviour.Instance == null) + return false; + try + { + manager.MinPlayers = 1; + if (ElysiumModMenuGUI.AutoHostInstantStart) + { + manager.startState = GameStartManager.StartingStates.Countdown; + manager.countDownTimer = 0f; + return true; + } + manager.BeginGame(); + return true; + } + catch { return false; } + } + + private static void EnterBackoff(string reason) + { + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + lastStartIssuedAt = -1f; + loadWaitStartedAt = -1f; + lastCountdownNotice = -1; + backoffUntil = Time.unscaledTime + BackoffSeconds; + SetState(AutoHostState.Backoff, reason); + Notify("Автохост: пауза", reason); + } + + private static void ResetLobbyFlow(bool clearBackoff) + { + countdownStartedAt = -1f; + lastStartIssuedAt = -1f; + lastCountdownNotice = -1; + if (clearBackoff) backoffUntil = -1f; + } + + private static void SetState(AutoHostState nextState, string reason) + { + if (!string.IsNullOrWhiteSpace(reason)) lastReason = reason.Trim(); + state = nextState; + } + + private static int CountLobbyPlayers(InnerNetClient client, out int readyPlayers, out string loadingName) + { + readyPlayers = 0; + loadingName = "игрок"; + if (client == null || client.allClients == null) return 0; + + int connected = 0; + try + { + var cursor = client.allClients.GetEnumerator(); + while (cursor.MoveNext()) + { + ClientData data = cursor.Current; + if (data == null || data.Id < 0) continue; + if (IsDisconnected(data)) continue; + connected++; + if (IsReady(data)) readyPlayers++; + else loadingName = CleanName(data.PlayerName); + } + } + catch { return CountReadyPlayerControls(out readyPlayers); } + return connected; + } + + private static int CountReadyPlayerControls(out int readyPlayers) + { + readyPlayers = 0; + try + { + if (PlayerControl.AllPlayerControls == null) return 0; + int count = 0; + var cursor = PlayerControl.AllPlayerControls.GetEnumerator(); + while (cursor.MoveNext()) + { + PlayerControl player = cursor.Current; + if (player == null || player.Data == null || player.Data.Disconnected || player.PlayerId >= 100) continue; + count++; + readyPlayers++; + } + return count; + } + catch { return 0; } + } + + private static bool IsReady(ClientData data) + { + try + { + PlayerControl character = data.Character; + return character != null && character.Data != null && !character.Data.Disconnected && character.PlayerId < 100; + } + catch { return false; } + } + + private static bool IsDisconnected(ClientData data) + { + try { return data.Character != null && data.Character.Data != null && data.Character.Data.Disconnected; } + catch { return false; } + } + + private static GameStartManager TryGetGameStartManager() + { + try { if (DestroyableSingleton.InstanceExists) return DestroyableSingleton.Instance; } catch { } + try { return UnityEngine.Object.FindObjectOfType(); } catch { return null; } + } + + private static InnerNetClient TryGetClient() + { + try { return AmongUsClient.Instance == null ? null : (InnerNetClient)AmongUsClient.Instance; } catch { return null; } + } + + private static bool CanBypassLoadWait(float now, int readyPlayers, int connectedPlayers, string loadingName) + { + if (readyPlayers < RequiredPlayers) { loadWaitStartedAt = -1f; return false; } + int grace = Mathf.Clamp((int)ElysiumModMenuGUI.AutoHostLoadGraceSeconds, 0, 90); + if (grace <= 0) { loadWaitStartedAt = -1f; return false; } + if (loadWaitStartedAt < 0f) loadWaitStartedAt = now; + if (now - loadWaitStartedAt < grace) + { + SetState(AutoHostState.WaitingLoad, $"Жду прогрузку {readyPlayers}/{connectedPlayers}: {loadingName}"); + return false; + } + SetState(AutoHostState.Countdown, "Прогрузка задержалась, старт по готовым"); + return true; + } + + private static bool ShouldForceStart(int connectedPlayers, out string reason) + { + int minPlayers = ForceMinPlayers; + if (ForceLastMinuteEnabled && connectedPlayers >= minPlayers && LobbyLifeRemaining >= 0f && LobbyLifeRemaining <= LastMinuteStartSeconds) + { + reason = "Форс-старт: лобби скоро закроется"; + return true; + } + int forceAfterMinutes = Mathf.Clamp(ElysiumModMenuGUI.AutoHostForceAfterMinutes, 0, 10); + if (forceAfterMinutes > 0 && connectedPlayers >= minPlayers && lobbyOpenedAt > 0f && Time.unscaledTime - lobbyOpenedAt >= forceAfterMinutes * 60f) + { + reason = $"Форс-старт: ожидание {forceAfterMinutes} мин"; + return true; + } + reason = string.Empty; + return false; + } + + private static bool IsFastStartActive(int connectedPlayers) + { + int threshold = Mathf.Clamp(ElysiumModMenuGUI.AutoHostFastStartPlayers, 0, 15); + return threshold > 0 && connectedPlayers >= threshold; + } + + private static float EffectiveStartDelay(int connectedPlayers) + { + float delay = StartDelaySeconds; + if (IsFastStartActive(connectedPlayers)) + delay = Mathf.Min(delay, Mathf.Clamp(ElysiumModMenuGUI.AutoHostFastStartDelaySeconds, 0, 60)); + return delay; + } + + private static bool IsInMatch() => ShipStatus.Instance != null && LobbyBehaviour.Instance == null && !IsEndGameScreen(); + + private static bool IsEndGameScreen() + { + try { return UnityEngine.Object.FindObjectOfType() != null; } catch { return false; } + } + + private static void Notify(string title, string detail) + { + if (!ElysiumModMenuGUI.AutoHostNotifications) return; + float now = Time.unscaledTime; + if (lastNotificationAt > 0f && now - lastNotificationAt < NotificationCooldownSeconds) return; + lastNotificationAt = now; + ElysiumModMenuGUI.ShowNotification($"[{title}] {detail}"); + } + + private static string FormatState(AutoHostState value) + { + return value switch + { + AutoHostState.Disabled => L("Disabled", "Выключен"), + AutoHostState.Idle => L("Idle", "Ожидание"), + AutoHostState.Warmup => L("Warmup", "Прогрев"), + AutoHostState.WaitingPlayers => L("Waiting for players", "Ждет игроков"), + AutoHostState.WaitingLoad => L("Waiting for load", "Ждет прогрузку"), + AutoHostState.Countdown => L("Countdown", "Отсчет"), + AutoHostState.Starting => L("Starting", "Запуск"), + AutoHostState.InGame => L("In Game", "В игре"), + AutoHostState.Returning => L("Returning", "Возврат"), + AutoHostState.Backoff => L("Backoff", "Пауза"), + _ => value.ToString(), + }; + } + + private static string CleanName(string value) + { + if (string.IsNullOrWhiteSpace(value)) return "игрок"; + string clean = value.Replace("\r", " ").Replace("\n", " ").Trim(); + return clean.Length <= 18 ? clean : clean.Substring(0, 17) + "..."; + } + + public static bool IsEnabled => ElysiumModMenuGUI.AutoHostEnabled; + public static bool ShouldReturnAfterMatch => IsEnabled && ElysiumModMenuGUI.AutoReturnLobbyAfterMatch; + private static bool ForceLastMinuteEnabled => ElysiumModMenuGUI.AutoHostForceLastMinute; + private static int RequiredPlayers => Mathf.Clamp(ElysiumModMenuGUI.AutoHostMinPlayers, 1, 15); + private static int ForceMinPlayers => Mathf.Clamp(ElysiumModMenuGUI.AutoHostForceMinPlayers, 1, 15); + private static float StartDelaySeconds => Mathf.Clamp(ElysiumModMenuGUI.AutoHostStartDelaySeconds, 0f, 180f); + private static float BackoffSeconds => Mathf.Clamp(ElysiumModMenuGUI.AutoHostBackoffSeconds, 2f, 60f); + private static float CountdownRemaining => countdownStartedAt < 0f ? 0f : Mathf.Clamp((activeCountdownDelay >= 0f ? activeCountdownDelay : StartDelaySeconds) - (Time.unscaledTime - countdownStartedAt), 0f, StartDelaySeconds); + private static float BackoffRemaining => backoffUntil < 0f ? 0f : Mathf.Clamp(backoffUntil - Time.unscaledTime, 0f, BackoffSeconds); + private static float LobbyLifeRemaining => lobbyOpenedAt < 0f ? -1f : Mathf.Clamp(LobbyLifetimeSeconds - (Time.unscaledTime - lobbyOpenedAt), 0f, LobbyLifetimeSeconds); + private static float WarmupRemaining => lobbyOpenedAt < 0f ? 0f : Mathf.Clamp(ElysiumModMenuGUI.AutoHostWarmupSeconds - (Time.unscaledTime - lobbyOpenedAt), 0f, 120f); + private static float LoadGraceRemaining => loadWaitStartedAt < 0f || ElysiumModMenuGUI.AutoHostLoadGraceSeconds <= 0 ? 0f : Mathf.Clamp(ElysiumModMenuGUI.AutoHostLoadGraceSeconds - (Time.unscaledTime - loadWaitStartedAt), 0f, 90f); + } + private int currentVisualsSubTab = 0; + private string[] visualsSubTabs = { "IN-GAME" }; + private int currentSelfSubTab = 0; + private string[] selfSubTabs = { "SPOOF", "MOVEMENT", "ROLES", "CHAT" }; + private string[] selfOtherTabs = { "MOVEMENT", "ROLES", "CHAT" }; + public static bool fakeStartCounterTroll = false; + public static bool fakeStartCounterCustom = false; + public static string fakeStartInput = "69"; + public static bool isEditingFakeStart = false; + public static float customStartTimer = -1f; + + public static bool localRainbow = false; + public static List rainbowPlayers = new List(); + public static float colorTimer = 0f; + public static byte currentColorId = 0; + private Vector2 playerListScrollPos = Vector2.zero; + private Vector2 playerActionScrollPos = Vector2.zero; + private byte selectedHydraPlayerId = 255; + + public static string spoofLevelString = "100"; + public static string customNameInput = "хыхых"; + public static string spoofFriendCodeInput = "crewmate01"; + public static string localFriendCodeInput = "Steam#Local"; + public static bool isEditingLevel = false; + public static bool isEditingName = false; + public static bool isEditingFriendCode = false; + public static bool isEditingLocalFriendCode = false; + public static bool enableLocalNameSpoof = false; + public static bool enableLocalFriendCodeSpoof = false; + public static bool enableFriendCodeSpoof = false; + public static bool enablePlatformSpoof = true; + public static int currentPlatformIndex = 1; + private static float localNameRefreshTimer = 0f; + private static float localFriendCodeRefreshTimer = 0f; + private static string originalLocalFriendCode = null; + private float brokenFcScanTimer = 0f; + private static readonly HashSet brokenFcPunishedOwners = new HashSet(); + + public static string[] platformNames = { + "Epic", "Steam", "Mac", "Microsoft", "Itch", "iOS", + "Android", "Switch", "Xbox", "PlayStation", "Starlight" + }; + + public static Platforms[] platformValues = { + (Platforms)1, + (Platforms)2, + (Platforms)3, + (Platforms)4, + (Platforms)5, + (Platforms)6, + (Platforms)7, + (Platforms)8, + (Platforms)9, + (Platforms)10, + (Platforms)112 + }; + + public static bool unlockFeatures = true; + + + + public class ElysiumNotification + { + public string title; + public string message; + public float ttl; + public float lifetime; + public bool HasExpired => lifetime > ttl; + + public ElysiumNotification(string title, string message, float ttl) + { + this.title = title; + this.message = message; + this.ttl = ttl; + this.lifetime = 0f; + } + } + public static List bannedEntries = new List(); + public static string banListPath = ""; + private Vector2 banListScroll = Vector2.zero; + public static bool autoBanEnabled = true; + public static string banInput = ""; + public static bool isEditingBan = false; + + public static void LoadBanList() + { + try + { + banListPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ElysiumModMenuBanList.txt"); + if (!System.IO.File.Exists(banListPath)) + { + System.IO.File.Create(banListPath).Dispose(); + } + bannedEntries = new List(System.IO.File.ReadAllLines(banListPath)); + } + catch { } + } + + public static void AddToBanList(string friendCode, string puid, string name, string reason) + { + try + { + if (string.IsNullOrEmpty(friendCode)) return; + + bool alreadyBanned = false; + string fcLower = friendCode.Trim().ToLower(); + + foreach (var e in bannedEntries) + { + string[] parts = e.Split('|'); + if (parts.Length > 0 && parts[0].Trim().ToLower() == fcLower) + { + alreadyBanned = true; + break; + } + } + + if (!alreadyBanned) + { + string date = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); + string entry = $"{friendCode}|{puid}|{name}|{date}|{reason}"; + bannedEntries.Add(entry); + System.IO.File.AppendAllText(banListPath, entry + Environment.NewLine); + } + } + catch { } + } + + public static void RemoveFromBanList(string entry) + { + try + { + bannedEntries.Remove(entry); + System.IO.File.WriteAllLines(banListPath, bannedEntries.ToArray()); + } + catch { } + } + + public static bool killReach = false, killAnyone = false; + public static bool endlessSsDuration = false, noVitalsCooldown = false; + public static bool endlessBattery = false, endlessVentTime = false, noVentCooldown = false; + public static bool reactorSab = false, oxygenSab = false, commsSab = false, elecSab = false; + public static bool autoOpenDoors = false; + public static bool moonWalk = false; + public static bool SeePlayersInVent = false; + public static bool seeGhosts = false; + public static bool seeRoles = false; + public static bool showPlayerInfo = false; + public static bool revealMeetingRoles = false; + public static bool showTracers = false; + public static bool fullBright = false; + public static bool seeProtections = false; + public static bool seeKillCooldown = false; + public static bool extendedLobby = false; + public static bool DarkModeEnabled = true; + public static bool enableChatDarkMode = true; + public static float customLightRadius = 5f; + private static Dictionary lastKillTimestamps = new Dictionary(); + + public static bool alwaysChat = false; + public static bool readGhostChat = false; + public static bool enableSpellCheck = false; + + public static bool neverEndGame = false; + public static void ShowNotification(string text) + { + string title = "ElysiumModMenu"; + string msg = text; + + if (text.Contains("[") && text.Contains("]")) + { + int start = text.IndexOf("["); + int end = text.IndexOf("]"); + if (end > start) + { + string rawTitle = text.Substring(start + 1, end - start - 1); + title = System.Text.RegularExpressions.Regex.Replace(rawTitle, "<.*?>", string.Empty); + msg = System.Text.RegularExpressions.Regex.Replace(msg, @"(]+>)?\[.*?\]()?\s*", ""); + } + } + SendNotification(title, msg.Trim(), 3.5f); + } + + public static void SendNotification(string title, string message, float ttl = 3.5f) + { + if (!EnableCustomNotifs) return; + screenNotifications.Add(new ElysiumNotification(title, message, ttl)); + } + + + + public static HashSet forcedImpostors = new HashSet(); + public static Dictionary forcedPreGameRoles = new Dictionary(); + public static bool enablePreGameRoleForce = false; + private Vector2 preRolesListScrollPos = Vector2.zero; + private Vector2 preRolesActionScrollPos = Vector2.zero; + private byte selectedPreRoleId = 255; + public static List lockedPlayersList = new List(); + public static bool LogAllRPCs = true; + public static bool blockRainbowChat = true; + public static bool blockFortegreenChat = true; + + public static bool EnableCustomNotifs = true; + public static Vector2 notificationBoxSize = new Vector2(260f, 65f); + public static List screenNotifications = new List(); + + + private bool stylesInited = false; + private GUIStyle windowStyle, btnStyle, activeTabStyle, headerStyle, boxStyle; + private GUIStyle sidebarStyle, sidebarBtnStyle, activeSidebarBtnStyle, titleStyle; + private GUIStyle toggleOnStyle, toggleOffStyle, toggleLabelStyle, safeLineStyle; + private GUIStyle sliderStyle, sliderThumbStyle, subTabStyle, activeSubTabStyle; + public GUIStyle inputBlockStyle; + private Texture2D texWindowBg, texBoxBg, texBtnBg, texAccent, texSidebarBg; + private Texture2D texToggleOff, texToggleOn, texSliderBg, texSliderThumb, texInputBg, texColorBtn, texScrollThumb; + private void DrawHostOnlyTab() + { + GUILayout.BeginHorizontal(); + for (int i = 0; i < hostOnlySubTabs.Length; i++) + { + if (GUILayout.Button(hostOnlySubTabs[i], currentHostOnlySubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) + { + currentHostOnlySubTab = i; + scrollPosition = Vector2.zero; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(8); + + if (currentHostOnlySubTab == 0) DrawLobbyControls(); + else if (currentHostOnlySubTab == 1) DrawPlayersRoles(); + else if (currentHostOnlySubTab == 2) DrawAntiCheatTab(); + else if (currentHostOnlySubTab == 3) DrawAutoHostTab(); + } + + + private void DrawVisualsInGame() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label(L("VISIBILITY", "ВИДИМОСТЬ"), headerStyle); + + GUILayout.BeginHorizontal(); + seeGhosts = DrawToggle(seeGhosts, L("See Ghosts", "Видеть призраков"), 210); + seeRoles = DrawToggle(seeRoles, L("See Roles", "Видеть роли"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + showPlayerInfo = DrawToggle(showPlayerInfo, L("Show Player Info (ESP)", "Инфо об игроке (ESP)"), 210); + revealMeetingRoles = DrawToggle(revealMeetingRoles, L("Reveal Roles (Meeting)", "Показывать роли на собрании"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + removePenalty = DrawToggle(removePenalty, L("No Disconnect Penalty", "Нет штрафа за выход"), 210); + alwaysShowLobbyTimer = DrawToggle(alwaysShowLobbyTimer, L("Always Show Lobby Timer", "Всегда показывать таймер лобби"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + showTracers = DrawToggle(showTracers, L("Show Tracers", "Показывать линии (Tracer)"), 210); + fullBright = DrawToggle(fullBright, L("Full Bright (No Shadows)", "Полная яркость (Нет теней)"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 210); + readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + freecam = DrawToggle(freecam, L("Freecam (WASD)", "Свободная камера (WASD)"), 210); + cameraZoom = DrawToggle(cameraZoom, L("Camera Zoom (Scroll)", "Зум камеры (Колесико)"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + RevealVotesEnabled = DrawToggle(RevealVotesEnabled, L("Reveal Votes (Meeting)", "Показывать голоса (Собрание)"), 210); + SeePlayersInVent = DrawToggle(SeePlayersInVent, L("See Players In Vents", "Видеть игроков в люках"), 210); + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + seeProtections = DrawToggle(seeProtections, L("See Protections", "Видеть щиты"), 210); + seeKillCooldown = DrawToggle(seeKillCooldown, L("See Kill Cooldown", "Видеть килл-кд"), 210); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + } + + + public static bool enableLocalPetSpamDrop = true; + public static bool enableHostPetSpamBan = false; + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] + public static class Shield_PetSpam_Patch + { + public static System.Collections.Generic.HashSet petSpamBlockedPlayers = new System.Collections.Generic.HashSet(); + + public static System.Collections.Generic.Dictionary> petSpamTrackers = new System.Collections.Generic.Dictionary>(); + + public static bool Prefix(PlayerPhysics __instance, byte callId, Hazel.MessageReader reader) + { + if (!ElysiumModMenuGUI.enableLocalPetSpamDrop && !ElysiumModMenuGUI.enableHostPetSpamBan) return true; + + if (callId == 49 || callId == 50) + { + try + { + if (__instance == null || __instance.myPlayer == null) return true; + + if (__instance.myPlayer == PlayerControl.LocalPlayer) return true; + + byte pId = __instance.myPlayer.PlayerId; + + if (petSpamBlockedPlayers.Contains(pId)) + { + if (ElysiumModMenuGUI.enableLocalPetSpamDrop) return false; + } + + float now = UnityEngine.Time.time; + + if (!petSpamTrackers.ContainsKey(pId)) + petSpamTrackers[pId] = new System.Collections.Generic.Queue(); + + var q = petSpamTrackers[pId]; + + while (q.Count > 0 && q.Peek() < now - 0.75f) + q.Dequeue(); + + q.Enqueue(now); + + if (q.Count > 160) + { + petSpamBlockedPlayers.Add(pId); + + string pName = __instance.myPlayer.Data?.PlayerName ?? "Unknown"; + + if (ElysiumModMenuGUI.enableLocalPetSpamDrop) + { + ElysiumModMenuGUI.ShowNotification($"[SHIELD] Игрок {pName} заблокирован за Pet Spam (Локально)!"); + } + + if (ElysiumModMenuGUI.enableHostPetSpamBan && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + string fc = string.IsNullOrEmpty(__instance.myPlayer.Data?.FriendCode) ? "Unknown" : __instance.myPlayer.Data.FriendCode; + string puid = "Unknown"; + + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(__instance.myPlayer); + if (client != null) puid = client.Id.ToString(); + } + catch { } + + ElysiumModMenuGUI.AddToBanList(fc, puid, pName, "Auto-banned for Pet Spam"); + + AmongUsClient.Instance.KickPlayer(__instance.myPlayer.OwnerId, true); + + ElysiumModMenuGUI.ShowNotification($"[SHIELD] Игрок {pName} АВТОМАТИЧЕСКИ ЗАБАНЕН за спам петом!"); + } + + return false; + } + } + catch { } + } + + return true; + } + } + public static int GetColorIdByName(string name) + { + string[] names = { "red", "blue", "green", "pink", "orange", "yellow", "black", "white", "purple", "brown", "cyan", "lime", "maroon", "rose", "banana", "gray", "tan", "coral", "fortegreen" }; + for (int i = 0; i < names.Length; i++) + if (names[i] == name.ToLower().Trim()) return i; + return -1; + } + private IEnumerator AttemptShapeshiftFrame(PlayerControl target, PlayerControl morphInto) + { + if (target == null || morphInto == null || PlayerControl.LocalPlayer == null || AmongUsClient.Instance == null) yield break; + + bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); + + if (target.Data.RoleType != RoleTypes.Shapeshifter && hasAnticheat) + { + RoleTypes currentRole = target.Data.RoleType; + target.RpcSetRole(RoleTypes.Shapeshifter, true); + + yield return new WaitForSeconds(0.5f); + + target.RpcShapeshift(morphInto, true); + + yield return new WaitForSeconds(0.5f); + + target.RpcSetRole(currentRole, true); + } + else + { + target.RpcShapeshift(morphInto, true); + } + ShowNotification($"[MORPH] {target.Data.PlayerName} превращен в {morphInto.Data.PlayerName}!"); + } + + private IEnumerator MassMorphCoroutine() + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) yield break; + + bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); + + Dictionary originalRoles = new Dictionary(); + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + originalRoles[pc.PlayerId] = pc.Data.RoleType; + + if (hasAnticheat && pc.Data.RoleType != RoleTypes.Shapeshifter) + { + pc.RpcSetRole(RoleTypes.Shapeshifter, true); + } + } + } + + if (hasAnticheat) yield return new UnityEngine.WaitForSeconds(0.5f); + + PlayerControl targetToMorphInto = null; + if (selectedMorphTargetId != 255) + { + targetToMorphInto = GameData.Instance.GetPlayerById(selectedMorphTargetId)?.Object; + } + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + PlayerControl morphTarget = targetToMorphInto != null ? targetToMorphInto : pc; + pc.RpcShapeshift(morphTarget, true); + } + } + + + if (hasAnticheat) yield return new UnityEngine.WaitForSeconds(0.5f); + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + if (hasAnticheat && originalRoles.ContainsKey(pc.PlayerId)) + { + pc.RpcSetRole(originalRoles[pc.PlayerId], true); + } + } + } + + string notifText = targetToMorphInto != null ? targetToMorphInto.Data.PlayerName : "Egg"; + ShowNotification($"[MASS MORPH] {notifText}"); + } + + + private void ForceMeetingAsPlayer(PlayerControl target) + { + if (target == null || AmongUsClient.Instance == null) return; + if (!AmongUsClient.Instance.AmHost) return; + + try + { + MeetingRoomManager.Instance.AssignSelf(target, null); + target.RpcStartMeeting(null); + DestroyableSingleton.Instance.OpenMeetingRoom(target); + } + catch { } + } + + private void KillAll() + { + if (PlayerControl.LocalPlayer == null || PlayerControl.AllPlayerControls == null) return; + Vector3 op = PlayerControl.LocalPlayer.transform.position; + foreach (var t in PlayerControl.AllPlayerControls) + { + if (t != null && t != PlayerControl.LocalPlayer && !t.Data.IsDead && !t.Data.Disconnected) + { + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(t.transform.position); + PlayerControl.LocalPlayer.CmdCheckMurder(t); + PlayerControl.LocalPlayer.RpcMurderPlayer(t, true); + } + } + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(op); + } + + private void KickAll() + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) + { + foreach (var pc in PlayerControl.AllPlayerControls) + if (pc != null && pc != PlayerControl.LocalPlayer && !pc.Data.Disconnected) + AmongUsClient.Instance.KickPlayer((int)pc.OwnerId, false); + } + } + + private void DespawnLobby() + { + try + { + if (LobbyBehaviour.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + LobbyBehaviour.Instance.Cast().Despawn(); + } + } + catch { } + } + + private void SpawnLobby() + { + try + { + if (GameStartManager.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + LobbyBehaviour newLobby = UnityEngine.Object.Instantiate(GameStartManager.Instance.LobbyPrefab); + AmongUsClient.Instance.Spawn(newLobby.Cast(), -2, SpawnFlags.None); + } + } + catch { } + } + + + + public static void UnlockCosmetics() + { + if (HatManager.Instance == null) return; + try + { + foreach (var h in HatManager.Instance.allHats) h.Free = true; + foreach (var s in HatManager.Instance.allSkins) s.Free = true; + foreach (var v in HatManager.Instance.allVisors) v.Free = true; + foreach (var p in HatManager.Instance.allPets) p.Free = true; + foreach (var n in HatManager.Instance.allNamePlates) n.Free = true; + } + catch { } + } + + public static void ChangeNameGlobalHost(PlayerControl target, string newName) + { + if (target == null) return; + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + try + { + target.RpcSetName(newName); + var netObj = GameData.Instance.GetComponent(); + if (netObj != null) netObj.SetDirtyBit(1U << (int)target.PlayerId); + } + catch { } + } + + private static void ApplyLocalNameSelf(string newName, bool notify = true) + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null) + { + if (notify) ShowNotification("[LOCAL NAME] Local player not found."); + return; + } + + string renderName = BuildLocalNameRenderText(newName); + + TryInvokeStringMethod(local, "SetName", renderName); + + try + { + if (local.cosmetics != null) + local.cosmetics.SetName(renderName); + } + catch { } + + TrySetPlayerNameObject(local.Data, renderName); + if (local.Data != null) + { + TrySetPlayerNameObject(local.Data.DefaultOutfit, renderName); + TrySetPlayerNameObject(local.CurrentOutfit, renderName); + } + + if (notify) + ShowNotification($"[LOCAL NAME] {L("Applied locally:", "Локально применен:")} {newName}"); + } + catch { } + } + + private static void ApplyLocalFriendCodeSelf(string fakeFriendCode, bool notify = true) + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null || local.Data == null) + { + if (notify) ShowNotification("[LOCAL FC] Local player data not found."); + return; + } + + fakeFriendCode ??= string.Empty; + string current = local.Data.FriendCode ?? string.Empty; + if (originalLocalFriendCode == null && current != fakeFriendCode) + originalLocalFriendCode = current; + + TrySetStringMember(local.Data, "FriendCode", fakeFriendCode); + + if (notify) + ShowNotification($"[LOCAL FC] {L("Applied locally:", "Локально применен:")} {fakeFriendCode}"); + } + catch { } + } + + private static void RestoreLocalFriendCodeSelf() + { + try + { + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null || originalLocalFriendCode == null) return; + TrySetStringMember(PlayerControl.LocalPlayer.Data, "FriendCode", originalLocalFriendCode); + originalLocalFriendCode = null; + } + catch { } + } + + private static void TrySetPlayerNameObject(object target, string newName) + { + TrySetStringMember(target, "PlayerName", newName); + } + + private static void TrySetStringMember(object target, string memberName, string value) + { + if (target == null || string.IsNullOrEmpty(memberName)) return; + + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + Type type = target.GetType(); + + try + { + PropertyInfo property = type.GetProperty(memberName, flags); + if (property != null && property.CanWrite) + { + property.SetValue(target, value, null); + return; + } + } + catch { } + + try + { + FieldInfo field = type.GetField(memberName, flags); + if (field != null) field.SetValue(target, value); + } + catch { } + } + + private static void TryInvokeStringMethod(object target, string methodName, string value) + { + if (target == null) return; + + try + { + MethodInfo method = target.GetType().GetMethod( + methodName, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + null, + new[] { typeof(string) }, + null); + + if (method != null) + method.Invoke(target, new object[] { value }); + } + catch { } + } + + public static bool showWatermark = true; + public static bool whiteMenuTheme = false; + + private static void SaveBool(string key, bool value) + { + PlayerPrefs.SetInt(key, value ? 1 : 0); + } + + private static bool LoadBool(string key, bool defaultValue) + { + return PlayerPrefs.HasKey(key) ? PlayerPrefs.GetInt(key) == 1 : defaultValue; + } + + private void SaveConfig() + { + try + { + PlayerPrefs.SetInt("M_BndMagnet", (int)bindMagnetCursor); + Plugin.SpoofedLevel.Value = spoofLevelString; + Plugin.EnableFriendCodeSpoofConfig.Value = enableFriendCodeSpoof; + Plugin.SpoofFriendCodeConfig.Value = spoofFriendCodeInput; + Plugin.EnablePlatformSpoof.Value = enablePlatformSpoof; + Plugin.AutoBanBrokenFriendCodeConfig.Value = autoBanBrokenFriendCode; + Plugin.PlatformIndex.Value = currentPlatformIndex; + Plugin.ShowWatermarkConfig.Value = showWatermark; + Plugin.UnlockCosmeticsConfig.Value = unlockCosmetics; + Plugin.MoreLobbyInfoConfig.Value = moreLobbyInfo; + Plugin.EnableChatDarkModeConfig.Value = enableChatDarkMode; + Plugin.RpcSpoofDelayConfig.Value = rpcSpoofDelay; + Plugin.MenuColorIndexConfig.Value = currentMenuColorIndex; + Plugin.RgbMenuModeConfig.Value = rgbMenuMode; + Plugin.MenuKeybind.Value = KeyCode.Insert; + menuToggleKey = KeyCode.Insert; + SaveBool("M_WhiteTheme", whiteMenuTheme); + PlayerPrefs.SetInt("M_BndMMorph", (int)bindMassMorph); + PlayerPrefs.SetInt("M_BndSpawn", (int)bindSpawnLobby); + PlayerPrefs.SetInt("M_BndDespawn", (int)bindDespawnLobby); + PlayerPrefs.SetInt("M_BndCloseMtg", (int)bindCloseMeeting); + PlayerPrefs.SetInt("M_BndInstaStart", (int)bindInstaStart); + PlayerPrefs.SetInt("M_BndEndCrew", (int)bindEndCrew); + PlayerPrefs.SetInt("M_BndEndImp", (int)bindEndImp); + PlayerPrefs.SetInt("M_BndEndImpDC", (int)bindEndImpDC); + PlayerPrefs.SetInt("M_BndEndHnsDC", (int)bindEndHnsDC); + SaveBool("M_AutoKickBugs", autoKickBugs); + PlayerPrefs.SetFloat("M_AutoKickTimer", autoKickTimer); + SaveBool("M_DisableVoteKicks", disableVoteKicks); + SaveBool("M_LocalNameSpoof", enableLocalNameSpoof); + SaveBool("M_LocalFakeFCEnabled", enableLocalFriendCodeSpoof); + PlayerPrefs.SetString("M_LocalFakeFC", localFriendCodeInput); + + SaveBool("M_ShowPlayerInfo", showPlayerInfo); + SaveBool("M_SeeGhosts", seeGhosts); + SaveBool("M_SeeRoles", seeRoles); + SaveBool("M_RevealMeetingRoles", revealMeetingRoles); + SaveBool("M_ShowTracers", showTracers); + SaveBool("M_FullBright", fullBright); + SaveBool("M_SeeProtections", seeProtections); + SaveBool("M_SeeKillCooldown", seeKillCooldown); + SaveBool("M_ExtendedLobby", extendedLobby); + SaveBool("M_MoreLobbyInfo", moreLobbyInfo); + SaveBool("M_AlwaysChat", alwaysChat); + SaveBool("M_ReadGhostChat", readGhostChat); + SaveBool("M_EnableExtendedChat", enableExtendedChat); + SaveBool("M_EnableFastChat", enableFastChat); + SaveBool("M_AllowLinksAndSymbols", allowLinksAndSymbols); + SaveBool("M_EnableChatHistory", enableChatHistory); + SaveBool("M_EnableClipboard", enableClipboard); + SaveBool("M_EnableChatLog", enableChatLog); + SaveBool("M_EnableColorCommand", enableColorCommand); + SaveBool("M_BlockRainbowChat", blockRainbowChat); + SaveBool("M_BlockFortegreenChat", blockFortegreenChat); + SaveBool("M_SpoofMenuEnabled", SpoofMenuEnabled); + SaveBool("M_NoClip", noClip); + SaveBool("M_TpToCursor", tpToCursor); + SaveBool("M_DragToCursor", dragToCursor); + SaveBool("M_AutoFollowCursor", autoFollowCursor); + SaveBool("M_Freecam", freecam); + SaveBool("M_CameraZoom", cameraZoom); + SaveBool("M_RevealVotes", RevealVotesEnabled); + SaveBool("M_NoTaskMode", noTaskMode); + SaveBool("M_NeverEndGame", neverEndGame); + SaveBool("M_RemovePenalty", removePenalty); + SaveBool("M_AlwaysShowLobbyTimer", alwaysShowLobbyTimer); + SaveBool("M_AutoBanEnabled", autoBanEnabled); + SaveBool("M_BlockSpoofRPC", blockSpoofRPC); + SaveBool("M_BlockSabotageRPC", blockSabotageRPC); + SaveBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); + SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); + SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); + SaveBool("M_AutoHostEnabled", AutoHostEnabled); + SaveBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); + SaveBool("M_AutoHostNotifications", AutoHostNotifications); + SaveBool("M_AutoHostForceLastMinute", AutoHostForceLastMinute); + SaveBool("M_AutoHostWaitLoadedPlayers", AutoHostWaitLoadedPlayers); + SaveBool("M_AutoHostCancelBelowMin", AutoHostCancelBelowMin); + SaveBool("M_AutoHostInstantStart", AutoHostInstantStart); + PlayerPrefs.SetInt("M_AutoHostMinPlayers", AutoHostMinPlayers); + PlayerPrefs.SetFloat("M_AutoHostStartDelaySeconds", AutoHostStartDelaySeconds); + PlayerPrefs.SetInt("M_AutoHostFastStartPlayers", AutoHostFastStartPlayers); + PlayerPrefs.SetFloat("M_AutoHostFastStartDelaySeconds", AutoHostFastStartDelaySeconds); + PlayerPrefs.SetFloat("M_WalkSpeed", walkSpeed); + PlayerPrefs.SetFloat("M_EngineSpeed", engineSpeed); + + Plugin.MenuConfig.Save(); + + PlayerPrefs.SetString("M_SpoofName", customNameInput); + PlayerPrefs.Save(); + } + catch { } + } + private void DrawAutoHostTab() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label(L("AUTO HOST SYSTEM", "СИСТЕМА АВТО-ХОСТА"), headerStyle); + + var snapshot = ElysiumAutoHostService.GetStatusSnapshot(); + GUILayout.Label($"{L("Status:", "Статус:")} {snapshot.State}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 13 }); + GUILayout.Space(10); + + AutoHostEnabled = DrawToggle(AutoHostEnabled, L("Enable Auto Host", "Включить Авто-Хост"), 250); + GUILayout.Space(5); + AutoReturnLobbyAfterMatch = DrawToggle(AutoReturnLobbyAfterMatch, L("Auto Return To Lobby", "Авто-возврат в лобби"), 250); + GUILayout.Space(5); + AutoHostNotifications = DrawToggle(AutoHostNotifications, L("Show Notifications", "Показывать уведомления"), 250); + GUILayout.Space(5); + AutoHostWaitLoadedPlayers = DrawToggle(AutoHostWaitLoadedPlayers, L("Wait For Players To Load", "Ждать прогрузки игроков"), 250); + GUILayout.Space(5); + AutoHostCancelBelowMin = DrawToggle(AutoHostCancelBelowMin, L("Cancel Countdown If Player Leaves", "Отмена отсчета, если игрок вышел"), 250); + GUILayout.Space(5); + AutoHostInstantStart = DrawToggle(AutoHostInstantStart, L("Instant Start (No 5s Wait)", "Мгновенный старт (Без 5с)"), 250); + GUILayout.Space(5); + AutoHostForceLastMinute = DrawToggle(AutoHostForceLastMinute, L("Force Start Last Minute", "Форс-старт на последней минуте"), 250); + + GUILayout.Space(15); + + string hexColor = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); + GUIStyle sliderLabelStyle = new GUIStyle(toggleLabelStyle) { richText = true }; + + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Min Players:", "Мин. игроков:")} {AutoHostMinPlayers}", sliderLabelStyle, GUILayout.Width(175)); + AutoHostMinPlayers = (int)GUILayout.HorizontalSlider(AutoHostMinPlayers, 1f, 15f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Start Delay:", "Задержка старта:")} {Mathf.Round(AutoHostStartDelaySeconds)}s", sliderLabelStyle, GUILayout.Width(175)); + AutoHostStartDelaySeconds = GUILayout.HorizontalSlider(AutoHostStartDelaySeconds, 0f, 180f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Fast Start Players:", "Игроков для фаст-старта:")} {AutoHostFastStartPlayers}", sliderLabelStyle, GUILayout.Width(175)); + AutoHostFastStartPlayers = (int)GUILayout.HorizontalSlider(AutoHostFastStartPlayers, 0f, 15f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Fast Start Delay:", "Задержка фаст-старта:")} {Mathf.Round(AutoHostFastStartDelaySeconds)}s", sliderLabelStyle, GUILayout.Width(175)); + AutoHostFastStartDelaySeconds = GUILayout.HorizontalSlider(AutoHostFastStartDelaySeconds, 0f, 60f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + } + private void LoadConfig() + { + try + { + spoofLevelString = Plugin.SpoofedLevel.Value; + enableFriendCodeSpoof = Plugin.EnableFriendCodeSpoofConfig.Value; + spoofFriendCodeInput = Plugin.SpoofFriendCodeConfig.Value; + enablePlatformSpoof = Plugin.EnablePlatformSpoof.Value; + autoBanBrokenFriendCode = Plugin.AutoBanBrokenFriendCodeConfig.Value; + currentPlatformIndex = Plugin.PlatformIndex.Value; + showWatermark = Plugin.ShowWatermarkConfig.Value; + unlockCosmetics = Plugin.UnlockCosmeticsConfig.Value; + moreLobbyInfo = Plugin.MoreLobbyInfoConfig.Value; + enableChatDarkMode = Plugin.EnableChatDarkModeConfig.Value; + rpcSpoofDelay = Plugin.RpcSpoofDelayConfig.Value; + currentMenuColorIndex = Plugin.MenuColorIndexConfig.Value; + rgbMenuMode = Plugin.RgbMenuModeConfig.Value; + whiteMenuTheme = LoadBool("M_WhiteTheme", whiteMenuTheme); + autoKickBugs = LoadBool("M_AutoKickBugs", autoKickBugs); + if (PlayerPrefs.HasKey("M_AutoKickTimer")) autoKickTimer = PlayerPrefs.GetFloat("M_AutoKickTimer"); + disableVoteKicks = LoadBool("M_DisableVoteKicks", disableVoteKicks); + enableLocalNameSpoof = LoadBool("M_LocalNameSpoof", enableLocalNameSpoof); + enableLocalFriendCodeSpoof = LoadBool("M_LocalFakeFCEnabled", enableLocalFriendCodeSpoof); + if (PlayerPrefs.HasKey("M_LocalFakeFC")) localFriendCodeInput = PlayerPrefs.GetString("M_LocalFakeFC"); + if (PlayerPrefs.HasKey("M_BndMagnet")) bindMagnetCursor = (KeyCode)PlayerPrefs.GetInt("M_BndMagnet"); + menuToggleKey = KeyCode.Insert; + if (PlayerPrefs.HasKey("M_BndMMorph")) bindMassMorph = (KeyCode)PlayerPrefs.GetInt("M_BndMMorph"); + if (PlayerPrefs.HasKey("M_BndSpawn")) bindSpawnLobby = (KeyCode)PlayerPrefs.GetInt("M_BndSpawn"); + if (PlayerPrefs.HasKey("M_BndDespawn")) bindDespawnLobby = (KeyCode)PlayerPrefs.GetInt("M_BndDespawn"); + if (PlayerPrefs.HasKey("M_BndCloseMtg")) bindCloseMeeting = (KeyCode)PlayerPrefs.GetInt("M_BndCloseMtg"); + if (PlayerPrefs.HasKey("M_BndInstaStart")) bindInstaStart = (KeyCode)PlayerPrefs.GetInt("M_BndInstaStart"); + if (PlayerPrefs.HasKey("M_BndEndCrew")) bindEndCrew = (KeyCode)PlayerPrefs.GetInt("M_BndEndCrew"); + if (PlayerPrefs.HasKey("M_BndEndImp")) bindEndImp = (KeyCode)PlayerPrefs.GetInt("M_BndEndImp"); + if (PlayerPrefs.HasKey("M_BndEndImpDC")) bindEndImpDC = (KeyCode)PlayerPrefs.GetInt("M_BndEndImpDC"); + if (PlayerPrefs.HasKey("M_BndEndHnsDC")) bindEndHnsDC = (KeyCode)PlayerPrefs.GetInt("M_BndEndHnsDC"); + + if (!rgbMenuMode && currentMenuColorIndex >= 0 && currentMenuColorIndex < menuColors.Length) + { + currentAccentColor = menuColors[currentMenuColorIndex]; + } + + showPlayerInfo = LoadBool("M_ShowPlayerInfo", showPlayerInfo); + seeGhosts = LoadBool("M_SeeGhosts", seeGhosts); + seeRoles = LoadBool("M_SeeRoles", seeRoles); + revealMeetingRoles = LoadBool("M_RevealMeetingRoles", revealMeetingRoles); + showTracers = LoadBool("M_ShowTracers", showTracers); + fullBright = LoadBool("M_FullBright", fullBright); + seeProtections = LoadBool("M_SeeProtections", seeProtections); + seeKillCooldown = LoadBool("M_SeeKillCooldown", seeKillCooldown); + extendedLobby = LoadBool("M_ExtendedLobby", extendedLobby); + moreLobbyInfo = LoadBool("M_MoreLobbyInfo", moreLobbyInfo); + alwaysChat = LoadBool("M_AlwaysChat", alwaysChat); + readGhostChat = LoadBool("M_ReadGhostChat", readGhostChat); + enableExtendedChat = LoadBool("M_EnableExtendedChat", enableExtendedChat); + enableFastChat = LoadBool("M_EnableFastChat", enableFastChat); + allowLinksAndSymbols = LoadBool("M_AllowLinksAndSymbols", allowLinksAndSymbols); + enableChatHistory = LoadBool("M_EnableChatHistory", enableChatHistory); + enableClipboard = LoadBool("M_EnableClipboard", enableClipboard); + enableChatLog = LoadBool("M_EnableChatLog", enableChatLog); + enableColorCommand = LoadBool("M_EnableColorCommand", enableColorCommand); + blockRainbowChat = LoadBool("M_BlockRainbowChat", blockRainbowChat); + blockFortegreenChat = LoadBool("M_BlockFortegreenChat", blockFortegreenChat); + SpoofMenuEnabled = LoadBool("M_SpoofMenuEnabled", SpoofMenuEnabled); + noClip = LoadBool("M_NoClip", noClip); + tpToCursor = LoadBool("M_TpToCursor", tpToCursor); + dragToCursor = LoadBool("M_DragToCursor", dragToCursor); + autoFollowCursor = LoadBool("M_AutoFollowCursor", autoFollowCursor); + freecam = LoadBool("M_Freecam", freecam); + cameraZoom = LoadBool("M_CameraZoom", cameraZoom); + RevealVotesEnabled = LoadBool("M_RevealVotes", RevealVotesEnabled); + noTaskMode = LoadBool("M_NoTaskMode", noTaskMode); + neverEndGame = LoadBool("M_NeverEndGame", neverEndGame); + removePenalty = LoadBool("M_RemovePenalty", removePenalty); + alwaysShowLobbyTimer = LoadBool("M_AlwaysShowLobbyTimer", alwaysShowLobbyTimer); + autoBanEnabled = LoadBool("M_AutoBanEnabled", autoBanEnabled); + blockSpoofRPC = LoadBool("M_BlockSpoofRPC", blockSpoofRPC); + blockSabotageRPC = LoadBool("M_BlockSabotageRPC", blockSabotageRPC); + blockGameRpcInLobby = LoadBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); + blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); + blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); + AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); + AutoReturnLobbyAfterMatch = LoadBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); + AutoHostNotifications = LoadBool("M_AutoHostNotifications", AutoHostNotifications); + AutoHostForceLastMinute = LoadBool("M_AutoHostForceLastMinute", AutoHostForceLastMinute); + AutoHostWaitLoadedPlayers = LoadBool("M_AutoHostWaitLoadedPlayers", AutoHostWaitLoadedPlayers); + AutoHostCancelBelowMin = LoadBool("M_AutoHostCancelBelowMin", AutoHostCancelBelowMin); + AutoHostInstantStart = LoadBool("M_AutoHostInstantStart", AutoHostInstantStart); + if (PlayerPrefs.HasKey("M_AutoHostMinPlayers")) AutoHostMinPlayers = PlayerPrefs.GetInt("M_AutoHostMinPlayers"); + if (PlayerPrefs.HasKey("M_AutoHostStartDelaySeconds")) AutoHostStartDelaySeconds = PlayerPrefs.GetFloat("M_AutoHostStartDelaySeconds"); + if (PlayerPrefs.HasKey("M_AutoHostFastStartPlayers")) AutoHostFastStartPlayers = PlayerPrefs.GetInt("M_AutoHostFastStartPlayers"); + if (PlayerPrefs.HasKey("M_AutoHostFastStartDelaySeconds")) AutoHostFastStartDelaySeconds = PlayerPrefs.GetFloat("M_AutoHostFastStartDelaySeconds"); + if (PlayerPrefs.HasKey("M_WalkSpeed")) walkSpeed = PlayerPrefs.GetFloat("M_WalkSpeed"); + if (PlayerPrefs.HasKey("M_EngineSpeed")) engineSpeed = PlayerPrefs.GetFloat("M_EngineSpeed"); + keyBinds["Toggle Menu"] = KeyCode.Insert; + if (PlayerPrefs.HasKey("M_SpoofName")) customNameInput = PlayerPrefs.GetString("M_SpoofName"); + } + catch { } + } + private Texture2D MakeRoundedTex(int size, Color col, float radius) + { + Texture2D result = new Texture2D(size, size, TextureFormat.RGBA32, false); + result.hideFlags = HideFlags.HideAndDontSave; + Color[] pix = new Color[size * size]; + float center = size / 2f; + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); + float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); + float dist = Mathf.Sqrt(dx * dx + dy * dy); + float alpha = Mathf.Clamp01(radius - dist + 0.5f); + Color c = col; + c.a = col.a * alpha; + pix[y * size + x] = c; + } + } + result.SetPixels(pix); result.Apply(); + return result; + } + + private RectOffset CreateRectOffset(int left, int right, int top, int bottom) + { + return new RectOffset { left = left, right = right, top = top, bottom = bottom }; + } + + private void UpdateSwitchTex(Texture2D tex, bool isOn, Color accentColor) + { + int width = tex.width; int height = tex.height; + Color transparent = new Color(0, 0, 0, 0); + Color offBg = new Color(0.23f, 0.23f, 0.23f, 1f); + Color offKnob = new Color(0.6f, 0.6f, 0.6f, 1f); + Color bgColor = isOn ? accentColor : offBg; + Color knobColor = isOn ? Color.white : offKnob; + float r = height / 2f; + float cx1 = r; float cx2 = width - r; float cy = r; + float knobRadius = r - 2f; + float knobCx = isOn ? cx2 : cx1; + Color[] pixels = new Color[width * height]; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + float dLeft = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx1, cy)); + float dRight = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx2, cy)); + float dRect = (x + 0.5f >= cx1 && x + 0.5f <= cx2) ? Mathf.Abs((y + 0.5f) - cy) : 9999f; + float distBg = Mathf.Min(dLeft, Mathf.Min(dRight, dRect)); + float alphaBg = Mathf.Clamp01(r - distBg + 0.5f); + float distKnob = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(knobCx, cy)); + float alphaKnob = Mathf.Clamp01(knobRadius - distKnob + 0.5f); + if (alphaBg > 0) + { + Color finalCol = Color.Lerp(bgColor, knobColor, alphaKnob); + finalCol.a = alphaBg; + pixels[y * width + x] = finalCol; + } + else pixels[y * width + x] = transparent; + } + } + tex.SetPixels(pixels); tex.Apply(); + } + + private static Color GetThemeAccentColor(Color source) + { + if (!whiteMenuTheme) return source; + + Color.RGBToHSV(source, out float h, out float s, out float v); + + if (s < 0.08f) + return new Color(0.34f, 0.34f, 0.34f, 1f); + + if (h <= 0.04f || h >= 0.96f) + return new Color(0.50f, 0.14f, 0.18f, 1f); + + if (h >= 0.11f && h <= 0.19f) + return new Color32(232, 194, 37, 255); + + float targetS = Mathf.Clamp(Mathf.Max(s, 0.55f), 0.55f, 0.95f); + float targetV = Mathf.Clamp(v * 0.62f, 0.26f, 0.72f); + Color mapped = Color.HSVToRGB(h, targetS, targetV); + mapped.a = 1f; + return mapped; + } + + private void UpdateAccentColor(Color color) + { + currentAccentColor = color; + Color effectiveColor = GetThemeAccentColor(color); + if (texAccent != null) + { + int size = texAccent.width; + Color[] pix = new Color[size * size]; + float center = size / 2f; + float radius = 6f; + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); + float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); + float dist = Mathf.Sqrt(dx * dx + dy * dy); + float alpha = Mathf.Clamp01(radius - dist + 0.5f); + Color c = effectiveColor; c.a = alpha; + pix[y * size + x] = c; + } + } + texAccent.SetPixels(pix); texAccent.Apply(); + } + if (texSliderThumb != null) + { + int size = texSliderThumb.width; + Color[] pix = new Color[size * size]; + float center = size / 2f; + float radius = 10f; + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); + float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); + float dist = Mathf.Sqrt(dx * dx + dy * dy); + float alpha = Mathf.Clamp01(radius - dist + 0.5f); + Color c = effectiveColor; c.a = alpha; + pix[y * size + x] = c; + } + } + texSliderThumb.SetPixels(pix); texSliderThumb.Apply(); + } + if (texScrollThumb != null) + { + int size = texScrollThumb.width; + Color[] pix = new Color[size * size]; + float center = size / 2f; + float radius = 4f; + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); + float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); + float dist = Mathf.Sqrt(dx * dx + dy * dy); + float alpha = Mathf.Clamp01(radius - dist + 0.5f); + Color c = effectiveColor; c.a = alpha; + pix[y * size + x] = c; + } + } + texScrollThumb.SetPixels(pix); texScrollThumb.Apply(); + } + if (texToggleOn != null) UpdateSwitchTex(texToggleOn, true, effectiveColor); + if (windowStyle != null) windowStyle.normal.textColor = whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : color; + if (headerStyle != null) headerStyle.normal.textColor = whiteMenuTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : color; + if (activeSidebarBtnStyle != null) { activeSidebarBtnStyle.normal.textColor = effectiveColor; activeSidebarBtnStyle.hover.textColor = effectiveColor; } + if (activeTabStyle != null) activeTabStyle.normal.background = texAccent; + if (activeSubTabStyle != null) activeSubTabStyle.normal.background = texAccent; + if (btnStyle != null) btnStyle.active.background = texAccent; + if (inputBlockStyle != null) inputBlockStyle.normal.textColor = whiteMenuTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : color; + } + + private void InitStyles() + { + bool isLightTheme = whiteMenuTheme; + Color accent = GetThemeAccentColor(currentAccentColor); + Color darkBg = isLightTheme ? new Color(0.97f, 0.97f, 0.97f, 0.78f) : new Color(0.12f, 0.12f, 0.12f, 0.90f); + Color sidebarBg = new Color(0.0f, 0.0f, 0.0f, 0.0f); + Color boxBg = new Color(0f, 0f, 0f, 0f); + Color btnCol = isLightTheme ? new Color(0.90f, 0.90f, 0.90f, 0.74f) : new Color(0.23f, 0.23f, 0.23f, 1f); + Color sliderBgCol = isLightTheme ? new Color(0.78f, 0.78f, 0.78f, 0.68f) : new Color(0.08f, 0.08f, 0.08f, 1f); + Color textMain = isLightTheme ? new Color(0.18f, 0.18f, 0.18f, 1f) : new Color(0.78f, 0.78f, 0.78f, 1f); + Color textMuted = isLightTheme ? new Color(0.33f, 0.33f, 0.33f, 1f) : new Color(0.6f, 0.6f, 0.6f, 1f); + Color textHover = isLightTheme ? new Color(0.06f, 0.06f, 0.06f, 1f) : Color.white; + Color headerText = isLightTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : accent; + Color inputBgCol = isLightTheme ? new Color(1f, 1f, 1f, 0.86f) : new Color(0.08f, 0.08f, 0.08f, 0.85f); + + texWindowBg = MakeRoundedTex(64, darkBg, 12f); + texSidebarBg = MakeRoundedTex(64, sidebarBg, 0f); + texBoxBg = MakeRoundedTex(64, boxBg, 0f); + texBtnBg = MakeRoundedTex(64, btnCol, 6f); + texAccent = MakeRoundedTex(64, accent, 6f); + texSliderBg = MakeRoundedTex(64, sliderBgCol, 4f); + texSliderThumb = MakeRoundedTex(20, accent, 10f); + texInputBg = MakeRoundedTex(64, inputBgCol, 6f); + texColorBtn = MakeRoundedTex(64, Color.white, 12f); + + texToggleOff = new Texture2D(30, 16, TextureFormat.RGBA32, false); + texToggleOff.hideFlags = HideFlags.HideAndDontSave; + texToggleOn = new Texture2D(30, 16, TextureFormat.RGBA32, false); + texToggleOn.hideFlags = HideFlags.HideAndDontSave; + UpdateSwitchTex(texToggleOff, false, Color.white); + UpdateSwitchTex(texToggleOn, true, accent); + + safeLineStyle = new GUIStyle(); + safeLineStyle.normal.background = MakeRoundedTex(2, isLightTheme ? new Color(0.75f, 0.75f, 0.75f, 1f) : Color.white, 0f); + + windowStyle = new GUIStyle(); + windowStyle.normal.background = texWindowBg; + windowStyle.normal.textColor = accent; + windowStyle.fontStyle = FontStyle.Bold; + windowStyle.fontSize = 14; + windowStyle.padding = CreateRectOffset(0, 0, 0, 0); + windowStyle.border = CreateRectOffset(12, 12, 12, 12); + + boxStyle = new GUIStyle(); + boxStyle.normal.background = texBoxBg; + boxStyle.padding = CreateRectOffset(0, 0, 0, 0); + boxStyle.margin = CreateRectOffset(0, 0, 4, 8); + + btnStyle = new GUIStyle(GUI.skin.button); + btnStyle.normal.background = texBtnBg; + btnStyle.normal.textColor = textMain; + btnStyle.active.background = texAccent; + btnStyle.active.textColor = Color.black; + btnStyle.alignment = TextAnchor.MiddleCenter; + btnStyle.border = CreateRectOffset(6, 6, 6, 6); + btnStyle.fontSize = 12; + btnStyle.fontStyle = FontStyle.Bold; + + activeTabStyle = new GUIStyle(btnStyle); + activeTabStyle.normal.background = texAccent; + activeTabStyle.normal.textColor = Color.black; + + subTabStyle = new GUIStyle(btnStyle); + subTabStyle.padding = CreateRectOffset(6, 6, 2, 2); + activeSubTabStyle = new GUIStyle(activeTabStyle); + activeSubTabStyle.padding = CreateRectOffset(6, 6, 2, 2); + + inputBlockStyle = new GUIStyle(btnStyle); + inputBlockStyle.normal.background = texInputBg; + inputBlockStyle.hover.background = texInputBg; + inputBlockStyle.active.background = texAccent; + inputBlockStyle.normal.textColor = isLightTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : accent; + inputBlockStyle.alignment = TextAnchor.MiddleCenter; + inputBlockStyle.fontStyle = FontStyle.Bold; + + headerStyle = new GUIStyle(); + headerStyle.normal.background = texBtnBg; + headerStyle.normal.textColor = headerText; + headerStyle.fontStyle = FontStyle.Bold; + headerStyle.alignment = TextAnchor.MiddleLeft; + headerStyle.padding = CreateRectOffset(6, 6, 4, 4); + headerStyle.margin = CreateRectOffset(0, 0, 4, 4); + headerStyle.fontSize = 13; + + sidebarStyle = new GUIStyle(); + sidebarStyle.normal.background = texSidebarBg; + sidebarStyle.padding = CreateRectOffset(0, 0, 5, 0); + + sidebarBtnStyle = new GUIStyle(); + sidebarBtnStyle.normal.textColor = textMuted; + sidebarBtnStyle.hover.textColor = textHover; + sidebarBtnStyle.padding = CreateRectOffset(12, 0, 6, 6); + sidebarBtnStyle.alignment = TextAnchor.MiddleLeft; + sidebarBtnStyle.fontSize = 13; + sidebarBtnStyle.fontStyle = FontStyle.Bold; + + activeSidebarBtnStyle = new GUIStyle(sidebarBtnStyle); + activeSidebarBtnStyle.normal.textColor = accent; + activeSidebarBtnStyle.hover.textColor = accent; + + toggleOffStyle = new GUIStyle(); + toggleOffStyle.normal.background = texToggleOff; + toggleOnStyle = new GUIStyle(); + toggleOnStyle.normal.background = texToggleOn; + + toggleLabelStyle = new GUIStyle(); + toggleLabelStyle.normal.textColor = textMain; + toggleLabelStyle.alignment = TextAnchor.MiddleLeft; + toggleLabelStyle.padding = CreateRectOffset(4, 0, 0, 0); + toggleLabelStyle.fontSize = 12; + toggleLabelStyle.fontStyle = FontStyle.Bold; + + sliderStyle = new GUIStyle(); + sliderStyle.normal.background = texSliderBg; + sliderStyle.border = CreateRectOffset(6, 6, 6, 6); + sliderStyle.fixedHeight = 10f; + sliderStyle.margin = CreateRectOffset(0, 0, 8, 8); + + sliderThumbStyle = new GUIStyle(); + sliderThumbStyle.normal.background = texSliderThumb; + sliderThumbStyle.fixedWidth = 18f; + sliderThumbStyle.fixedHeight = 18f; + sliderThumbStyle.margin = CreateRectOffset(0, 0, -4, 0); + + titleStyle = new GUIStyle(); + titleStyle.normal.textColor = accent; + titleStyle.fontStyle = FontStyle.Bold; + titleStyle.fontSize = 14; + titleStyle.padding = CreateRectOffset(10, 0, 8, 0); + + Texture2D texScrollBg = MakeRoundedTex(8, new Color(0.1f, 0.1f, 0.1f, 0.2f), 4f); + texScrollThumb = MakeRoundedTex(8, accent, 4f); + + GUIStyle scrollBarStyle = new GUIStyle(GUI.skin.verticalScrollbar); + scrollBarStyle.normal.background = texScrollBg; + scrollBarStyle.fixedWidth = 8f; + scrollBarStyle.border = CreateRectOffset(0, 0, 4, 4); + scrollBarStyle.margin = CreateRectOffset(2, 2, 2, 2); + + GUIStyle scrollBarThumbStyle = new GUIStyle(GUI.skin.verticalScrollbarThumb); + scrollBarThumbStyle.normal.background = texScrollThumb; + scrollBarThumbStyle.hover.background = texScrollThumb; + scrollBarThumbStyle.active.background = texScrollThumb; + scrollBarThumbStyle.fixedWidth = 8f; + scrollBarThumbStyle.border = CreateRectOffset(0, 0, 4, 4); + + GUI.skin.verticalScrollbar = scrollBarStyle; + GUI.skin.verticalScrollbarThumb = scrollBarThumbStyle; + GUI.skin.horizontalScrollbar.normal.background = null; + GUI.skin.horizontalScrollbarThumb.normal.background = null; + GUI.skin.label.normal.textColor = textMain; + GUI.skin.box.normal.textColor = textMain; + + stylesInited = true; + } + public static bool autoCopyCodeAndLeave = false; + public static HashSet votedPlayerIds = new HashSet(); + + private void LoadBackgroundImage() + { + try + { + string bgPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "MenuBG.png"); + if (!System.IO.File.Exists(bgPath)) bgPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "MenuBG.jpg"); + if (System.IO.File.Exists(bgPath)) + { + byte[] fileData = System.IO.File.ReadAllBytes(bgPath); + Texture2D tempTex = new Texture2D(2, 2); + ImageConversion.LoadImage(tempTex, fileData); + customMenuBg = new Texture2D(tempTex.width, tempTex.height, TextureFormat.RGBA32, false); + customMenuBg.hideFlags = HideFlags.HideAndDontSave; + Color[] pix = tempTex.GetPixels(); + UnityEngine.Object.Destroy(tempTex); + int w = customMenuBg.width, h = customMenuBg.height; + float targetRadius = 12f, rx = targetRadius * (w / windowRect.width), ry = targetRadius * (h / windowRect.height); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + float dx = 0f, dy = 0f; + if (x < rx) dx = rx - x; + else if (x > w - rx) dx = x - (w - rx); + if (y < ry) dy = ry - y; + else if (y > h - ry) dy = y - (h - ry); + if (dx > 0 && dy > 0) + { + float nx = dx / rx, ny = dy / ry; + float dist = Mathf.Sqrt(nx * nx + ny * ny); + if (dist > 1f) { Color c = pix[y * w + x]; c.a = 0f; pix[y * w + x] = c; } + else + { + float alphaMult = Mathf.Clamp01((1f - dist) * Mathf.Max(rx, ry)); + Color c = pix[y * w + x]; c.a *= alphaMult; pix[y * w + x] = c; + } + } + } + customMenuBg.SetPixels(pix); customMenuBg.Apply(); + } + else enableBackground = false; + } + catch { enableBackground = false; } + } + + public static string ApplyMenuShimmer(string text) + { + string result = ""; + Color baseColor = currentAccentColor, glowColor = Color.white; + for (int i = 0; i < text.Length; i++) + { + if (text[i] == ' ') { result += " "; continue; } + float wave = Mathf.Sin(Time.unscaledTime * 6f - (i * 0.4f)) * 0.5f + 0.5f; + Color c = Color.Lerp(baseColor, glowColor, wave); + result += $"{text[i]}"; + } + return result; + } + + private bool DrawToggle(bool value, string text, int width = 0) + { + GUILayout.BeginHorizontal(GUILayout.Width(width > 0 ? width : 200)); + + bool clickedBox = GUILayout.Button("", value ? toggleOnStyle : toggleOffStyle, GUILayout.Width(30), GUILayout.Height(16)); + + GUILayout.Space(6); + + bool clickedText = GUILayout.Button(text, toggleLabelStyle); + + GUILayout.EndHorizontal(); + + return (clickedBox || clickedText) ? !value : value; + } + + private bool DrawBindableButton(string label, string bindKey, float width) + { + bool clicked = false; + GUILayout.BeginVertical(GUILayout.Width(width)); + if (GUILayout.Button(label, btnStyle, GUILayout.Height(25), GUILayout.Width(width))) clicked = true; + string bindTxt = bindingAction == bindKey ? "Press Key..." : (keyBinds.ContainsKey(bindKey) ? $"[{keyBinds[bindKey]}]" : "[Bind Key]"); + GUIStyle bindStyle = new GUIStyle(btnStyle) { fontSize = 10, normal = { textColor = new Color(0.6f, 0.6f, 0.6f) } }; + if (bindingAction == bindKey) bindStyle.normal.textColor = GetThemeAccentColor(currentAccentColor); + if (GUILayout.Button(bindTxt, bindStyle, GUILayout.Height(15), GUILayout.Width(width))) bindingAction = bindKey; + GUILayout.EndVertical(); + return clicked; + } + + private bool DrawHostToggle(bool value, string text, float totalWidth = 250f) + { + GUILayout.BeginHorizontal(GUILayout.Width(totalWidth), GUILayout.Height(20)); + bool clickedBox = GUILayout.Button("", value ? toggleOnStyle : toggleOffStyle, GUILayout.Width(30), GUILayout.Height(16)); + GUILayout.Space(6); + bool clickedText = GUILayout.Button(text, toggleLabelStyle, GUILayout.Width(totalWidth - 36f), GUILayout.Height(16)); + GUILayout.EndHorizontal(); + return (clickedBox || clickedText) ? !value : value; + } + private void DrawBindsTab() + { + GUILayout.BeginVertical(boxStyle); + try + { + GUILayout.Label("?? CUSTOM KEYBINDS", headerStyle); + GUILayout.Space(10); + + DrawKeybindRow("Magnet Cursor:", ref bindMagnetCursor, ref isWaitBindMagnetCursor); + DrawKeybindRow("Mass Morph:", ref bindMassMorph, ref isWaitBindMassMorph); + DrawKeybindRow("Spawn Lobby:", ref bindSpawnLobby, ref isWaitBindSpawnLobby); + DrawKeybindRow("Despawn Lobby:", ref bindDespawnLobby, ref isWaitBindDespawnLobby); + DrawKeybindRow("Close Meeting:", ref bindCloseMeeting, ref isWaitBindCloseMeeting); + DrawKeybindRow("Insta Start:", ref bindInstaStart, ref isWaitBindInstaStart); + DrawKeybindRow("End: Crewmate Win:", ref bindEndCrew, ref isWaitBindEndCrew); + DrawKeybindRow("End: Impostor Win:", ref bindEndImp, ref isWaitBindEndImp); + DrawKeybindRow("End: Imp Disconnect:", ref bindEndImpDC, ref isWaitBindEndImpDC); + DrawKeybindRow("End: H&S Disconnect:", ref bindEndHnsDC, ref isWaitBindEndHnsDC); + } + finally { GUILayout.EndVertical(); } + } + + private void DrawKeybindRow(string label, ref KeyCode currentKey, ref bool isWaiting) + { + GUILayout.BeginHorizontal(); + GUILayout.Space(10); + GUIStyle alignedLabel = new GUIStyle(toggleLabelStyle) { alignment = TextAnchor.MiddleLeft, margin = CreateRectOffset(0, 0, 4, 0) }; + GUILayout.Label(label, alignedLabel, GUILayout.Width(220), GUILayout.Height(25)); + + string bindText = isWaiting ? "Press any key..." : (currentKey == KeyCode.None ? "NONE" : currentKey.ToString()); + if (GUILayout.Button(bindText, isWaiting ? activeTabStyle : btnStyle, GUILayout.Width(120), GUILayout.Height(25))) + { + ResetAllBindWaits(); + isWaiting = true; + } + + if (GUILayout.Button("Clear", btnStyle, GUILayout.Width(50), GUILayout.Height(25))) + { + currentKey = KeyCode.None; + isWaiting = false; + SaveConfig(); + } + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + } + public static bool AnimShieldsEnabled = false; + public static bool AnimAsteroidsEnabled = false; + public static bool AnimCamsInUseEnabled = false; + public static bool IsScanning = false; + private void ResetAllBindWaits() + { + isWaitingForBind = false; + isWaitBindMassMorph = false; + isWaitBindSpawnLobby = false; + isWaitBindDespawnLobby = false; + isWaitBindCloseMeeting = false; + isWaitBindInstaStart = false; + isWaitBindEndCrew = false; + isWaitBindEndImp = false; + isWaitBindEndImpDC = false; + isWaitBindEndHnsDC = false; + isWaitBindMagnetCursor = false; + } + + private void DrawGeneralTab() + { + GUILayout.BeginHorizontal(); + for (int i = 0; i < generalSubTabs.Length; i++) + { + if (GUILayout.Button(generalSubTabs[i], currentGeneralSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(22))) + { + currentGeneralSubTab = i; + scrollPosition = Vector2.zero; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(10); + + if (currentGeneralSubTab == 0) DrawGeneralInfoTab(); + else if (currentGeneralSubTab == 1) DrawBindsTab(); + } + + private bool DrawColoredActionButton(string text, Color color, float width, float height = 24f) + { + GUIStyle style = new GUIStyle(btnStyle); + Color themedColor = whiteMenuTheme ? GetThemeAccentColor(color) : color; + Color hoverColor = whiteMenuTheme + ? Color.Lerp(themedColor, Color.black, 0.18f) + : Color.Lerp(themedColor, Color.white, 0.22f); + + style.normal.textColor = themedColor; + style.hover.textColor = hoverColor; + style.focused.textColor = themedColor; + style.active.textColor = whiteMenuTheme ? Color.white : Color.black; + + return GUILayout.Button(text, style, GUILayout.Width(width), GUILayout.Height(height)); + } + + private bool DrawPseudoInputButton(string value, bool editing, float height = 28f, int maxChars = 52) + { + GUIStyle style = new GUIStyle(editing ? activeTabStyle : inputBlockStyle); + style.alignment = TextAnchor.MiddleLeft; + style.clipping = TextClipping.Clip; + style.wordWrap = false; + style.padding = CreateRectOffset(10, 10, 0, 0); + + Rect rect = GUILayoutUtility.GetRect(GUIContent.none, style, GUILayout.ExpandWidth(true), GUILayout.Height(height)); + return GUI.Button(rect, FormatInputPreview(value, editing, maxChars), style); + } + + private void DrawClippedHint(string text, float height = 13f) + { + GUIStyle style = new GUIStyle(toggleLabelStyle) + { + fontSize = 10, + clipping = TextClipping.Clip, + wordWrap = false, + alignment = TextAnchor.MiddleLeft + }; + + Rect rect = GUILayoutUtility.GetRect(GUIContent.none, style, GUILayout.ExpandWidth(true), GUILayout.Height(height)); + GUI.Label(rect, text, style); + } + + private void OpenExternalLink(string url, string label) + { + try + { + Application.OpenURL(url); + ShowNotification($"[LINK] {L("Opening", "Открываю")} {label}"); + } + catch + { + ShowNotification($"[LINK] {L("Failed to open link.", "Не удалось открыть ссылку.")}"); + } + } + + private void DrawGeneralInfoTab() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("ELYSIUM OVERVIEW", headerStyle); + GUILayout.Space(6); + + GUILayout.BeginHorizontal(); + for (int i = 0; i < generalInfoSubTabs.Length; i++) + { + if (GUILayout.Button(generalInfoSubTabs[i], currentGeneralInfoSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Width(108), GUILayout.Height(24))) + { + currentGeneralInfoSubTab = i; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(10); + + string accentHex = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); + string githubHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(26, 188, 156, 255)) : new Color32(26, 188, 156, 255)); + string goldHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(255, 187, 54, 255)) : new Color32(255, 187, 54, 255)); + string leadHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(255, 92, 122, 255)) : new Color32(255, 92, 122, 255)); + string devHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(38, 194, 129, 255)) : new Color32(38, 194, 129, 255)); + string contributorHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(109, 138, 255, 255)) : new Color32(109, 138, 255, 255)); + string dangerHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(231, 76, 60, 255)) : new Color32(231, 76, 60, 255)); + string safeHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(57, 255, 20, 255)) : new Color32(57, 255, 20, 255)); + string versionText = "1.3.0"; + + GUIStyle textStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 12 }; + textStyle.normal.textColor = whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : new Color(0.85f, 0.85f, 0.85f, 1f); + + if (currentGeneralInfoSubTab == 0) + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label( + $"{L("Welcome to", "Добро пожаловать в")} ElysiumModMenu " + + $"v{versionText} {L("by", "от")} meowchelo!", + textStyle); + GUILayout.Space(4); + GUILayout.Label(L( + "ElysiumModMenu is a lightweight BepInEx IL2CPP utility for Among Us with lobby tools, visuals, spoofing and host-side controls.", + "ElysiumModMenu это легкий BepInEx IL2CPP мод для Among Us с инструментами для лобби, визуалом, спуфингом и хост-функциями."), textStyle); + GUILayout.Label(L( + "Use the buttons below to open the GitHub repository or jump straight to the latest public release.", + "Кнопки ниже открывают GitHub репозиторий и страницу с последним публичным релизом."), textStyle); + GUILayout.Space(6); + + GUILayout.BeginHorizontal(); + if (DrawColoredActionButton("GitHub", new Color32(26, 188, 156, 255), 110f)) + OpenExternalLink("https://github.com/meowchelo/ElysiumModMenu", "GitHub"); + GUILayout.Space(6); + if (DrawColoredActionButton("Check for Updates", new Color32(255, 187, 54, 255), 165f)) + OpenExternalLink("https://github.com/meowchelo/ElysiumModMenu/releases/latest", "Latest Release"); + GUILayout.EndHorizontal(); + + GUILayout.Space(8); + GUILayout.Label($"{L("Project", "Проект")}: meowchelo/ElysiumModMenu", textStyle); + GUILayout.Label($"{L("Main page", "Главная ссылка")}: https://github.com/meowchelo/ElysiumModMenu", textStyle); + GUILayout.Space(8); + GUILayout.Label($"{L("ElysiumModMenu is free and open-source software.", "ElysiumModMenu это бесплатный open-source проект.")}", textStyle); + GUILayout.Label($"{L("If you paid for this menu, demand a refund immediately.", "Если вы заплатили за это меню, требуйте возврат денег сразу.")}", textStyle); + GUILayout.Label($"{L("Make sure you are using the latest version from GitHub releases.", "Убедитесь, что используете последнюю версию из GitHub releases.")}", textStyle); + GUILayout.Space(8); + GUILayout.Label($"{L("Quick Hotkeys", "Быстрые клавиши")}", textStyle); + GUILayout.Label(L("Insert / Right Shift: open or close menu", "Insert / Right Shift: открыть или закрыть меню"), textStyle); + GUILayout.Label(L("Right Click: teleport to cursor", "ПКМ: телепорт к курсору"), textStyle); + GUILayout.Label(L("F9: magnet cursor", "F9: магнит курсора"), textStyle); + GUILayout.EndVertical(); + } + else + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label(L( + "ElysiumModMenu is an open-source project. Meet the people behind this build:", + "ElysiumModMenu это open-source проект. Ниже люди, которые стоят за этой сборкой:"), textStyle); + GUILayout.Space(8); + + GUILayout.Label($"LEAD DEVELOPER", textStyle); + GUILayout.Space(4); + if (DrawColoredActionButton("meowchelo", new Color32(255, 92, 122, 255), 150f)) + OpenExternalLink("https://github.com/meowchelo", "meowchelo"); + + GUILayout.Space(10); + GUILayout.Label($"DEVELOPERS", textStyle); + GUILayout.Space(4); + GUILayout.BeginHorizontal(); + if (DrawColoredActionButton("abobanamne", new Color32(38, 194, 129, 255), 150f)) + OpenExternalLink("https://github.com/abobanamne", "abobanamne"); + GUILayout.Space(6); + if (DrawColoredActionButton("wextikit", new Color32(109, 138, 255, 255), 150f)) + OpenExternalLink("https://github.com/wextikit", "wextikit"); + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + GUILayout.Label($"{L("Repository", "Репозиторий")}", textStyle); + GUILayout.Label(L( + "The public source, releases and project updates are published on GitHub.", + "Публичный исходный код, релизы и обновления проекта публикуются на GitHub."), textStyle); + GUILayout.Space(4); + if (DrawColoredActionButton("Open ElysiumModMenu Repository", new Color32(26, 188, 156, 255), 220f)) + OpenExternalLink("https://github.com/meowchelo/ElysiumModMenu", "ElysiumModMenu Repository"); + + GUILayout.Space(10); + GUILayout.Label($"{L("Notes", "Примечание")}", textStyle); + GUILayout.Label(L( + "Thank you to everyone helping with ideas, testing and polishing the menu.", + "Спасибо всем, кто помогает идеями, тестами и полировкой меню."), textStyle); + GUILayout.EndVertical(); + } + + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + } + [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] + public static class ChatLogger_Patch + { + public static void Prefix(PlayerControl sourcePlayer, ref string chatText) + { + if (!ElysiumModMenuGUI.enableChatLog || string.IsNullOrWhiteSpace(chatText)) return; + + try + { + string time = System.DateTime.Now.ToString("HH:mm:ss"); + + string name = "System/Unknown"; + string levelStr = "?"; + string fc = "Hidden"; + string puid = "Unknown"; + string platformStr = "Unknown"; + + if (sourcePlayer != null && sourcePlayer.Data != null) + { + name = sourcePlayer.Data.PlayerName; + + uint rawLevel = sourcePlayer.Data.PlayerLevel; + if (rawLevel != uint.MaxValue && rawLevel < 10000) levelStr = (rawLevel + 1).ToString(); + + fc = GetDisplayedFriendCode(sourcePlayer.Data, "Hidden"); + + var client = AmongUsClient.Instance?.GetClientFromCharacter(sourcePlayer); + if (client != null) + { + puid = client.Id.ToString(); + platformStr = ElysiumModMenuGUI.GetPlatform(client); + } + } + + string cleanText = System.Text.RegularExpressions.Regex.Replace(chatText, "<.*?>", string.Empty); + + string logLine = $"[{time}] [{name}] [Lv:{levelStr}] [FC:{fc}] [ID:{puid}] [{platformStr}] : {cleanText}\n"; + + string chatLogPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ChatLog.txt"); + System.IO.File.AppendAllText(chatLogPath, logLine); + } + catch { } + } + } + + + private void DrawSelfTab() + { + if (currentSelfSubTab == 0) currentSelfSubTab = 1; + + float selfColumnWidth = Mathf.Max(270f, (windowRect.width - 170f) * 0.5f); + + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(GUILayout.Width(selfColumnWidth)); + DrawSelfSpoof(); + GUILayout.EndVertical(); + + GUILayout.Space(8); + + GUILayout.BeginVertical(boxStyle, GUILayout.Width(selfColumnWidth), GUILayout.ExpandHeight(false)); + GUILayout.Label("SELF TOOLS", headerStyle); + GUILayout.Space(4); + + GUILayout.BeginHorizontal(); + for (int i = 0; i < selfOtherTabs.Length; i++) + { + int tabIndex = i + 1; + if (GUILayout.Button(selfOtherTabs[i], currentSelfSubTab == tabIndex ? activeSubTabStyle : subTabStyle, GUILayout.Height(22))) + { + currentSelfSubTab = tabIndex; + scrollPosition = Vector2.zero; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(8); + + if (currentSelfSubTab == 1) DrawPlayerMovementCompact(); + else if (currentSelfSubTab == 2) DrawRolesCompact(); + else if (currentSelfSubTab == 3) DrawChatSettingsCompact(); + + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + + private void DrawPlayerMovementCompact() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("MOVEMENT & TELEPORT", headerStyle); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"Engine: {Mathf.Round(engineSpeed)}x", toggleLabelStyle, GUILayout.Width(86)); + engineSpeed = GUILayout.HorizontalSlider(engineSpeed, 1f, 555f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + if (GUILayout.Button("R", btnStyle, GUILayout.Width(28), GUILayout.Height(22))) engineSpeed = 1f; + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + GUILayout.Label($"Walk: {Mathf.Round(walkSpeed)}x", toggleLabelStyle, GUILayout.Width(86)); + walkSpeed = GUILayout.HorizontalSlider(walkSpeed, 1f, 30f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + if (GUILayout.Button("R", btnStyle, GUILayout.Width(28), GUILayout.Height(22))) walkSpeed = 1f; + GUILayout.EndHorizontal(); + + GUILayout.Space(8); + tpToCursor = DrawToggle(tpToCursor, "TP To Cursor", 230); + GUILayout.Space(3); + dragToCursor = DrawToggle(dragToCursor, "Drag To Cursor", 230); + GUILayout.Space(3); + autoFollowCursor = DrawToggle(autoFollowCursor, "Magnet Cursor (F9)", 230); + GUILayout.Space(3); + noClip = DrawToggle(noClip, "True NoClip", 230); + + GUILayout.EndVertical(); + } + + private void DrawRolesCompact() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("ROLE TOOLS", headerStyle); + + GUIStyle roleMidStyle = new GUIStyle(btnStyle) + { + fontStyle = FontStyle.Bold, + normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, + alignment = TextAnchor.MiddleCenter + }; + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("<", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) + { + fakeRoleIdx--; + if (fakeRoleIdx < 0) fakeRoleIdx = forceRoleOptions.Length - 1; + } + GUILayout.Label(forceRoleOptions[fakeRoleIdx].ToString(), roleMidStyle, GUILayout.ExpandWidth(true), GUILayout.Height(24)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) + { + fakeRoleIdx++; + if (fakeRoleIdx >= forceRoleOptions.Length) fakeRoleIdx = 0; + } + if (GUILayout.Button("Set", activeTabStyle, GUILayout.Width(42), GUILayout.Height(24))) + RoleManager.Instance?.SetRole(PlayerControl.LocalPlayer, forceRoleOptions[fakeRoleIdx]); + GUILayout.EndHorizontal(); + + GUILayout.Space(8); + GUILayout.Label("IMPOSTOR", headerStyle); + killReach = DrawToggle(killReach, "Kill Reach", 230); + GUILayout.Space(3); + killAnyone = DrawToggle(killAnyone, "Kill Anyone", 230); + GUILayout.Space(3); + killAuraHostOnly = DrawToggle(killAuraHostOnly, "Kill Aura", 230); + GUILayout.Space(3); + noKillCooldownHostOnly = DrawToggle(noKillCooldownHostOnly, "Kill Cooldown 0", 230); + GUILayout.Space(3); + spamReportBodies = DrawToggle(spamReportBodies, "Spam Report Bodies", 230); + + GUILayout.Space(8); + GUILayout.Label("SPECIAL ROLES", headerStyle); + NoShapeshiftAnim = DrawToggle(NoShapeshiftAnim, "No Ss Animation", 230); + GUILayout.Space(3); + endlessSsDuration = DrawToggle(endlessSsDuration, "Endless Ss Duration", 230); + GUILayout.Space(3); + EndlessTracking = DrawToggle(EndlessTracking, "Endless Tracking", 230); + GUILayout.Space(3); + NoTrackingCooldown = DrawToggle(NoTrackingCooldown, "No Track Cooldown", 230); + GUILayout.Space(3); + endlessVentTime = DrawToggle(endlessVentTime, "Endless Vent Time", 230); + GUILayout.Space(3); + noVentCooldown = DrawToggle(noVentCooldown, "No Vent Cooldown", 230); + GUILayout.Space(3); + endlessBattery = DrawToggle(endlessBattery, "Endless Battery", 230); + GUILayout.Space(3); + noVitalsCooldown = DrawToggle(noVitalsCooldown, "No Vitals Cooldown", 230); + GUILayout.Space(3); + UnlimitedInterrogateRange = DrawToggle(UnlimitedInterrogateRange, "Interrogate Reach", 230); + + GUILayout.EndVertical(); + } + + private void DrawChatSettingsCompact() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label(L("CHAT SETTINGS", "НАСТРОЙКИ ЧАТА"), headerStyle); + + alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 230); + GUILayout.Space(3); + readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 230); + GUILayout.Space(3); + enableExtendedChat = DrawToggle(enableExtendedChat, L("Extended Chat", "Длинный чат"), 230); + GUILayout.Space(3); + enableFastChat = DrawToggle(enableFastChat, L("Fast Chat", "Быстрый чат"), 230); + GUILayout.Space(3); + allowLinksAndSymbols = DrawToggle(allowLinksAndSymbols, L("Links & Symbols", "Ссылки и символы"), 230); + GUILayout.Space(3); + enableSpellCheck = DrawToggle(enableSpellCheck, L("Spell Check", "Проверка орфографии"), 230); + + GUILayout.Space(8); + GUILayout.Label(L("CHAT UTILITY", "УТИЛИТЫ ЧАТА"), headerStyle); + enableChatHistory = DrawToggle(enableChatHistory, L("Chat History", "История чата"), 230); + GUILayout.Space(3); + enableClipboard = DrawToggle(enableClipboard, L("Clipboard", "Буфер обмена"), 230); + GUILayout.Space(3); + enableChatLog = DrawToggle(enableChatLog, L("Save Chat Log", "Сохранять лог чата"), 230); + GUILayout.Space(3); + enableChatDarkMode = DrawToggle(enableChatDarkMode, L("Dark Chat Theme", "Темная тема чата"), 230); + GUILayout.Space(3); + enableColorCommand = DrawToggle(enableColorCommand, L("Enable /color", "Разрешить /color"), 230); + GUILayout.Space(3); + blockFortegreenChat = DrawToggle(blockFortegreenChat, L("Block Fortegreen", "Блок Fortegreen"), 230); + GUILayout.Space(3); + blockRainbowChat = DrawToggle(blockRainbowChat, L("Block Rainbow", "Блок Rainbow"), 230); + + GUILayout.Space(8); + GUILayout.Label(L("CHAT SENDER", "ОТПРАВКА ЧАТА"), headerStyle); + GUILayout.Space(4); + + GUIStyle fieldStyle = new GUIStyle(GUI.skin.textField) + { + fontSize = 12, + alignment = TextAnchor.MiddleLeft, + clipping = TextClipping.Clip + }; + fieldStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); + + Rect chatInputRect = GUILayoutUtility.GetRect(10f, 32f, GUILayout.ExpandWidth(true), GUILayout.Height(32)); + GUI.Box(chatInputRect, string.Empty, fieldStyle); + + string drawText = string.IsNullOrEmpty(customChatMessage) + ? L("Type a message...", "Введите сообщение...") + : customChatMessage; + if (customChatInputFocused && (Time.unscaledTime % 1f) < 0.5f) drawText += "|"; + + GUIStyle chatInputTextStyle = new GUIStyle(GUI.skin.label) + { + alignment = TextAnchor.MiddleLeft, + clipping = TextClipping.Clip, + richText = false, + fontSize = 12 + }; + chatInputTextStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); + GUI.Label(new Rect(chatInputRect.x + 9f, chatInputRect.y + 3f, chatInputRect.width - 18f, chatInputRect.height - 6f), drawText, chatInputTextStyle); + + Event e = Event.current; + if (e != null) + { + if (e.type == EventType.MouseDown) + { + customChatInputFocused = chatInputRect.Contains(e.mousePosition); + if (customChatInputFocused) e.Use(); + } + else if (customChatInputFocused && e.type == EventType.KeyDown) + { + if (HandleClipboardShortcut(e, ref customChatMessage, 120)) + { + } + else if (e.keyCode == KeyCode.Backspace) + { + if (!string.IsNullOrEmpty(customChatMessage)) + customChatMessage = customChatMessage.Substring(0, customChatMessage.Length - 1); + e.Use(); + } + else if (e.keyCode == KeyCode.Escape) + { + customChatInputFocused = false; + e.Use(); + } + else if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) + { + TrySendCustomChatMessage(customChatMessage); + e.Use(); + } + else if (!char.IsControl(e.character)) + { + if (customChatMessage == null) customChatMessage = string.Empty; + if (customChatMessage.Length < 120) customChatMessage += e.character; + e.Use(); + } + } + } + + GUILayout.Space(6); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(L("Send", "Отправить"), btnStyle, GUILayout.Height(28))) + TrySendCustomChatMessage(customChatMessage); + GUILayout.Space(6); + string spamBtnText = customChatSpamEnabled ? L("Spam ON", "Спам ВКЛ") : L("Spam OFF", "Спам ВЫКЛ"); + if (GUILayout.Button(spamBtnText, customChatSpamEnabled ? activeTabStyle : btnStyle, GUILayout.Height(28))) + customChatSpamEnabled = !customChatSpamEnabled; + GUILayout.EndHorizontal(); + + GUILayout.Space(6); + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Delay:", "Задержка:")} {Mathf.Round(customChatSpamDelay * 10f) / 10f}s", toggleLabelStyle, GUILayout.Width(92)); + customChatSpamDelay = GUILayout.HorizontalSlider(customChatSpamDelay, 0.5f, 10f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + } + + private void DrawPlayerMovement() + { + GUILayout.BeginVertical(boxStyle); + try + { + GUILayout.Label("MOVEMENT & TELEPORT", headerStyle); + + GUILayout.BeginHorizontal(); + try + { + GUILayout.Label($"Engine Speed: {Mathf.Round(engineSpeed)}x", GUILayout.Width(130)); + engineSpeed = GUILayout.HorizontalSlider(engineSpeed, 1f, 555f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + GUILayout.Space(10); + if (GUILayout.Button("Reset", btnStyle, GUILayout.Width(50), GUILayout.Height(20))) engineSpeed = 1f; + } + finally { GUILayout.EndHorizontal(); } + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + try + { + GUILayout.Label($"Walk Speed: {Mathf.Round(walkSpeed)}x", GUILayout.Width(130)); + walkSpeed = GUILayout.HorizontalSlider(walkSpeed, 1f, 30f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + GUILayout.Space(10); + if (GUILayout.Button("Reset", btnStyle, GUILayout.Width(50), GUILayout.Height(20))) walkSpeed = 1f; + } + finally { GUILayout.EndHorizontal(); } + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + try + { + tpToCursor = DrawToggle(tpToCursor, "TP To Cursor", 160); + dragToCursor = DrawToggle(dragToCursor, "Drag To Cursor", 160); + GUILayout.FlexibleSpace(); + } + finally { GUILayout.EndHorizontal(); } + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + try + { + autoFollowCursor = DrawToggle(autoFollowCursor, "Magnet Cursor (F9)", 160); + noClip = DrawToggle(noClip, "True NoClip", 160); + GUILayout.FlexibleSpace(); + } + finally { GUILayout.EndHorizontal(); } + } + finally { GUILayout.EndVertical(); } + } + private void SmartEndGame(string outcome) + { + if (GameManager.Instance == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + + bool isHns = GameManager.Instance.IsHideAndSeek(); + int reasonCode = 0; + + switch (outcome) + { + case "CrewWin": reasonCode = isHns ? 7 : 0; break; + case "ImpWin": reasonCode = isHns ? 8 : 3; break; + case "ImpDisconnect": + case "HnsImpDisconnect": reasonCode = 5; break; + } + + bool tempBlock = neverEndGame; + neverEndGame = false; + GameManager.Instance.RpcEndGame((GameOverReason)reasonCode, false); + neverEndGame = tempBlock; + } + + private static string SanitizeSpoofFriendCode(string input) + { + if (string.IsNullOrWhiteSpace(input)) return ""; + + string clean = ""; + foreach (char c in input.ToLowerInvariant()) + { + if (char.IsWhiteSpace(c)) break; + if (char.IsLetterOrDigit(c)) clean += c; + if (clean.Length >= 10) break; + } + return clean; + } + + private static string BuildLocalNameRenderText(string input) + { + string value = (input ?? string.Empty).Replace("\r\n", "\n").Replace('\r', '\n'); + if (string.IsNullOrWhiteSpace(value)) return string.Empty; + + string trimmed = value.TrimStart(); + if (trimmed.StartsWith("shimmer:", StringComparison.OrdinalIgnoreCase)) + return ApplyMenuShimmer(trimmed.Substring("shimmer:".Length).TrimStart()); + + Match hexPrefix = Regex.Match(trimmed, @"^#([0-9A-Fa-f]{6})(.*)$"); + if (hexPrefix.Success) + { + string payload = hexPrefix.Groups[2].Value.TrimStart(' ', ':', '|', '-', '>'); + if (!string.IsNullOrEmpty(payload)) + return $"{payload}"; + } + + return value; + } + + private static string GetDisplayedFriendCode(NetworkedPlayerInfo data, string emptyValue = "Hidden") + { + if (data == null) return emptyValue; + + string value = data.FriendCode; + if (enableLocalFriendCodeSpoof && + PlayerControl.LocalPlayer != null && + data.PlayerId == PlayerControl.LocalPlayer.PlayerId && + !string.IsNullOrEmpty(localFriendCodeInput)) + { + value = localFriendCodeInput; + } + + return string.IsNullOrEmpty(value) ? emptyValue : value; + } + + public static bool PrepareLocalFriendCodeForSerialize(NetworkedPlayerInfo data, out string restoreValue) + { + restoreValue = null; + try + { + if (!enableLocalFriendCodeSpoof || enableFriendCodeSpoof) return false; + if (data == null || PlayerControl.LocalPlayer == null || data.PlayerId != PlayerControl.LocalPlayer.PlayerId) return false; + + restoreValue = data.FriendCode; + TrySetStringMember(data, "FriendCode", originalLocalFriendCode ?? string.Empty); + return true; + } + catch + { + restoreValue = null; + return false; + } + } + + public static void RestoreLocalFriendCodeAfterSerialize(NetworkedPlayerInfo data, string restoreValue) + { + try + { + if (data == null || restoreValue == null) return; + TrySetStringMember(data, "FriendCode", restoreValue); + } + catch { } + } + + private static string FormatInputPreview(string value, bool editing, int maxChars = 52) + { + string preview = value ?? string.Empty; + if (preview.Length > maxChars) + preview = "..." + preview.Substring(preview.Length - (maxChars - 3)); + + if (editing) preview += "_"; + return string.IsNullOrEmpty(preview) ? " " : preview; + } + + private static bool HandleClipboardShortcut(Event e, ref string target, int maxLength = -1) + { + if (e == null || e.type != EventType.KeyDown) return false; + + bool ctrlOrCmd = e.control || e.command; + bool pasteAlt = e.shift && e.keyCode == KeyCode.Insert; + if (!ctrlOrCmd && !pasteAlt) return false; + + target ??= string.Empty; + + if (ctrlOrCmd && e.keyCode == KeyCode.C) + { + GUIUtility.systemCopyBuffer = target; + e.Use(); + return true; + } + + if (ctrlOrCmd && e.keyCode == KeyCode.X) + { + GUIUtility.systemCopyBuffer = target; + target = string.Empty; + e.Use(); + return true; + } + + if ((ctrlOrCmd && e.keyCode == KeyCode.V) || pasteAlt) + { + string paste = (GUIUtility.systemCopyBuffer ?? string.Empty).Replace("\r\n", "\n").Replace('\r', '\n'); + if (paste.Length > 0) + { + target += paste; + if (maxLength >= 0 && target.Length > maxLength) + target = target.Substring(0, maxLength); + } + e.Use(); + return true; + } + + return false; + } + + private static bool IsBrokenFriendCode(string friendCode) + { + if (string.IsNullOrWhiteSpace(friendCode)) return true; + if (friendCode.Contains(" ")) return true; + if (friendCode.Contains("<") || friendCode.Contains(">")) return true; + if (!friendCode.Contains("#")) return true; + + string[] parts = friendCode.Split('#'); + if (parts.Length != 2) return true; + if (string.IsNullOrWhiteSpace(parts[0]) || string.IsNullOrWhiteSpace(parts[1])) return true; + if (parts[0].Length < 3 || parts[0].Length > 16) return true; + if (parts[1].Length < 3 || parts[1].Length > 8) return true; + if (!parts[0].All(char.IsLetterOrDigit)) return true; + if (!parts[1].All(char.IsDigit)) return true; + + return false; + } + + private void TryAutoBanBrokenFriendCodeTick() + { + try + { + if (!autoBanBrokenFriendCode) + { + brokenFcScanTimer = 0f; + brokenFcPunishedOwners.Clear(); + return; + } + + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) + { + brokenFcScanTimer = 0f; + return; + } + + if (PlayerControl.AllPlayerControls.Count <= 1) + brokenFcPunishedOwners.Clear(); + + brokenFcScanTimer += Time.deltaTime; + if (brokenFcScanTimer < 0.8f) return; + brokenFcScanTimer = 0f; + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; + + string fc = pc.Data.FriendCode ?? ""; + if (!IsBrokenFriendCode(fc)) continue; + + int owner = (int)pc.OwnerId; + if (brokenFcPunishedOwners.Contains(owner)) continue; + brokenFcPunishedOwners.Add(owner); + + string name = string.IsNullOrWhiteSpace(pc.Data.PlayerName) ? "Unknown" : pc.Data.PlayerName; + string puid = "Unknown"; + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(pc); + if (client != null) puid = client.Id.ToString(); + } + catch { } + + AddToBanList(string.IsNullOrWhiteSpace(fc) ? "Unknown" : fc, puid, name, "Broken FriendCode"); + AmongUsClient.Instance.KickPlayer(owner, true); + ShowNotification($"[ANTICHEAT] {name} banned: broken FC"); + } + } + catch { } + } + + private void DrawSelfSpoof() + { + GUILayout.BeginVertical(boxStyle); + GUIStyle greenHeader = new GUIStyle(headerStyle); + greenHeader.normal.textColor = GetThemeAccentColor(currentAccentColor); + GUILayout.Label("ACCOUNT SPOOFER", greenHeader); + + GUILayout.Space(4); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("LEVEL SPOOF", headerStyle); + GUILayout.BeginHorizontal(); + GUILayout.Label("Fake Level", btnStyle, GUILayout.Width(86), GUILayout.Height(28)); + if (DrawPseudoInputButton(spoofLevelString, isEditingLevel, 28f, 32)) + { + isEditingLevel = !isEditingLevel; + isEditingName = false; + isEditingFriendCode = false; + isEditingLocalFriendCode = false; + } + if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) + { + isEditingLevel = false; + if (uint.TryParse(spoofLevelString, out uint parsedLvl)) + { + try { AmongUs.Data.DataManager.Player.stats.level = parsedLvl > 0 ? parsedLvl - 1 : 0; AmongUs.Data.DataManager.Player.Save(); } + catch { try { AmongUs.Data.DataManager.Player.Stats.Level = parsedLvl > 0 ? parsedLvl - 1 : 0; AmongUs.Data.DataManager.Player.Save(); } catch { } } + } + SaveConfig(); + } + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(6); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("LOCAL NAME SPOOF", headerStyle); + bool newLocalNameToggle = DrawToggle(enableLocalNameSpoof, "Keep Local Nick", 180); + if (newLocalNameToggle != enableLocalNameSpoof) + { + enableLocalNameSpoof = newLocalNameToggle; + if (enableLocalNameSpoof) ApplyLocalNameSelf(customNameInput, false); + SaveConfig(); + } + GUILayout.Space(2); + GUILayout.BeginHorizontal(); + GUILayout.Label("Nick", btnStyle, GUILayout.Width(58), GUILayout.Height(28)); + if (DrawPseudoInputButton(customNameInput, isEditingName, 28f, 54)) + { + isEditingName = !isEditingName; + isEditingLevel = false; + isEditingFriendCode = false; + isEditingLocalFriendCode = false; + } + if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) + { + isEditingName = false; + ApplyLocalNameSelf(customNameInput, true); + SaveConfig(); + } + GUILayout.EndHorizontal(); + DrawClippedHint("Local only: no RPC broadcast. Supports shimmer:Text, #68B6E7Text and raw rich text."); + GUILayout.EndVertical(); + + GUILayout.Space(6); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("LOCAL FAKE FRIEND CODE", headerStyle); + bool newLocalFcToggle = DrawToggle(enableLocalFriendCodeSpoof, "Keep Fake FC Local", 180); + if (newLocalFcToggle != enableLocalFriendCodeSpoof) + { + enableLocalFriendCodeSpoof = newLocalFcToggle; + if (enableLocalFriendCodeSpoof) ApplyLocalFriendCodeSelf(localFriendCodeInput, false); + else RestoreLocalFriendCodeSelf(); + SaveConfig(); + } + GUILayout.Space(2); + GUILayout.BeginHorizontal(); + GUILayout.Label("Fake FC", btnStyle, GUILayout.Width(58), GUILayout.Height(28)); + if (DrawPseudoInputButton(localFriendCodeInput, isEditingLocalFriendCode, 28f, 54)) + { + isEditingLocalFriendCode = !isEditingLocalFriendCode; + isEditingName = false; + isEditingLevel = false; + isEditingFriendCode = false; + } + if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) + { + isEditingLocalFriendCode = false; + ApplyLocalFriendCodeSelf(localFriendCodeInput, true); + SaveConfig(); + } + GUILayout.EndHorizontal(); + DrawClippedHint("Local only: any text, any symbols. Used in this client UI only."); + GUILayout.EndVertical(); + + GUILayout.Space(6); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("FRIEND CODE SPOOF", headerStyle); + enableFriendCodeSpoof = DrawToggle(enableFriendCodeSpoof, "Enable FC Spoof", 180); + GUILayout.Space(2); + GUILayout.BeginHorizontal(); + if (DrawPseudoInputButton(spoofFriendCodeInput, isEditingFriendCode, 28f, 54)) + { + isEditingFriendCode = !isEditingFriendCode; + isEditingName = false; + isEditingLevel = false; + isEditingLocalFriendCode = false; + } + if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) + { + isEditingFriendCode = false; + spoofFriendCodeInput = SanitizeSpoofFriendCode(spoofFriendCodeInput); + SaveConfig(); + } + GUILayout.EndHorizontal(); + DrawClippedHint("Guest-style code: <=10, [a-z0-9], no spaces"); + GUILayout.EndVertical(); + + GUILayout.Space(6); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("PLATFORM SPOOF", headerStyle); + if (GUILayout.Button("Spoof Platform", enablePlatformSpoof ? activeTabStyle : btnStyle, GUILayout.Height(26))) + { + enablePlatformSpoof = !enablePlatformSpoof; + SaveConfig(); + } + GUILayout.Space(2); + string hexColor = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); + GUILayout.Label($"Platform: {platformNames[currentPlatformIndex]}", new GUIStyle(toggleLabelStyle) { fontSize = 12, richText = true }, GUILayout.Height(23)); + int newPlatIdx = (int)GUILayout.HorizontalSlider(currentPlatformIndex, 0, platformNames.Length - 1, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + if (newPlatIdx != currentPlatformIndex) + { + currentPlatformIndex = newPlatIdx; + SaveConfig(); + } + GUILayout.EndVertical(); + + GUILayout.Space(8); + GUILayout.Label("TASKS", headerStyle); + if (GUILayout.Button("Complete My Tasks", btnStyle, GUILayout.Height(30))) + { + if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.myTasks != null) + foreach (var task in PlayerControl.LocalPlayer.myTasks) + if (task != null && !task.IsComplete) PlayerControl.LocalPlayer.RpcCompleteTask((uint)task.Id); + } + GUILayout.EndVertical(); + } + + + + private void DrawVisualsTab() + { + GUILayout.BeginHorizontal(); + for (int i = 0; i < visualsSubTabs.Length; i++) + if (GUILayout.Button(visualsSubTabs[i], currentVisualsSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) + { currentVisualsSubTab = i; scrollPosition = Vector2.zero; } + GUILayout.EndHorizontal(); + GUILayout.Space(8); + if (currentVisualsSubTab == 0) DrawVisualsInGame(); + } + + + + [HarmonyPatch(typeof(PlayerBanData), nameof(PlayerBanData.BanPoints), MethodType.Setter)] + public static class RemoveDisconnectPenalty_Patch + { + public static bool Prefix(PlayerBanData __instance, ref float value) + { + if (!ElysiumModMenuGUI.removePenalty) return true; + if (AmongUsClient.Instance == null || AmongUsClient.Instance.NetworkMode != NetworkModes.OnlineGame) + return true; + + value = 0f; + return false; + } + } + + [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Start))] + public static class ShowLobbyTimer_Patch + { + public static void Postfix(GameStartManager __instance) + { + if (!ElysiumModMenuGUI.alwaysShowLobbyTimer) return; + + if (__instance == null || GameData.Instance == null || AmongUsClient.Instance == null) return; + if (AmongUsClient.Instance.NetworkMode == NetworkModes.LocalGame || !AmongUsClient.Instance.AmHost) return; + + if (HudManager.Instance != null) + { + HudManager.Instance.ShowLobbyTimer(600); + } + } + } + private void DrawPlayersTab() + { + GUILayout.BeginHorizontal(); + for (int i = 0; i < playersSubTabs.Length; i++) + if (GUILayout.Button(playersSubTabs[i], currentPlayersSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) + { currentPlayersSubTab = i; scrollPosition = Vector2.zero; } + GUILayout.EndHorizontal(); + GUILayout.Space(8); + + if (currentPlayersSubTab == 1) + { + DrawPlayersHistoryTab(); + return; + } + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(boxStyle, GUILayout.Width(200)); + playerListScrollPos = GUILayout.BeginScrollView(playerListScrollPos); + if (lockedPlayersList.Count > 0) + { + foreach (var pc in lockedPlayersList) + { + if (pc == null || pc.Data == null || pc.PlayerId >= 100) continue; + string pName = pc.Data.PlayerName ?? "Unknown"; + + if (forcedPreGameRoles.ContainsKey(pc.PlayerId)) pName += " [*]"; + else if (forcedImpostors.Contains(pc.PlayerId)) pName += " [Imp]"; + + bool isSelected = selectedHydraPlayerId == pc.PlayerId; + + GUI.contentColor = Color.white; + try { GUI.contentColor = Palette.PlayerColors[pc.Data.DefaultOutfit.ColorId]; } catch { } + + if (GUILayout.Button(pName, isSelected ? activeTabStyle : btnStyle, GUILayout.Height(30))) + { + selectedHydraPlayerId = pc.PlayerId; + } + GUI.contentColor = Color.white; + } + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + + GUILayout.BeginVertical(boxStyle, GUILayout.ExpandWidth(true)); + playerActionScrollPos = GUILayout.BeginScrollView(playerActionScrollPos); + + PlayerControl target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedHydraPlayerId); + + if (target != null && target.Data != null) + { + GUILayout.Label($"Selected: {target.Data.PlayerName}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 14 }); + GUILayout.Space(10); + GUILayout.BeginHorizontal(); + + GUI.backgroundColor = new Color(0.8f, 0.2f, 0.2f, 1f); + if (GUILayout.Button("KILL", btnStyle, GUILayout.Height(25))) + { + Vector3 op = PlayerControl.LocalPlayer.transform.position; + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(target.transform.position); + PlayerControl.LocalPlayer.CmdCheckMurder(target); + PlayerControl.LocalPlayer.RpcMurderPlayer(target, true); + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(op); + } + GUI.backgroundColor = Color.white; + + if (GUILayout.Button("TP TO", activeTabStyle, GUILayout.Height(25))) + { + teleportToPlayer(target); + ShowNotification($"[TELEPORT] Телепортирован к {target.Data.PlayerName}!"); + } + + GUI.backgroundColor = new Color(1f, 0.5f, 0f, 1f); + if (GUILayout.Button("Force Eject", btnStyle, GUILayout.Height(25))) ForceGlobalEject(target); + GUI.backgroundColor = Color.white; + + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + + if (GUILayout.Button("Force Meeting", btnStyle, GUILayout.Height(25))) ForceMeetingAsPlayer(target); + + bool hr = rainbowPlayers.Contains(target.PlayerId); + if (GUILayout.Button(hr ? "RGB: ON" : "RGB: OFF", hr ? activeTabStyle : btnStyle, GUILayout.Height(25))) + { + if (!hr) rainbowPlayers.Add(target.PlayerId); + else rainbowPlayers.Remove(target.PlayerId); + } + + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + + if (GUILayout.Button("Report Body", btnStyle, GUILayout.Height(25))) + AttemptReportBody(target); + + if (GUILayout.Button("Flood Tasks", btnStyle, GUILayout.Height(25))) + FloodPlayerWithTasks(target); + + if (GUILayout.Button("Clear Tasks", btnStyle, GUILayout.Height(25))) + ClearPlayerTasks(target); + + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + GUILayout.Label("TARGET ROLE CONTROL", headerStyle); + + GUILayout.BeginHorizontal(); + GUIStyle roleMidStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, alignment = TextAnchor.MiddleCenter }; + if (GUILayout.Button("<", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) + { + targetRoleAssignIdx--; + if (targetRoleAssignIdx < 0) targetRoleAssignIdx = roleAssignOptions.Length - 1; + } + GUILayout.Label(roleAssignNames[targetRoleAssignIdx], roleMidStyle, GUILayout.Height(24), GUILayout.ExpandWidth(true)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) + { + targetRoleAssignIdx++; + if (targetRoleAssignIdx >= roleAssignOptions.Length) targetRoleAssignIdx = 0; + } + GUILayout.EndHorizontal(); + + GUILayout.Space(4); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("TARGET -> ROLE", btnStyle, GUILayout.Height(26))) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ОШИБКА] Требуются права хоста!"); + } + else + { + SetPlayerRole(target, roleAssignOptions[targetRoleAssignIdx]); + ShowNotification($"[ROLE] {target.Data.PlayerName} -> {roleAssignNames[targetRoleAssignIdx]}"); + } + } + if (GUILayout.Button("REVIVE TARGET", activeTabStyle, GUILayout.Height(26))) + { + RevivePlayer(target); + } + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + GUILayout.Label("Morph Target:", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11 }); + GUILayout.BeginHorizontal(); + + int mIdx = lockedPlayersList.FindIndex(p => p.PlayerId == selectedMorphTargetId); + + GUI.backgroundColor = currentAccentColor; + if (GUILayout.Button("<", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) + { + if (lockedPlayersList.Count > 0) { mIdx--; if (mIdx < 0) mIdx = lockedPlayersList.Count - 1; selectedMorphTargetId = lockedPlayersList[mIdx].PlayerId; } + } + GUI.backgroundColor = Color.white; + + string morphName = "Target"; + if (mIdx >= 0 && mIdx < lockedPlayersList.Count) morphName = lockedPlayersList[mIdx].Data.PlayerName; + if (morphName.Length > 10) morphName = morphName.Substring(0, 10) + ".."; + + GUIStyle morphLabelStyle = new GUIStyle(btnStyle); + morphLabelStyle.normal.background = null; + morphLabelStyle.hover.background = null; + morphLabelStyle.normal.textColor = GetThemeAccentColor(currentAccentColor); + morphLabelStyle.fontStyle = FontStyle.Bold; + morphLabelStyle.alignment = TextAnchor.MiddleCenter; + + GUILayout.Label(morphName, morphLabelStyle, GUILayout.Height(25), GUILayout.ExpandWidth(true)); + + GUI.backgroundColor = currentAccentColor; + if (GUILayout.Button(">", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) + { + if (lockedPlayersList.Count > 0) { mIdx++; if (mIdx >= lockedPlayersList.Count) mIdx = 0; selectedMorphTargetId = lockedPlayersList[mIdx].PlayerId; } + } + GUILayout.EndHorizontal(); + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + GUI.backgroundColor = currentAccentColor; + if (GUILayout.Button("MORPH TARGET", btnStyle, GUILayout.Width(160), GUILayout.Height(25))) + { + var morphTarget = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedMorphTargetId) ?? target; + this.StartCoroutine(AttemptShapeshiftFrame(target, morphTarget).WrapToIl2Cpp()); + } + GUI.backgroundColor = Color.white; + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + GUILayout.Label("SET PLAYER COLOR", headerStyle); + GUILayout.BeginVertical(boxStyle); + + GUIStyle roundedColorBtnStyle = new GUIStyle(); + roundedColorBtnStyle.normal.background = texColorBtn; + roundedColorBtnStyle.margin = CreateRectOffset(2, 2, 2, 2); + + int colorsPerRow = 7; + for (int i = 0; i < Palette.PlayerColors.Length; i++) + { + if (i % colorsPerRow == 0) GUILayout.BeginHorizontal(); + + GUI.color = Palette.PlayerColors[i]; + + if (GUILayout.Button("", roundedColorBtnStyle, GUILayout.Width(32), GUILayout.Height(30))) + target.RpcSetColor((byte)i); + + if (i % colorsPerRow == colorsPerRow - 1 || i == Palette.PlayerColors.Length - 1) + GUILayout.EndHorizontal(); + } + GUI.color = Color.white; + GUILayout.EndVertical(); + + GUILayout.Space(15); + GUILayout.Label("PRE-GAME ROLE (HOST)", headerStyle); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Impostor", btnStyle, GUILayout.Height(25))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Add(target.PlayerId); enablePreGameRoleForce = true; } + if (GUILayout.Button("Crewmate", btnStyle, GUILayout.Height(25))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Crewmate; enablePreGameRoleForce = true; } + if (GUILayout.Button("Shapeshifter", btnStyle, GUILayout.Height(25))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Shapeshifter; enablePreGameRoleForce = true; } + GUILayout.EndHorizontal(); + GUILayout.Space(5); + if (GUILayout.Button("REMOVE FORCED ROLE", activeTabStyle, GUILayout.Height(25))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Remove(target.PlayerId); } + } + else + { + GUILayout.FlexibleSpace(); + GUILayout.Label("Select a player...", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); + GUILayout.FlexibleSpace(); + } + + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + + GUILayout.EndHorizontal(); + } + + private void DrawPlayersHistoryTab() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("PLAYER HISTORY", headerStyle); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"Entries: {playerHistoryEntries.Count}", new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(120)); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Clear History", btnStyle, GUILayout.Width(120), GUILayout.Height(24))) + playerHistoryEntries.Clear(); + GUILayout.EndHorizontal(); + + GUILayout.Space(6); + playersHistoryScroll = GUILayout.BeginScrollView(playersHistoryScroll); + if (playerHistoryEntries.Count == 0) + { + GUILayout.Label("История пока пустая.", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); + } + else + { + foreach (var e in playerHistoryEntries.OrderByDescending(x => x.LastSeenUtc)) + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label($"{e.Name} ({e.Platform})", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 13 }); + GUILayout.Label($"FC: {e.FriendCode} | PUID: {e.Puid}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); + GUILayout.Label($"Lv: {e.Level} | Last: {e.LastSeenUtc:HH:mm:ss}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); + GUILayout.EndVertical(); + GUILayout.Space(2); + } + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + } + private void ForceGlobalEject(PlayerControl target) + { + if (target == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ERROR] Нужны права Хоста!"); + return; + } + + try + { + target.Data.IsDead = false; + + if (MeetingHud.Instance == null) + { + MeetingHud.Instance = UnityEngine.Object.Instantiate(DestroyableSingleton.Instance.MeetingPrefab); + AmongUsClient.Instance.Spawn(MeetingHud.Instance.Cast(), -2, SpawnFlags.None); + } + + var emptyStates = new Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray(0); + + MeetingHud.Instance.RpcVotingComplete(emptyStates, target.Data, false); + + MeetingHud.Instance.RpcClose(); + + ShowNotification($"[EJECT] Изгоняем {target.Data.PlayerName}..."); + } + catch (Exception ex) + { + System.Console.WriteLine("Eject Error: " + ex.Message); + } + } + + private static bool IsDeadBodyForPlayerPresent(byte playerId) + { + try + { + var allBehaviours = UnityEngine.Object.FindObjectsOfType(); + foreach (var mb in allBehaviours) + { + if (mb == null || mb.gameObject == null) continue; + Type t = mb.GetType(); + if (t == null || t.Name != "DeadBody") continue; + + byte parentId = byte.MaxValue; + var parentProp = t.GetProperty("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentProp != null) + { + object val = parentProp.GetValue(mb, null); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + else + { + var parentField = t.GetField("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentField != null) + { + object val = parentField.GetValue(mb); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + } + + if (parentId == playerId) return true; + } + } + catch { } + + return false; + } + + private static void AttemptReportBody(PlayerControl target) + { + if (target == null || target.Data == null || PlayerControl.LocalPlayer == null) return; + + try + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + PlayerControl.LocalPlayer.CmdReportDeadBody(target.Data); + ShowNotification($"[REPORT] Репорт {target.Data.PlayerName}"); + return; + } + + if (LobbyBehaviour.Instance != null) + { + ShowNotification("[REPORT] Игра должна начаться."); + return; + } + + if (!target.Data.IsDead) + { + ShowNotification("[REPORT] Можно репортить только мертвых игроков."); + return; + } + + if (!IsDeadBodyForPlayerPresent(target.PlayerId)) + { + ShowNotification("[REPORT] Труп не найден или уже исчез."); + return; + } + + PlayerControl.LocalPlayer.CmdReportDeadBody(target.Data); + ShowNotification($"[REPORT] Репорт {target.Data.PlayerName}"); + } + catch (Exception ex) + { + System.Console.WriteLine($"Report body error: {ex.Message}"); + } + } + + private static void FloodPlayerWithTasks(PlayerControl target) + { + if (target == null || target.Data == null) + { + ShowNotification("[TASKS] Цель не найдена."); + return; + } + + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[TASKS] Нужны права хоста."); + return; + } + + try + { + byte[] taskIds = new byte[255]; + for (byte i = 0; i < 255; i++) taskIds[i] = i; + target.Data.RpcSetTasks(taskIds); + ShowNotification($"[TASKS] {target.Data.PlayerName} получил flood tasks."); + } + catch (Exception ex) + { + System.Console.WriteLine($"Flood tasks error: {ex.Message}"); + } + } + + private static void ClearPlayerTasks(PlayerControl target) + { + if (target == null || target.Data == null) + { + ShowNotification("[TASKS] Цель не найдена."); + return; + } + + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[TASKS] Нужны права хоста."); + return; + } + + try + { + target.Data.RpcSetTasks(Array.Empty()); + ShowNotification($"[TASKS] Задачи {target.Data.PlayerName} очищены."); + } + catch (Exception ex) + { + System.Console.WriteLine($"Clear tasks error: {ex.Message}"); + } + } + + private static string GetRoleDisplayName(RoleTypes role) + { + for (int i = 0; i < roleAssignOptions.Length; i++) + if (roleAssignOptions[i].Equals(role)) + return roleAssignNames[i]; + return role.ToString(); + } + + public static void RevivePlayer(PlayerControl target) + { + if (target == null || target.Data == null) + { + ShowNotification("[ОШИБКА] Цель не найдена!"); + return; + } + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ОШИБКА] Требуются права хоста!"); + return; + } + if (!target.Data.IsDead) + { + ShowNotification($"{target.Data.PlayerName} уже жив!"); + return; + } + + try + { + target.Data.IsDead = false; + + if (target.Collider != null) target.Collider.enabled = true; + + if (target.MyPhysics != null) + target.MyPhysics.gameObject.layer = LayerMask.NameToLayer("Players"); + + try + { + var allBehaviours = UnityEngine.Object.FindObjectsOfType(); + foreach (var mb in allBehaviours) + { + if (mb == null || mb.gameObject == null) continue; + Type t = mb.GetType(); + if (t == null || t.Name != "DeadBody") continue; + + byte parentId = byte.MaxValue; + + var parentProp = t.GetProperty("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentProp != null) + { + object val = parentProp.GetValue(mb, null); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + else + { + var parentField = t.GetField("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentField != null) + { + object val = parentField.GetValue(mb); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + } + + if (parentId == target.PlayerId) + mb.gameObject.SetActive(false); + } + } + catch { } + + bool wasImpTeam = false; + try + { + if (target.Data.Role != null) + { + int roleId = (int)target.Data.Role.Role; + wasImpTeam = roleId == 1 || roleId == 5 || roleId == 7 || roleId == 9 || roleId == 18; + } + else + { + var rt = target.Data.RoleType; + wasImpTeam = rt == RoleTypes.Impostor || rt == RoleTypes.Shapeshifter || (int)rt == 9 || (int)rt == 18; + } + } + catch { } + + target.RpcSetRole(wasImpTeam ? RoleTypes.Impostor : RoleTypes.Crewmate, true); + + var netObj = GameData.Instance?.GetComponent(); + if (netObj != null) netObj.SetDirtyBit(uint.MaxValue); + + ShowNotification($"[ВОСКРЕШЕНИЕ] {target.Data.PlayerName} воскрешён!"); + } + catch (Exception ex) + { + System.Console.WriteLine($"Revive error: {ex.Message}"); + ShowNotification("Ошибка воскрешения!"); + } + } + + public static void SetAllPlayersRole(RoleTypes role) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ОШИБКА] Требуются права хоста!"); + return; + } + if (PlayerControl.AllPlayerControls == null) return; + + int count = 0; + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + pc.RpcSetRole(role, true); + count++; + } + } + + ShowNotification($"[РОЛИ] {count} игрок(а/ов) получили роль {GetRoleDisplayName(role)}!"); + } + + public static void SetPlayerRole(PlayerControl target, RoleTypes newRole) + { + if (target == null || target.Data == null) return; + target.RpcSetRole(newRole, true); + } + + private void DrawRolesTab() + { + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(GUILayout.Width(280)); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Roles", headerStyle); + GUILayout.BeginHorizontal(); + GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) } }; + if (GUILayout.Button("<", btnStyle, GUILayout.Width(25), GUILayout.Height(22))) { fakeRoleIdx--; if (fakeRoleIdx < 0) fakeRoleIdx = forceRoleOptions.Length - 1; } + GUILayout.Label(forceRoleOptions[fakeRoleIdx].ToString(), middleLabelStyle, GUILayout.Width(100), GUILayout.Height(22)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(25), GUILayout.Height(22))) { fakeRoleIdx++; if (fakeRoleIdx >= forceRoleOptions.Length) fakeRoleIdx = 0; } + GUILayout.Space(15); + if (GUILayout.Button("Set", activeTabStyle, GUILayout.Width(45), GUILayout.Height(22))) RoleManager.Instance?.SetRole(PlayerControl.LocalPlayer, forceRoleOptions[fakeRoleIdx]); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Impostor", headerStyle); + killReach = DrawToggle(killReach, "Kill Reach", 160); + GUILayout.Space(5); + killAnyone = DrawToggle(killAnyone, "Kill Anyone", 160); + GUILayout.Space(5); + killAuraHostOnly = DrawToggle(killAuraHostOnly, "Kill Aura", 160); + GUILayout.Space(5); + noKillCooldownHostOnly = DrawToggle(noKillCooldownHostOnly, "Kill Cooldown 0 (Host)", 160); + GUILayout.Space(5); + spamReportBodies = DrawToggle(spamReportBodies, "Spam Report Bodies", 160); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Shapeshifter", headerStyle); + NoShapeshiftAnim = DrawToggle(NoShapeshiftAnim, "No Ss Animation", 160); + GUILayout.Space(5); + endlessSsDuration = DrawToggle(endlessSsDuration, "Endless Ss Duration", 160); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Tracker", headerStyle); + EndlessTracking = DrawToggle(EndlessTracking, "Endless Tracking", 160); + GUILayout.Space(5); + NoTrackingCooldown = DrawToggle(NoTrackingCooldown, "No Track Cooldown", 160); + GUILayout.EndVertical(); + + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.BeginVertical(GUILayout.Width(280)); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Engineer", headerStyle); + endlessVentTime = DrawToggle(endlessVentTime, "Endless Vent Time", 160); + GUILayout.Space(5); + noVentCooldown = DrawToggle(noVentCooldown, "No Vent Cooldown", 160); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Scientist", headerStyle); + endlessBattery = DrawToggle(endlessBattery, "Endless Battery", 160); + GUILayout.Space(5); + noVitalsCooldown = DrawToggle(noVitalsCooldown, "No Vitals Cooldown", 160); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Detective", headerStyle); + UnlimitedInterrogateRange = DrawToggle(UnlimitedInterrogateRange, "Interrogate Reach", 160); + GUILayout.EndVertical(); + + GUILayout.EndVertical(); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + } + + private Vector2 doorsScrollPos = Vector2.zero; + + private void DrawSabotagesTab() + { + GUIStyle miniLabelStyle = new GUIStyle(toggleLabelStyle) { fontSize = 11, richText = true, wordWrap = true }; + miniLabelStyle.normal.textColor = whiteMenuTheme ? new Color(0.25f, 0.25f, 0.25f, 1f) : new Color(0.72f, 0.72f, 0.72f, 1f); + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(boxStyle, GUILayout.Width(276), GUILayout.ExpandHeight(false)); + GUILayout.Label("CRITICAL SABOTAGES", headerStyle); + GUILayout.Space(4); + + GUILayout.BeginHorizontal(); + if (DrawColoredActionButton("FIX ALL", new Color32(83, 231, 139, 255), 126f, 32f)) FixAllSabotages(); + GUILayout.Space(6); + if (DrawColoredActionButton("TRIGGER ALL", new Color32(255, 74, 74, 255), 126f, 32f)) TriggerAllSabotages(); + GUILayout.EndHorizontal(); + + GUILayout.Space(6); + if (GUILayout.Button("CALL MEETING", btnStyle, GUILayout.Height(30))) callMeetingPublic(); + + GUILayout.Space(8); + GUILayout.BeginHorizontal(); + DrawSabotageButton("Reactor", ref reactorSab, ToggleReactor, new Color32(255, 84, 84, 255)); + GUILayout.Space(6); + DrawSabotageButton("Oxygen", ref oxygenSab, ToggleO2, new Color32(255, 132, 54, 255)); + GUILayout.EndHorizontal(); + + GUILayout.Space(6); + GUILayout.BeginHorizontal(); + DrawSabotageButton("Comms", ref commsSab, ToggleComms, new Color32(66, 205, 128, 255)); + GUILayout.Space(6); + DrawSabotageButton("Lights", ref elecSab, ToggleLights, new Color32(255, 218, 77, 255)); + GUILayout.EndHorizontal(); + + GUILayout.Space(8); + if (GUILayout.Button("MUSHROOM MIXUP", btnStyle, GUILayout.Height(28))) SabotageMushroom(); + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.BeginVertical(boxStyle, GUILayout.ExpandWidth(true)); + GUILayout.Label("DOOR LOCKDOWN", headerStyle); + GUILayout.Space(4); + GUILayout.Label("Global controls", miniLabelStyle); + + GUILayout.BeginHorizontal(); + if (DrawColoredActionButton("CLOSE", new Color32(255, 106, 66, 255), 88f, 30f)) SabotageDoors(); + GUILayout.Space(6); + if (DrawColoredActionButton("LOCK", new Color32(255, 184, 64, 255), 88f, 30f)) LockAllDoors(); + GUILayout.Space(6); + if (DrawColoredActionButton("OPEN", new Color32(89, 219, 146, 255), 88f, 30f)) OpenAllDoors(); + GUILayout.EndHorizontal(); + + GUILayout.Space(8); + GUILayout.Label("Target doors", miniLabelStyle); + + if (ShipStatus.Instance != null && ShipStatus.Instance.AllDoors != null) + { + var rooms = ShipStatus.Instance.AllDoors + .Where(d => d != null) + .Select(d => d.Room) + .Distinct() + .OrderBy(r => r.ToString()) + .ToList(); + + doorsScrollPos = GUILayout.BeginScrollView(doorsScrollPos, false, true, GUILayout.Height(214)); + foreach (var room in rooms) + { + DrawDoorTargetRow(room); + GUILayout.Space(3); + } + GUILayout.EndScrollView(); + } + else + { + GUILayout.FlexibleSpace(); + GUILayout.Label("Вы не в игре или на карте нет дверей.", new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, richText = true }); + GUILayout.FlexibleSpace(); + } + GUILayout.EndVertical(); + + GUILayout.EndHorizontal(); + } + + private void DrawSabotageButton(string label, ref bool state, Action toggleAction, Color accent) + { + GUIStyle style = state ? activeTabStyle : btnStyle; + Color oldBackground = GUI.backgroundColor; + GUI.backgroundColor = state ? accent : Color.white; + + if (GUILayout.Button(state ? label + " ON" : label, style, GUILayout.Height(30))) + { + state = !state; + toggleAction(state); + } + + GUI.backgroundColor = oldBackground; + } + + private void DrawDoorTargetRow(SystemTypes room) + { + GUILayout.BeginHorizontal(boxStyle); + GUILayout.Label($"{room}", toggleLabelStyle, GUILayout.Width(96)); + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("Close", btnStyle, GUILayout.Width(52), GUILayout.Height(24))) CloseDoorsOfType(room); + GUILayout.Space(4); + if (GUILayout.Button("Lock", activeSubTabStyle, GUILayout.Width(52), GUILayout.Height(24))) LockDoorsOfType(room); + GUILayout.Space(4); + if (GUILayout.Button("Open", btnStyle, GUILayout.Width(52), GUILayout.Height(24))) OpenDoorsOfType(room); + + GUILayout.EndHorizontal(); + } + private void callMeetingPublic() + { + if (PlayerControl.LocalPlayer == null || PlayerControl.AllPlayerControls == null) return; + try + { + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && pc.Data.IsDead && !pc.Data.Disconnected) + { + PlayerControl.LocalPlayer.CmdReportDeadBody(pc.Data); + ShowNotification($"[MEETING] Найден и зарепорчен труп: {pc.Data.PlayerName}!"); + return; + } + } + + PlayerControl.LocalPlayer.CmdReportDeadBody(null); + ShowNotification("[MEETING] Легально нажата кнопка собрания!"); + } + catch (Exception ex) + { + Debug.Log("Public Meeting Error: " + ex.Message); + } + } + private void TriggerAllSabotages() + { + if (ShipStatus.Instance == null) return; + try + { + reactorSab = true; + oxygenSab = true; + commsSab = true; + elecSab = true; + + ToggleReactor(true); + ToggleO2(true); + ToggleComms(true); + ToggleLights(true); + + ShowNotification("[SABOTAGE] Все системы саботированы!"); + } + catch (Exception ex) { Debug.Log("Trigger All Sabotages Error: " + ex.Message); } + } + private void FixAllSabotages() + { + if (ShipStatus.Instance == null) return; + try + { + reactorSab = false; + oxygenSab = false; + commsSab = false; + elecSab = false; + + ToggleReactor(false); + ToggleO2(false); + ToggleComms(false); + ToggleLights(false); + + if (ShipStatus.Instance.AllDoors != null) + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null) + { + door.SetDoorway(true); + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(door.Id | 64)); } catch { } + } + } + } + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.MushroomMixupSabotage, 0); } catch { } + ShowNotification("[SABOTAGE] Все саботажи и двери починены!"); + } + catch (Exception ex) { Debug.Log("Fix All Sabotages Error: " + ex.Message); } + } + + private void SabotageDoors() + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + var rooms = new System.Collections.Generic.HashSet(); + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null) + { + rooms.Add(door.Room); + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); } catch { } + } + } + foreach (var room in rooms) + { + try { ShipStatus.Instance.RpcCloseDoorsOfType(room); } catch { } + } + ShowNotification("[DOORS] Сигнал на закрытие отправлен!"); + } + catch { } + } + + + private void CloseDoorsOfType(SystemTypes room) + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + ShipStatus.Instance.RpcCloseDoorsOfType(room); + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null && door.Room == room) + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); + } + ShowNotification($"[DOORS] {room}: close sent"); + } + catch { } + } + + private void LockDoorsOfType(SystemTypes room) + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null && door.Room == room) + { + door.SetDoorway(false); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); + } + } + ShipStatus.Instance.RpcCloseDoorsOfType(room); + ShowNotification($"[DOORS] {room}: locked"); + } + catch { } + } + + private void OpenDoorsOfType(SystemTypes room) + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null && door.Room == room) + { + door.SetDoorway(true); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(door.Id | 64)); + } + } + ShowNotification($"[DOORS] {room}: opened"); + } + catch { } + } + + private void LockAllDoors() + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + var rooms = new System.Collections.Generic.HashSet(); + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null) + { + door.SetDoorway(false); + rooms.Add(door.Room); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); + } + } + foreach (var room in rooms) + ShipStatus.Instance.RpcCloseDoorsOfType(room); + + ShowNotification("[DOORS] Все двери залочены!"); + } + catch { } + } + private void OpenAllDoors() + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null) + { + door.SetDoorway(true); + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(door.Id | 64)); } catch { } + } + } + ShowNotification("[DOORS] Все двери открыты!"); + } + catch { } + } + + private void ToggleReactor(bool state) { if (ShipStatus.Instance == null) return; byte flag = (byte)(state ? 128 : 16); try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Reactor, flag); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Laboratory, flag); if (state) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)128); else { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)16); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)17); } } catch { } } + private void ToggleO2(bool state) { if (ShipStatus.Instance == null) return; try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.LifeSupp, (byte)(state ? 128 : 16)); } catch { } } + private void ToggleComms(bool state) { if (ShipStatus.Instance == null) return; try { if (state) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)128); else { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)16); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)17); } } catch { } } + private void ToggleLights(bool state) + { + if (ShipStatus.Instance == null) return; + try + { + if (state) + { + byte b = 4; + for (int i = 0; i < 5; i++) if (UnityEngine.Random.value > 0.5f) b |= (byte)(1 << i); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, (byte)(b | 128)); + } + else + { + var sys = ShipStatus.Instance.Systems[SystemTypes.Electrical].Cast(); + if (sys != null) + { + for (int i = 0; i < 5; i++) + { + bool expected = (sys.ExpectedSwitches & (1 << i)) != 0; + bool actual = (sys.ActualSwitches & (1 << i)) != 0; + if (expected != actual) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, (byte)i); + } + } + } + } + catch { } + } + private void SabotageMushroom() { if (ShipStatus.Instance == null) return; try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.MushroomMixupSabotage, (byte)1); } catch { } } + + private void DrawPlayersRoles() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("PRE-GAME ROLE MANAGER", headerStyle); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(enablePreGameRoleForce ? "Role Forcing: ON" : "Role Forcing: OFF", enablePreGameRoleForce ? activeTabStyle : btnStyle, GUILayout.Height(25))) enablePreGameRoleForce = !enablePreGameRoleForce; + if (GUILayout.Button("Random 2 Imps", btnStyle, GUILayout.Width(110), GUILayout.Height(25))) + { + forcedPreGameRoles.Clear(); forcedImpostors.Clear(); + var activePlayers = PlayerControl.AllPlayerControls.ToArray().Where(p => p != null && !p.Data.Disconnected).ToList(); + if (activePlayers.Count >= 2) + { + for (int i = activePlayers.Count - 1; i > 0; i--) { int swapIndex = UnityEngine.Random.Range(0, i + 1); var temp = activePlayers[i]; activePlayers[i] = activePlayers[swapIndex]; activePlayers[swapIndex] = temp; } + forcedImpostors.Add(activePlayers[0].PlayerId); forcedImpostors.Add(activePlayers[1].PlayerId); + enablePreGameRoleForce = true; + } + } + if (GUILayout.Button("Clear All Roles", btnStyle, GUILayout.Width(110), GUILayout.Height(25))) { forcedPreGameRoles.Clear(); forcedImpostors.Clear(); } + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(8); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("LIVE ROLE DISTRIBUTOR (HOST)", headerStyle); + GUILayout.BeginHorizontal(); + + GUIStyle allRoleMidStyle = new GUIStyle(btnStyle) + { + fontStyle = FontStyle.Bold, + normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, + alignment = TextAnchor.MiddleCenter + }; + + if (GUILayout.Button("<", btnStyle, GUILayout.Width(28), GUILayout.Height(25))) + { + allPlayersRoleAssignIdx--; + if (allPlayersRoleAssignIdx < 0) allPlayersRoleAssignIdx = roleAssignOptions.Length - 1; + } + + GUILayout.Label(roleAssignNames[allPlayersRoleAssignIdx], allRoleMidStyle, GUILayout.Height(25), GUILayout.ExpandWidth(true)); + + if (GUILayout.Button(">", btnStyle, GUILayout.Width(28), GUILayout.Height(25))) + { + allPlayersRoleAssignIdx++; + if (allPlayersRoleAssignIdx >= roleAssignOptions.Length) allPlayersRoleAssignIdx = 0; + } + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + if (GUILayout.Button("SET ALL PLAYERS ROLE", activeTabStyle, GUILayout.Height(28))) + SetAllPlayersRole(roleAssignOptions[allPlayersRoleAssignIdx]); + GUILayout.EndVertical(); + + GUILayout.Space(10); + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(boxStyle, GUILayout.Width(200)); + preRolesListScrollPos = GUILayout.BeginScrollView(preRolesListScrollPos); + foreach (var pc in lockedPlayersList) + { + if (pc == null || pc.Data == null || pc.PlayerId >= 100) continue; + string pName = pc.Data.PlayerName ?? "Unknown"; + if (forcedPreGameRoles.ContainsKey(pc.PlayerId)) { string rShort = forcedPreGameRoles[pc.PlayerId].ToString().Replace("9", "Pha").Replace("10", "Tra").Replace("8", "Noi").Replace("12", "Det").Replace("18", "Vip"); if (rShort.Length > 3) rShort = rShort.Substring(0, 3); pName += $" [{rShort}]"; } + else if (forcedImpostors.Contains(pc.PlayerId)) pName += " [Imp]"; + bool isSelected = selectedPreRoleId == pc.PlayerId; + try { GUI.contentColor = Palette.PlayerColors[pc.Data.DefaultOutfit.ColorId]; } catch { } + if (GUILayout.Button(pName, isSelected ? activeTabStyle : btnStyle, GUILayout.Height(30))) selectedPreRoleId = pc.PlayerId; + GUI.contentColor = Color.white; + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + + GUILayout.BeginVertical(boxStyle, GUILayout.ExpandWidth(true)); + preRolesActionScrollPos = GUILayout.BeginScrollView(preRolesActionScrollPos); + PlayerControl target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedPreRoleId); + if (target != null && target.Data != null) + { + GUIStyle infoStyle = new GUIStyle(GUI.skin.label) { richText = true, fontSize = 14 }; + GUILayout.Label($"Selecting role for: {target.Data.PlayerName}", infoStyle); + RoleTypes currentForced = forcedPreGameRoles.ContainsKey(target.PlayerId) ? forcedPreGameRoles[target.PlayerId] : RoleTypes.Crewmate; + bool isForced = forcedPreGameRoles.ContainsKey(target.PlayerId) || forcedImpostors.Contains(target.PlayerId); + string roleNameStr = currentForced.ToString().Replace("9", "Phantom").Replace("10", "Tracker").Replace("8", "Noisemaker").Replace("12", "Detective").Replace("18", "Viper"); + if (forcedImpostors.Contains(target.PlayerId)) roleNameStr = "Impostor"; + GUILayout.Label($"Status: {(isForced ? $"Forced ({roleNameStr})" : "Not Forced (Random)")}", infoStyle); + GUILayout.Space(15); + GUILayout.Label("IMPOSTOR ROLES (Red Team)", headerStyle); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Impostor", btnStyle, GUILayout.Height(30))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Add(target.PlayerId); } + if (GUILayout.Button("Shapeshifter", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Shapeshifter; } + if (GUILayout.Button("Phantom", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)9; } + if (GUILayout.Button("Viper", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)18; } + GUILayout.EndHorizontal(); + GUILayout.Space(10); + GUILayout.Label("CREWMATE ROLES (Blue Team)", headerStyle); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Crewmate", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Crewmate; } + if (GUILayout.Button("Engineer", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Engineer; } + if (GUILayout.Button("Scientist", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Scientist; } + if (GUILayout.Button("Tracker", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)10; } + GUILayout.EndHorizontal(); + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Noisemaker", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)8; } + if (GUILayout.Button("Guardian Angel", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.GuardianAngel; } + if (GUILayout.Button("Detective", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)12; } + GUILayout.EndHorizontal(); + GUILayout.Space(15); + if (GUILayout.Button("REMOVE FORCED ROLE", activeTabStyle, GUILayout.Height(35))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Remove(target.PlayerId); } + GUILayout.Space(20); + GUILayout.Label("Hide & Seek Notice:\nВыбор Impostor/Shapeshifter/Phantom/Viper расширит лимит маньяков (Seekers) в Прятках!", new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true }); + } + else + { + GUILayout.FlexibleSpace(); + GUILayout.Label("Select a player to set their role", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); + GUILayout.FlexibleSpace(); + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + + private void DrawMenuTab() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("MENU CUSTOMIZATION", headerStyle); + GUILayout.Space(5); + + bool prevRgb = rgbMenuMode; + rgbMenuMode = DrawToggle(rgbMenuMode, "RGB Menu Mode"); + if (prevRgb && !rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); + + GUILayout.Space(5); + + bool prevWhiteTheme = whiteMenuTheme; + whiteMenuTheme = DrawToggle(whiteMenuTheme, "White Theme"); + if (prevWhiteTheme != whiteMenuTheme) + { + InitStyles(); + UpdateAccentColor(currentAccentColor); + SaveConfig(); + } + + GUILayout.Space(5); + + bool prevBg = enableBackground; + enableBackground = DrawToggle(enableBackground, "Enable Image Background"); + if (enableBackground && !prevBg) LoadBackgroundImage(); + + GUILayout.Space(5); + GUILayout.Label("Put 'MenuBG.png' or .jpg in BepInEx/config to add a background image.", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11 }); + + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + GUIStyle middleColorStyle = new GUIStyle(btnStyle) { normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, fontStyle = FontStyle.Bold }; + GUI.enabled = !rgbMenuMode; + if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex--; if (currentMenuColorIndex < 0) currentMenuColorIndex = menuColors.Length - 1; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); } + GUILayout.Label(menuColorNames[currentMenuColorIndex], middleColorStyle, GUILayout.Width(110), GUILayout.Height(25)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex++; if (currentMenuColorIndex >= menuColors.Length) currentMenuColorIndex = 0; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); } + GUI.enabled = true; + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("SPOOF MENU IDENTITY", headerStyle); + SpoofMenuEnabled = DrawToggle(SpoofMenuEnabled, "Enable Fake RPC"); + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) } }; + if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex--; if (selectedSpoofMenuIndex < 0) selectedSpoofMenuIndex = spoofMenuNames.Length - 1; } + GUILayout.Label($"{spoofMenuNames[selectedSpoofMenuIndex]}", middleLabelStyle, GUILayout.Width(110), GUILayout.Height(25)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex++; if (selectedSpoofMenuIndex >= spoofMenuNames.Length) selectedSpoofMenuIndex = 0; } + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("NOTIFICATIONS & LOGGING", headerStyle); + GUILayout.Space(5); + EnableCustomNotifs = DrawToggle(EnableCustomNotifs, "Enable Custom UI Notifications", 250); + GUILayout.Space(5); + LogAllRPCs = DrawToggle(LogAllRPCs, "Sniff All RPCs (On-Screen)", 250); + GUILayout.EndVertical(); + } + private Vector2 outfitsScrollPos = Vector2.zero; + public static bool AutoHostEnabled = false; + public static bool AutoReturnLobbyAfterMatch = true; + public static bool AutoHostNotifications = true; + public static bool AutoHostForceLastMinute = true; + public static bool AutoHostWaitLoadedPlayers = true; + public static bool AutoHostCancelBelowMin = true; + public static bool AutoHostInstantStart = false; + + public static int AutoHostMinPlayers = 4; + public static int AutoHostForceMinPlayers = 2; + public static float AutoHostStartDelaySeconds = 15f; + public static float AutoHostBackoffSeconds = 8f; + public static float AutoHostWarmupSeconds = 5f; + public static float AutoHostLoadGraceSeconds = 20f; + + public static int AutoHostForceAfterMinutes = 0; + public static int AutoHostFastStartPlayers = 13; + public static float AutoHostFastStartDelaySeconds = 5f; + + private int currentAutoHostSubTab = 0; + private string[] autoHostSubTabs = { "LOBBY CONTROLS", "ROLE MANAGER", "ANTI CHEAT", "AUTO HOST" }; + private void DrawOutfitsTab() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("COPY SPECIFIC PLAYER", headerStyle); + + outfitsScrollPos = GUILayout.BeginScrollView(outfitsScrollPos); + if (lockedPlayersList.Count > 0) + { + foreach (var pc in lockedPlayersList) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null) continue; + + GUILayout.BeginHorizontal(boxStyle); + try + { + string pName = pc.Data.PlayerName ?? "Unknown"; + GUILayout.Label(pName, GUILayout.Width(150)); + + if (GUILayout.Button("Copy Outfit", btnStyle, GUILayout.Height(25))) + { + try + { + PlayerControl.LocalPlayer.RpcSetSkin(pc.Data.DefaultOutfit.SkinId); + PlayerControl.LocalPlayer.RpcSetHat(pc.Data.DefaultOutfit.HatId); + PlayerControl.LocalPlayer.RpcSetVisor(pc.Data.DefaultOutfit.VisorId); + PlayerControl.LocalPlayer.RpcSetNamePlate(pc.Data.DefaultOutfit.NamePlateId); + PlayerControl.LocalPlayer.RpcSetPet(pc.Data.DefaultOutfit.PetId); + } + catch { } + } + } + finally { GUILayout.EndHorizontal(); } + GUILayout.Space(2); + } + } + else + { + GUILayout.Label("Нет игроков для копирования."); + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + } + public static bool removePenalty = true; + public static bool alwaysShowLobbyTimer = false; + public static bool enableChatLog = true; + public static bool enableFastChat = true; + public static bool allowLinksAndSymbols = false; + + private static readonly System.Collections.Generic.Dictionary CachedSprites = new(); + + public static Sprite LoadEmbeddedSprite(string fileName, float pixelsPerUnit = 1f) + { + string path = $"ElysiumModMenu.{fileName}"; + + try + { + if (CachedSprites.TryGetValue(path + pixelsPerUnit, out var cachedSprite)) + return cachedSprite; + + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream(path); + + if (stream == null) + { + System.Console.WriteLine($"[ELYSIUM] Стрим равен null! Убедись, что {fileName} установлен как Embedded Resource."); + return null; + } + + var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); + using System.IO.MemoryStream ms = new System.IO.MemoryStream(); + stream.CopyTo(ms); + + ImageConversion.LoadImage(texture, ms.ToArray(), false); + + Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); + + sprite.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontSaveInEditor; + + return CachedSprites[path + pixelsPerUnit] = sprite; + } + catch (System.Exception ex) + { + System.Console.WriteLine($"[ELYSIUM] Ошибка загрузки спрайта {fileName}: " + ex.Message); + return null; + } + } + public void Start() + { + if (enableBackground) LoadBackgroundImage(); + UnlockCosmetics(); + LoadConfig(); + LoadBanList(); + + + try + { + int starts = UnityEngine.PlayerPrefs.GetInt("Elysium_GameStarts", 0); + starts++; + + string chatLogPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ChatLog.txt"); + + if (starts >= 3) + { + if (System.IO.File.Exists(chatLogPath)) + { + System.IO.File.WriteAllText(chatLogPath, string.Empty); + } + starts = 0; + } + + UnityEngine.PlayerPrefs.SetInt("Elysium_GameStarts", starts); + UnityEngine.PlayerPrefs.Save(); + } + catch { } + } + + public void OnApplicationQuit() + { + SaveConfig(); + } + + public void OnDisable() + { + SaveConfig(); + } + + public static KeyCode bindMagnetCursor = KeyCode.F9; + public static bool isWaitBindMagnetCursor = false; + public void Update() + { + bool isTypingOrBinding = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingBan || customChatInputFocused || + isWaitingForBind || isWaitBindMassMorph || isWaitBindSpawnLobby || + isWaitBindDespawnLobby || isWaitBindCloseMeeting || isWaitBindInstaStart || + isWaitBindEndCrew || isWaitBindEndImp || isWaitBindEndImpDC || isWaitBindEndHnsDC || + isWaitBindMagnetCursor; + + if (!isTypingOrBinding && Input.GetKeyDown(KeyCode.Insert)) + { + showMenu = !showMenu; + if (!showMenu) SaveConfig(); + } + + if (!isTypingOrBinding) + { + if (bindMassMorph != KeyCode.None && Input.GetKeyDown(bindMassMorph)) + this.StartCoroutine(MassMorphCoroutine().WrapToIl2Cpp()); + + if (bindSpawnLobby != KeyCode.None && Input.GetKeyDown(bindSpawnLobby)) + SpawnLobby(); + + if (bindDespawnLobby != KeyCode.None && Input.GetKeyDown(bindDespawnLobby)) + DespawnLobby(); + + if (bindCloseMeeting != KeyCode.None && Input.GetKeyDown(bindCloseMeeting) && MeetingHud.Instance != null) + MeetingHud.Instance.RpcClose(); + + if (bindInstaStart != KeyCode.None && Input.GetKeyDown(bindInstaStart) && GameStartManager.Instance != null) + { + GameStartManager.Instance.startState = GameStartManager.StartingStates.Countdown; + GameStartManager.Instance.countDownTimer = 0f; + } + if (bindMagnetCursor != KeyCode.None && Input.GetKeyDown(bindMagnetCursor)) + { + autoFollowCursor = !autoFollowCursor; + ShowNotification(autoFollowCursor ? + "[MAGNET] Magnet Cursor: ON" : + "[MAGNET] Magnet Cursor: OFF"); + } + if (bindEndCrew != KeyCode.None && Input.GetKeyDown(bindEndCrew)) SmartEndGame("CrewWin"); + if (bindEndImp != KeyCode.None && Input.GetKeyDown(bindEndImp)) SmartEndGame("ImpWin"); + if (bindEndImpDC != KeyCode.None && Input.GetKeyDown(bindEndImpDC)) SmartEndGame("ImpDisconnect"); + if (bindEndHnsDC != KeyCode.None && Input.GetKeyDown(bindEndHnsDC)) SmartEndGame("HnsImpDisconnect"); + } + + ElysiumAutoHostService.Tick(); + ElysiumAutoLobbyReturn.UpdateLogic(); + if (votekickEveryone) + { + TickVotekickEveryoneRun(); + } + if (stylesInited && rgbMenuMode) + { + rgbMenuHue += Time.deltaTime * 0.2f; + if (rgbMenuHue > 1f) rgbMenuHue -= 1f; + UpdateAccentColor(Color.HSVToRGB(rgbMenuHue, 1f, 1f)); + } + + if (wasShowMenu && !showMenu) SaveConfig(); + wasShowMenu = showMenu; + + if (PlayerControl.LocalPlayer != null) + { + TryHostOnlyKillAuraTick(); + TryAutoBanBrokenFriendCodeTick(); + + if (enableLocalNameSpoof && !isEditingName) + { + localNameRefreshTimer += Time.deltaTime; + if (localNameRefreshTimer >= 0.5f) + { + localNameRefreshTimer = 0f; + ApplyLocalNameSelf(customNameInput, false); + } + } + else + { + localNameRefreshTimer = 0f; + } + + if (enableLocalFriendCodeSpoof && !isEditingLocalFriendCode) + { + localFriendCodeRefreshTimer += Time.deltaTime; + if (localFriendCodeRefreshTimer >= 0.5f) + { + localFriendCodeRefreshTimer = 0f; + ApplyLocalFriendCodeSelf(localFriendCodeInput, false); + } + } + else + { + localFriendCodeRefreshTimer = 0f; + } + + if ((tpToCursor && Input.GetMouseButtonDown(1)) || + (dragToCursor && Input.GetMouseButton(2)) || + autoFollowCursor) + { + if (Camera.main != null) + { + Vector3 mPos = Camera.main.ScreenToWorldPoint(Input.mousePosition); + mPos.z = PlayerControl.LocalPlayer.transform.position.z; + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(mPos); + } + } + try + { + if (noTaskMode && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + if (GameOptionsManager.Instance != null && GameOptionsManager.Instance.CurrentGameOptions != null) + { + var opts = GameOptionsManager.Instance.CurrentGameOptions; + opts.SetInt(Int32OptionNames.NumCommonTasks, 0); + opts.SetInt(Int32OptionNames.NumLongTasks, 0); + opts.SetInt(Int32OptionNames.NumShortTasks, 0); + } + } + } + catch { } + if (autoChatEveryone && pendingAutoMeeting && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + try + { + if (PlayerControl.LocalPlayer != null && ShipStatus.Instance != null && !PlayerControl.LocalPlayer.Data.IsDead) + { + autoMeetingTimer += Time.deltaTime; + + if (autoMeetingTimer >= autoChatEveryoneDelay) + { + if (MeetingHud.Instance == null) + { + PlayerControl.LocalPlayer.CmdReportDeadBody(null); + } + else + { + MeetingHud.Instance.RpcClose(); + pendingAutoMeeting = false; + autoMeetingTimer = 0f; + ShowNotification("[CHAT EVERYONE] Игроки собраны в кафетерии!"); + } + } + } + } + catch { } + } + + if (customChatSpamEnabled) + { + customChatSpamTimer += Time.deltaTime; + if (customChatSpamTimer >= customChatSpamDelay) + { + customChatSpamTimer = 0f; + TrySendCustomChatMessage(customChatMessage); + } + } + else + { + customChatSpamTimer = 0f; + } + if (autoKickBugs && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && fortegreenTimer.Count > 0) + { + foreach (var kvp in fortegreenTimer.ToList()) + { + if (Time.time >= kvp.Value) + { + byte pid = kvp.Key; + var player = GameData.Instance.GetPlayerById(pid); + + if (player != null && !player.Disconnected && player.Object != null) + { + int currentColor = (int)player.DefaultOutfit.ColorId; + if (currentColor == 18 || currentColor >= Palette.PlayerColors.Length) + { + AmongUsClient.Instance.KickPlayer(player.ClientId, false); + ShowNotification($"[AUTO-KICK] Игрок {player.PlayerName} кикнут (Баг цвета)!"); + } + } + fortegreenTimer.Remove(pid); + } + } + } + if (PlayerControl.LocalPlayer != null) + { + try + { + if (AnimAsteroidsEnabled) + { + PlayerControl.LocalPlayer.PlayAnimation((byte)TaskTypes.ClearAsteroids); + RpcPlayAnimationMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, (byte)TaskTypes.ClearAsteroids); + AmongUsClient.Instance.LateBroadcastUnreliableMessage(Unsafe.As(rpcMessage)); + } + + if (AnimShieldsEnabled) + { + PlayerControl.LocalPlayer.PlayAnimation((byte)TaskTypes.PrimeShields); + RpcPlayAnimationMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, (byte)TaskTypes.PrimeShields); + AmongUsClient.Instance.LateBroadcastUnreliableMessage(Unsafe.As(rpcMessage)); + } + + if (IsScanning && !isScannerActiveFlag) + { + var count = ++PlayerControl.LocalPlayer.scannerCount; + PlayerControl.LocalPlayer.SetScanner(true, count); + RpcSetScannerMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, true, count); + AmongUsClient.Instance.LateBroadcastReliableMessage(Unsafe.As(rpcMessage)); + isScannerActiveFlag = true; + } + else if (!IsScanning && isScannerActiveFlag) + { + var count = ++PlayerControl.LocalPlayer.scannerCount; + PlayerControl.LocalPlayer.SetScanner(false, count); + RpcSetScannerMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, false, count); + AmongUsClient.Instance.LateBroadcastReliableMessage(Unsafe.As(rpcMessage)); + isScannerActiveFlag = false; + } + + if (ShipStatus.Instance != null) + { + if (AnimCamsInUseEnabled && !isCamsActiveFlag) + { + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Security, 1); + isCamsActiveFlag = true; + } + else if (!AnimCamsInUseEnabled && isCamsActiveFlag) + { + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Security, 0); + isCamsActiveFlag = false; + } + } + } + catch { } + } + try + { + if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.MyPhysics != null && PlayerControl.LocalPlayer.Data != null) + { + if (PlayerControl.LocalPlayer.Collider != null) + { + PlayerControl.LocalPlayer.Collider.enabled = !(noClip || PlayerControl.LocalPlayer.onLadder); + } + + float baseSpeed = 3f; + float targetSpeed = walkSpeed * baseSpeed; + + if (PlayerControl.LocalPlayer.Data.IsDead) + { + PlayerControl.LocalPlayer.MyPhysics.GhostSpeed = targetSpeed; + } + else + { + PlayerControl.LocalPlayer.MyPhysics.Speed = targetSpeed; + } + } + } + catch { } + + if (SpoofMenuEnabled && PlayerControl.LocalPlayer != null) + { + uiSpoofTimer += Time.deltaTime; + if (uiSpoofTimer >= rpcSpoofDelay) + { + uiSpoofTimer = 0f; + byte rpc = spoofMenuRPCs[selectedSpoofMenuIndex]; + try + { + MessageWriter msg = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, rpc, SendOption.None, -1); + AmongUsClient.Instance.FinishRpcImmediately(msg); + } + catch { } + } + } + try + { + if (autoBanEnabled && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) + { + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.Data == null || pc.Data.Disconnected || pc == PlayerControl.LocalPlayer) continue; + + string fc = pc.Data.FriendCode; + if (!string.IsNullOrEmpty(fc)) + { + foreach (var entry in bannedEntries) + { + string[] parts = entry.Split('|'); + if (parts.Length > 0 && parts[0].Trim().ToLower() == fc.Trim().ToLower()) + { + AmongUsClient.Instance.KickPlayer(pc.OwnerId, true); + break; + } + } + } + } + } + } + catch { } + if (freecam) + { + if (!_freecamActive && Camera.main != null) + { + var cam = Camera.main.gameObject.GetComponent(); + if (cam != null) { cam.enabled = false; cam.Target = null; } + _freecamActive = true; + } + if (PlayerControl.LocalPlayer != null) PlayerControl.LocalPlayer.moveable = false; + Vector3 movement = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0.0f); + if (Camera.main != null) Camera.main.transform.position += movement * 15f * Time.deltaTime; + } + else if (_freecamActive) + { + if (PlayerControl.LocalPlayer != null) PlayerControl.LocalPlayer.moveable = true; + if (Camera.main != null) + { + var cam = Camera.main.gameObject.GetComponent(); + if (cam != null && PlayerControl.LocalPlayer != null) { cam.enabled = true; cam.SetTarget(PlayerControl.LocalPlayer); } + } + _freecamActive = false; + } + + try + { + if (cameraZoom && Camera.main != null && Input.GetAxis("Mouse ScrollWheel") != 0f) + { + if (Input.GetAxis("Mouse ScrollWheel") < 0f) Camera.main.orthographicSize += 0.5f; + else if (Input.GetAxis("Mouse ScrollWheel") > 0f && Camera.main.orthographicSize > 3f) Camera.main.orthographicSize -= 0.5f; + } + } + catch { } + + try + { + if (rainbowPlayers.Count > 0 && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) + { + colorTimer += Time.deltaTime; + if (colorTimer > 0.15f) + { + colorTimer = 0f; + currentColorId++; + if (currentColorId > 17) currentColorId = 0; + foreach (var p in PlayerControl.AllPlayerControls) + if (p != null && p.Data != null && !p.Data.Disconnected && rainbowPlayers.Contains(p.PlayerId)) + p.RpcSetColor(currentColorId); + } + } + } + catch { } + try + { + if (PlayerControl.AllPlayerControls != null) + { + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null) HandleTracer(pc, showTracers); + } + } + } + catch { } + + + + if (!isEditingLevel && uint.TryParse(spoofLevelString, out uint parsedLvl)) + { + uint targetLevel = parsedLvl > 0 ? parsedLvl - 1 : 0; + try + { + if (AmongUs.Data.DataManager.Player.stats.level != targetLevel) + { + AmongUs.Data.DataManager.Player.stats.level = targetLevel; + } + } + catch + { + try + { + if (AmongUs.Data.DataManager.Player.Stats.Level != targetLevel) + { + AmongUs.Data.DataManager.Player.Stats.Level = targetLevel; + } + } + catch { } + } + } + try + { + if (localRainbow || rainbowPlayers.Count > 0) + { + colorTimer += Time.deltaTime; + if (colorTimer > 0.15f) + { + colorTimer = 0f; + currentColorId++; + if (currentColorId > 17) currentColorId = 0; + + if (localRainbow && PlayerControl.LocalPlayer != null) + PlayerControl.LocalPlayer.CmdCheckColor(currentColorId); + + if (rainbowPlayers.Count > 0 && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) + { + foreach (var p in PlayerControl.AllPlayerControls) + { + if (p != null && p.Data != null && !p.Data.Disconnected && rainbowPlayers.Contains(p.PlayerId)) + p.RpcSetColor(currentColorId); + } + } + } + } + } + + + catch { } + + + } + } + public static void ForceSetScanner(PlayerControl player, bool toggle) + { + var count = ++player.scannerCount; + player.SetScanner(toggle, count); + RpcSetScannerMessage rpcMessage = new(player.NetId, toggle, count); + AmongUsClient.Instance.LateBroadcastReliableMessage(Unsafe.As(rpcMessage)); + } + public static void ForcePlayAnimation(byte animationType) + { + PlayerControl.LocalPlayer.PlayAnimation(animationType); + RpcPlayAnimationMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, animationType); + AmongUsClient.Instance.LateBroadcastUnreliableMessage(Unsafe.As(rpcMessage)); + } + + public void OnGUI() + { + Event e = Event.current; + + bool isTyping = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingBan; + bool isBinding = isWaitingForBind || isWaitBindMassMorph || isWaitBindSpawnLobby || isWaitBindDespawnLobby || + isWaitBindCloseMeeting || isWaitBindInstaStart || isWaitBindEndCrew || isWaitBindEndImp || + isWaitBindEndImpDC || isWaitBindEndHnsDC || isWaitBindMagnetCursor; + + if (e != null && e.isKey && e.type == EventType.KeyDown) + { + if (e.keyCode == KeyCode.Escape) + { + isEditingName = isEditingLevel = isEditingFriendCode = isEditingLocalFriendCode = isEditingBan = false; + ResetAllBindWaits(); + e.Use(); + } + else if (isBinding && e.keyCode != KeyCode.None) + { + if (isWaitingForBind) { menuToggleKey = e.keyCode; } + else if (isWaitBindMassMorph) { bindMassMorph = e.keyCode; } + else if (isWaitBindSpawnLobby) { bindSpawnLobby = e.keyCode; } + else if (isWaitBindDespawnLobby) { bindDespawnLobby = e.keyCode; } + else if (isWaitBindCloseMeeting) { bindCloseMeeting = e.keyCode; } + else if (isWaitBindInstaStart) { bindInstaStart = e.keyCode; } + else if (isWaitBindEndCrew) { bindEndCrew = e.keyCode; } + else if (isWaitBindEndImp) { bindEndImp = e.keyCode; } + else if (isWaitBindEndImpDC) { bindEndImpDC = e.keyCode; } + else if (isWaitBindEndHnsDC) { bindEndHnsDC = e.keyCode; } + else if (isWaitBindMagnetCursor) { bindMagnetCursor = e.keyCode; } + + ResetAllBindWaits(); + SaveConfig(); + e.Use(); + } + else if (isTyping) + { + if (isEditingBan && HandleClipboardShortcut(e, ref banInput)) { } + else if (isEditingName && HandleClipboardShortcut(e, ref customNameInput)) { } + else if (isEditingLevel && HandleClipboardShortcut(e, ref spoofLevelString)) { } + else if (isEditingFriendCode && HandleClipboardShortcut(e, ref spoofFriendCodeInput)) { } + else if (isEditingLocalFriendCode && HandleClipboardShortcut(e, ref localFriendCodeInput)) { } + else if (e.keyCode == KeyCode.Backspace) + { + if (isEditingBan && banInput.Length > 0) { banInput = banInput.Substring(0, banInput.Length - 1); } + if (isEditingName && customNameInput.Length > 0) { customNameInput = customNameInput.Substring(0, customNameInput.Length - 1); } + if (isEditingLevel && spoofLevelString.Length > 0) { spoofLevelString = spoofLevelString.Substring(0, spoofLevelString.Length - 1); } + if (isEditingFriendCode && spoofFriendCodeInput.Length > 0) { spoofFriendCodeInput = spoofFriendCodeInput.Substring(0, spoofFriendCodeInput.Length - 1); } + if (isEditingLocalFriendCode && localFriendCodeInput.Length > 0) { localFriendCodeInput = localFriendCodeInput.Substring(0, localFriendCodeInput.Length - 1); } + e.Use(); + } + else if (e.character != 0 && e.character != '\n' && e.character != '\r') + { + if (isEditingBan) { banInput += e.character; } + if (isEditingName) { customNameInput += e.character; } + if (isEditingLevel) { spoofLevelString += e.character; } + if (isEditingFriendCode) { spoofFriendCodeInput += e.character; } + if (isEditingLocalFriendCode) { localFriendCodeInput += e.character; } + e.Use(); + } + } + } + + if (Event.current.type == EventType.Layout) + { + lockedPlayersList.Clear(); + if (PlayerControl.AllPlayerControls != null) + { + foreach (var p in PlayerControl.AllPlayerControls) + { + if (p != null && p.Data != null && !p.Data.Disconnected && p.PlayerId < 100) + lockedPlayersList.Add(p); + } + } + + if (!stylesInited) InitStyles(); + + if (showMenu) + { + windowRect = GUI.Window(0, windowRect, (Action)DrawElysiumModMenu, "", windowStyle); + } + + for (int i = screenNotifications.Count - 1; i >= 0; i--) + { + screenNotifications[i].lifetime += Time.deltaTime; + if (screenNotifications[i].HasExpired) screenNotifications.RemoveAt(i); + } + } + + try + { + if (AmongUsClient.Instance != null && (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Joined || AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started)) + { + if (PlayerControl.AllPlayerControls != null) + { + List currentIds = new List(); + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null) + { + currentIds.Add(pc.PlayerId); + UpsertPlayerHistory(pc); + } + } + + foreach (var id in currentIds) + { + if (!lastPlayerIds.Contains(id) && !pendingJoinTimers.ContainsKey(id)) + { + pendingJoinTimers[id] = 1.5f; + } + } + + var keysToProcess = pendingJoinTimers.Keys.ToList(); + foreach (var id in keysToProcess) + { + pendingJoinTimers[id] -= Time.deltaTime; + if (pendingJoinTimers[id] <= 0f) + { + pendingJoinTimers.Remove(id); + + var pc = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p != null && p.PlayerId == id); + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + if (DetailedJoinInfo) + { + int level = 1; + try + { + uint rawLevel = pc.Data.PlayerLevel; + if (rawLevel != uint.MaxValue && rawLevel < 10000) level = (int)rawLevel + 1; + } + catch { } + + string platform = GetPlatform(AmongUsClient.Instance.GetClientFromCharacter(pc)); + string fc = GetDisplayedFriendCode(pc.Data); + + ShowNotification($"[+] {pc.Data.PlayerName} joined\nLvl: {level} | {platform} | FC: {fc}"); + } + else + { + ShowNotification($"[+] {pc.Data.PlayerName} присоединился"); + } + } + } + } + + foreach (var id in lastPlayerIds) + { + if (!currentIds.Contains(id)) pendingJoinTimers.Remove(id); + } + + lastPlayerIds = new List(currentIds); + } + } + else + { + lastPlayerIds.Clear(); + pendingJoinTimers.Clear(); + } + } + catch { } + if (screenNotifications.Count > 0) + { + int maxNotifs = 6; + int startIdx = Mathf.Max(0, screenNotifications.Count - maxNotifs); + for (int i = startIdx; i < screenNotifications.Count; i++) + { + ElysiumNotification notif = screenNotifications[i]; + int reverseIndex = screenNotifications.Count - 1 - i; + + float slideOffset = 0f; + float animSpeed = 0.3f; + float currentAlpha = 0.95f; + + if (notif.lifetime < animSpeed) + { + float t = Mathf.Clamp01(1f - (notif.lifetime / animSpeed)); + slideOffset = t * t * 300f; + } + else if (notif.lifetime > notif.ttl - animSpeed) + { + float t = Mathf.Clamp01((notif.lifetime - (notif.ttl - animSpeed)) / animSpeed); + slideOffset = t * t * 300f; + currentAlpha = Mathf.Lerp(0.95f, 0f, t); + } + + float xPos = (float)Screen.width - notificationBoxSize.x - 15f + slideOffset; + float yPos = Screen.height - 150f - (reverseIndex * (notificationBoxSize.y + 5f)); + + GUI.color = new Color(0.12f, 0.12f, 0.12f, currentAlpha); + GUI.Box(new Rect(xPos, yPos, notificationBoxSize.x, notificationBoxSize.y), "", windowStyle); + + GUI.color = new Color(1f, 1f, 1f, currentAlpha > 0.5f ? 1f : currentAlpha * 2f); + string accentHex = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); + + GUI.Label(new Rect(xPos + 10f, yPos + 5f, notificationBoxSize.x - 20f, 20f), $"{notif.title}"); + + float timeLeft = Mathf.Max(0, notif.ttl - notif.lifetime); + GUI.Label(new Rect(xPos + 10f, yPos + 5f, notificationBoxSize.x - 20f, 20f), $"{timeLeft:F1}s", new GUIStyle(GUI.skin.label) { alignment = TextAnchor.UpperRight, fontSize = 12, richText = true }); + GUI.Label(new Rect(xPos + 10f, yPos + 25f, notificationBoxSize.x - 20f, notificationBoxSize.y - 30f), notif.message, new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 12 }); + + float progress = 1f - (notif.lifetime / notif.ttl); + GUI.color = new Color(currentAccentColor.r, currentAccentColor.g, currentAccentColor.b, currentAlpha); + GUI.Box(new Rect(xPos + 8f, yPos + notificationBoxSize.y - 6f, (notificationBoxSize.x - 16f) * progress, 2f), "", safeLineStyle); + GUI.color = Color.white; + } + } + } + + public static bool votekickEveryone = false; + public static List votekickedPlayerIds = new List(); + private static bool votekickExitQueued = false; + private static float votekickExitAt = 0f; + private const float VotekickExitDelay = 0.45f; + private Vector2 votekickScrollPosition = Vector2.zero; + + private void StartVotekickEveryoneRun() + { + votekickEveryone = true; + votekickedPlayerIds.Clear(); + votekickExitQueued = false; + votekickExitAt = 0f; + ShowNotification("[AUTO-VOTEKICK] Armed. Join a room and votes will be sent automatically."); + } + + private void StopVotekickEveryoneRun(bool clearVotes = true) + { + votekickEveryone = false; + votekickExitQueued = false; + votekickExitAt = 0f; + if (clearVotes) votekickedPlayerIds.Clear(); + } + + private void TickVotekickEveryoneRun() + { + if (!votekickEveryone) return; + + if (votekickExitQueued) + { + if (Time.unscaledTime >= votekickExitAt) + LeaveRoomAfterVotekick(); + return; + } + + if (VoteBanSystem.Instance == null || PlayerControl.AllPlayerControls == null || AmongUsClient.Instance == null) + return; + + int sent = ExecuteVotekickEveryone(true); + if (sent <= 0) return; + + votekickExitQueued = true; + votekickExitAt = Time.unscaledTime + VotekickExitDelay; + ShowNotification($"[AUTO-VOTEKICK] Votes sent: {sent}. Leaving room..."); + } + + private int ExecuteVotekickEveryone(bool rememberTargets) + { + if (VoteBanSystem.Instance == null || PlayerControl.AllPlayerControls == null) return 0; + + int votesSent = 0; + try + { + foreach (PlayerControl pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.AmOwner || pc.Data == null || pc.Data.Disconnected) continue; + + int clientId = pc.Data.ClientId; + + if (!rememberTargets || !votekickedPlayerIds.Contains(clientId)) + { + for (int i = 0; i < 3; i++) + { + VoteBanSystem.Instance.CmdAddVote(clientId); + votesSent++; + } + + if (rememberTargets) + votekickedPlayerIds.Add(clientId); + + ShowNotification($"[VOTEKICK] 3 votes sent to {pc.Data.PlayerName}."); + System.Console.WriteLine($"[Votekick] Auto-votekicked {pc.Data.PlayerName}"); + } + } + } + catch (Exception ex) + { + System.Console.WriteLine("VotekickAll error: " + ex.Message); + } + + return votesSent; + } + + private void SendVotekickEveryoneStay() + { + int sent = ExecuteVotekickEveryone(false); + if (sent > 0) + ShowNotification($"[VOTEKICK] Sent {sent} votes. Staying in room."); + else + ShowNotification("[VOTEKICK] No valid targets or VoteBanSystem is not ready."); + } + + private void LeaveRoomAfterVotekick() + { + try + { + votekickExitQueued = false; + votekickExitAt = 0f; + votekickedPlayerIds.Clear(); + + if (AmongUsClient.Instance != null) + { + AmongUsClient.Instance.ExitGame(DisconnectReasons.ExitGame); + ShowNotification("[AUTO-VOTEKICK] Left room. Auto mode is still armed."); + } + } + catch (Exception ex) + { + System.Console.WriteLine("Auto-votekick leave error: " + ex.Message); + votekickExitQueued = false; + votekickExitAt = 0f; + } + } + + public static void ExecuteVotekickTarget(PlayerControl target) + { + if (target == null || target.Data == null || VoteBanSystem.Instance == null) return; + + try + { + int targetClientId = target.Data.ClientId; + + VoteBanSystem.Instance.CmdAddVote(targetClientId); + + System.Console.WriteLine($"Votekick added to player with ClientId: {targetClientId}"); + + if (DestroyableSingleton.Instance != null && DestroyableSingleton.Instance.Notifier != null) + { + DestroyableSingleton.Instance.Notifier.AddDisconnectMessage("Votekick sent! Leave and rejoin 2 more times."); + } + + ShowNotification($"[VOTEKICK] Vote sent to {target.Data.PlayerName}!"); + } + catch (Exception ex) + { + System.Console.WriteLine("Target Votekick error: " + ex.Message); + } + } + + private void DrawVotekickTab() + { + GUILayout.BeginVertical(boxStyle); + try + { + GUIStyle voteInfoStyle = new GUIStyle(toggleLabelStyle) { richText = true, wordWrap = true }; + GUILayout.Label("VOTEKICK MENU", headerStyle); + GUILayout.Label("Auto mode: sends 3 votes to every valid player, leaves the room, and stays armed until you press it again.", voteInfoStyle); + GUILayout.Space(5); + + string autoButtonText = votekickEveryone ? "STOP AUTO VOTEKICK + LEAVE" : "AUTO VOTEKICK + LEAVE"; + if (GUILayout.Button(autoButtonText, votekickEveryone ? activeTabStyle : btnStyle, GUILayout.Height(35))) + { + if (votekickEveryone) StopVotekickEveryoneRun(); + else StartVotekickEveryoneRun(); + } + + GUILayout.Space(5); + GUILayout.Label("Manual mode: sends 3 votes now and stays in the current room.", voteInfoStyle); + if (GUILayout.Button("SEND 3 VOTES + STAY", btnStyle, GUILayout.Height(32))) + { + SendVotekickEveryoneStay(); + } + } + finally { GUILayout.EndVertical(); } + + GUILayout.Space(10); + GUILayout.Label("TARGET VOTE", headerStyle); + + if (PlayerControl.AllPlayerControls != null) + { + var safePlayersList = new System.Collections.Generic.List(); + foreach (var p in PlayerControl.AllPlayerControls) safePlayersList.Add(p); + + votekickScrollPosition = GUILayout.BeginScrollView(votekickScrollPosition); + try + { + foreach (var pc in safePlayersList) + { + if (pc == null || pc.Data == null || pc.PlayerId >= 100 || pc == PlayerControl.LocalPlayer) continue; + + GUILayout.BeginHorizontal(boxStyle); + try + { + string pName = pc.Data.PlayerName ?? "Unknown"; + bool isHost = (AmongUsClient.Instance != null && AmongUsClient.Instance.GetHost()?.Character == pc); + string displayStr = isHost ? pName + " [H]" : pName; + + GUILayout.Label(displayStr, GUILayout.Width(110)); + + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("Vote", btnStyle, GUILayout.Width(60), GUILayout.Height(25))) + { + ExecuteVotekickTarget(pc); + } + } + finally + { + GUILayout.EndHorizontal(); + } + GUILayout.Space(2); + } + } + finally + { + GUILayout.EndScrollView(); + } + } + } + + private void DrawElysiumModMenu(int windowID) + { + if (Event.current.type == EventType.Repaint && tabTransitionProgress < 1f) + { + tabTransitionProgress += Time.unscaledDeltaTime * 8f; + if (tabTransitionProgress >= 1f) { tabTransitionProgress = 1f; currentTab = targetTabIndex; } + } + + if (enableBackground && customMenuBg != null) + { + GUI.color = new Color(0.6f, 0.6f, 0.6f, 0.8f); + GUIStyle bgStyle = new GUIStyle() { normal = { background = customMenuBg } }; + GUI.Box(new Rect(0, 0, windowRect.width, windowRect.height), GUIContent.none, bgStyle); + GUI.color = Color.white; + } + + GUILayout.BeginHorizontal(); + GUILayout.Label(ApplyMenuShimmer("ElysiumModMenu Meowchelo & Carrot"), titleStyle); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("-", new GUIStyle(btnStyle) { fixedWidth = 20, fixedHeight = 18, margin = CreateRectOffset(0, 8, 6, 0) })) showMenu = false; + GUILayout.EndHorizontal(); + + GUI.color = new Color(1f, 1f, 1f, 0.1f); + GUI.Box(new Rect(0, 30, windowRect.width, 1), "", safeLineStyle); + GUI.color = Color.white; + + GUILayout.BeginArea(new Rect(0f, 31f, 130f, windowRect.height - 31f)); + GUILayout.BeginVertical(sidebarStyle, GUILayout.ExpandHeight(true)); + GUILayout.Space(5); + for (int i = 0; i < tabNames.Length; i++) + if (GUILayout.Button(tabNames[i], i == targetTabIndex ? activeSidebarBtnStyle : sidebarBtnStyle, GUILayout.Height(24))) + if (targetTabIndex != i) { targetTabIndex = i; tabTransitionProgress = 0f; scrollPosition = Vector2.zero; } + GUILayout.EndVertical(); + GUILayout.EndArea(); + + GUI.color = new Color(1f, 1f, 1f, 0.1f); + GUI.Box(new Rect(130, 31, 1, windowRect.height), "", safeLineStyle); + GUI.color = new Color(1f, 1f, 1f, tabTransitionProgress); + + GUILayout.BeginArea(new Rect(140f, 36f + ((1f - tabTransitionProgress) * 10f), windowRect.width - 150f, windowRect.height - 46f)); + scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, false, GUIStyle.none, GUI.skin.verticalScrollbar); + int tabToDraw = (tabTransitionProgress < 1f) ? targetTabIndex : currentTab; + + if (tabToDraw == 0) DrawGeneralTab(); + else if (tabToDraw == 1) DrawSelfTab(); + else if (tabToDraw == 2) DrawVisualsTab(); + else if (tabToDraw == 3) DrawPlayersTab(); + else if (tabToDraw == 4) DrawSabotagesTab(); + else if (tabToDraw == 5) DrawHostOnlyTab(); + else if (tabToDraw == 6) DrawOutfitsTab(); + else if (tabToDraw == 7) DrawVotekickTab(); + else if (tabToDraw == 8) DrawMenuTab(); + else if (tabToDraw == 9) DrawMapsTab(); + else if (tabToDraw == 10) DrawAnimationsTab(); + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + + GUI.color = Color.white; + GUI.DragWindow(new Rect(0, 0, 10000, 30)); + } + public static int punishmentMode = 1; + public static string[] punishmentNames = { "Null", "Warn", "Kick", "Ban" }; + + public static bool blockSpoofRPC = true; + public static bool blockSabotageRPC = true; + public static bool blockGameRpcInLobby = true; + public static bool blockChatFloodRpc = true; + public static bool blockMeetingFloodRpc = true; + public static bool autoBanBrokenFriendCode = false; + public static int chatRpcLimit = 1; + public static float chatRpcWindow = 1f; + public static int meetingRpcLimit = 2; + public static float meetingRpcWindow = 9999f; + + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleAnimation))] + public static class PlayerPhysics_HandleAnimation + { + public static bool Prefix(PlayerPhysics __instance) + { + if (ElysiumModMenuGUI.moonWalk && __instance.AmOwner) + { + __instance.ResetAnimState(); + return false; + } + return true; + } + } + + [HarmonyPatch(typeof(FreeChatInputField), nameof(FreeChatInputField.UpdateCharCount))] + public static class FreeChatInputField_UpdateCharCount_Patch + { + public static void Postfix(FreeChatInputField __instance) + { + if (__instance == null || __instance.textArea == null || __instance.charCountText == null) return; + + __instance.textArea.characterLimit = 120; + + int length = __instance.textArea.text.Length; + + __instance.charCountText.SetText($"{length}/{__instance.textArea.characterLimit}"); + + if (length < 90) + { + __instance.charCountText.color = Color.white; + } + else if (length < 115) + { + __instance.charCountText.color = Color.yellow; + } + else + { + __instance.charCountText.color = Color.red; + } + } + } + + public static class ChatHistory + { + public static List sentMessages = new List(); + public static int HistoryIndex = -1; + public static string DraftBeforeHistory = ""; + public static bool BrowsingHistory = false; + + public static void Remember(string message) + { + if (string.IsNullOrWhiteSpace(message)) return; + bool isNewEntry = sentMessages.Count == 0 || sentMessages[sentMessages.Count - 1] != message; + if (isNewEntry) + { + sentMessages.Add(message); + } + HistoryIndex = sentMessages.Count; + } + + public static void HandleNavigation(ChatController chat) + { + if (sentMessages.Count == 0 || chat.freeChatField == null || chat.freeChatField.textArea == null || !chat.freeChatField.textArea.hasFocus) + return; + + if (Input.GetKeyDown(KeyCode.UpArrow)) + { + if (!BrowsingHistory) + { + DraftBeforeHistory = chat.freeChatField.textArea.text; + BrowsingHistory = true; + } + if (HistoryIndex <= 0) return; + + HistoryIndex = Mathf.Clamp(HistoryIndex - 1, 0, sentMessages.Count - 1); + chat.freeChatField.textArea.SetText(sentMessages[HistoryIndex], string.Empty); + } + else if (Input.GetKeyDown(KeyCode.DownArrow)) + { + if (!BrowsingHistory) return; + + HistoryIndex += 1; + if (HistoryIndex < sentMessages.Count) + { + chat.freeChatField.textArea.SetText(sentMessages[HistoryIndex], string.Empty); + } + else + { + chat.freeChatField.textArea.SetText(DraftBeforeHistory, string.Empty); + BrowsingHistory = false; + } + } + } + } + + public static class ClipboardBridge + { + private static bool isPastingChatInput = false; + private static int currentPasteCharPos = 0; + private static int lastClipboardFrame = -1; + + public static void Run(TextBoxTMP box) + { + if (!enableClipboard) return; + if (box == null || !box.hasFocus) return; + + bool controlHeld = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); + bool shiftHeld = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); + + bool copyPressed = controlHeld && (Input.GetKeyDown(KeyCode.C) || Input.GetKeyDown(KeyCode.Insert)); + bool pastePressed = (controlHeld && Input.GetKeyDown(KeyCode.V)) || (shiftHeld && Input.GetKeyDown(KeyCode.Insert)); + bool cutPressed = controlHeld && Input.GetKeyDown(KeyCode.X); + + if (!copyPressed && !pastePressed && !cutPressed) return; + if (lastClipboardFrame == Time.frameCount) return; + lastClipboardFrame = Time.frameCount; + + if (copyPressed) + { + GUIUtility.systemCopyBuffer = box.text ?? string.Empty; + } + else if (pastePressed) + { + string paste = GUIUtility.systemCopyBuffer; + if (!string.IsNullOrEmpty(paste)) + { + string currentText = box.text ?? string.Empty; + int caretPos = Mathf.Clamp(box.caretPos, 0, currentText.Length); + string nextText = currentText.Insert(caretPos, paste); + + isPastingChatInput = true; + box.SetText(nextText, string.Empty); + isPastingChatInput = false; + } + } + else if (cutPressed) + { + GUIUtility.systemCopyBuffer = box.text ?? string.Empty; + box.SetText(string.Empty, string.Empty); + } + } + + public static bool IsCharAllowed(TextBoxTMP box, ref bool result) + { + if (box == null) return true; + + string input = isPastingChatInput ? GUIUtility.systemCopyBuffer : Input.inputString; + if (string.IsNullOrEmpty(input)) return true; + + string currentText = box.text ?? string.Empty; + int caretPos = Mathf.Clamp(box.caretPos, 0, currentText.Length); + string text = currentText.Insert(caretPos, input); + + currentPasteCharPos = Mathf.Clamp(currentPasteCharPos, 0, Mathf.Max(0, text.Length - 1)); + char currentChar = text[currentPasteCharPos]; + currentPasteCharPos = currentPasteCharPos >= text.Length - 1 ? 0 : currentPasteCharPos + 1; + + if (enableClipboard || allowLinksAndSymbols) + { + result = currentChar != '\b' && currentChar != '\r' && currentChar != '\n'; + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.Update))] + public static class AllowSymbols_TextBoxTMP_Update_Patch + { + public static void Postfix(TextBoxTMP __instance) + { + if (__instance == null) return; + __instance.allowAllCharacters = true; + __instance.AllowSymbols = true; + __instance.AllowEmail = true; + } + } + + [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.Update))] + public static class Clipboard_TextBoxTMP_Patch + { + public static void Postfix(TextBoxTMP __instance) + { + ClipboardBridge.Run(__instance); + } + } + + [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.IsCharAllowed))] + public static class Clipboard_TextBoxTMP_IsCharAllowed_Patch + { + public static bool Prefix(TextBoxTMP __instance, ref bool __result) + { + return ClipboardBridge.IsCharAllowed(__instance, ref __result); + } + } + + [HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] + public static class ChatHistory_Update_Patch + { + public static void Postfix(ChatController __instance) + { + if (__instance != null && __instance.freeChatField != null && __instance.freeChatField.textArea != null) + { + ClipboardBridge.Run(__instance.freeChatField.textArea); + } + ChatHistory.HandleNavigation(__instance); + } + } + public static bool enableExtendedChat = true; + public static bool enableChatHistory = true; + public static bool enableClipboard = true; + public static bool AnimEmptyGarbageEnabled = false; + public static bool skipShhhAnim = false; + public static bool isManualMapSpawn = false; + private void DrawAnimationsTab() + { + GUILayout.BeginVertical(boxStyle); + + GUILayout.Label(L("LOOPED PLAYER ANIMATIONS", "ЗАЦИКЛЕННЫЕ АНИМАЦИИ ИГРОКА"), headerStyle); + + string animInfo = L("Animations are looped. They will run as long as the toggle is ON.", + "Анимации зациклены. Будут работать, пока включен тумблер."); + GUILayout.Label(animInfo, new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); + + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + AnimAsteroidsEnabled = DrawToggle(AnimAsteroidsEnabled, L("Weapons (Asteroids)", "Оружие (Астероиды)"), 250); + IsScanning = DrawToggle(IsScanning, L("Medbay Scan", "Скан в медпункте"), 250); + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + AnimShieldsEnabled = DrawToggle(AnimShieldsEnabled, L("Turn On Shields", "Включить щиты"), 250); + AnimCamsInUseEnabled = DrawToggle(AnimCamsInUseEnabled, L("Use Cameras (Blink Red)", "Камеры (Красный индикатор)"), 250); + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + AnimEmptyGarbageEnabled = DrawToggle(AnimEmptyGarbageEnabled, L("Empty Garbage", "Выкинуть мусор"), 250); + skipShhhAnim = DrawToggle(skipShhhAnim, L("Skip 'Shhh!' Intro", "Пропустить 'Shhh!' интро"), 250); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + } + public static string GetPlatform(ClientData client) + { + if (client == null || client.PlatformData == null) return "Unknown"; + + int platformId = (int)client.PlatformData.Platform; + + switch (platformId) + { + case 1: return "Epic"; + case 2: return "Steam"; + case 3: return "Mac"; + case 4: return "Microsoft"; + case 5: return "Itch"; + case 6: return "iOS"; + case 7: return "Android"; + case 8: return "Switch"; + case 9: return "Xbox"; + case 10: return "PlayStation"; + case 112: return "Starlight"; + default: return $"Unknown ({platformId})"; + } + } + + private static string CompactEspValue(string value, int maxLength = 24) + { + value = Regex.Replace(value ?? string.Empty, "<.*?>", string.Empty) + .Replace('\r', ' ') + .Replace('\n', ' ') + .Trim(); + + if (string.IsNullOrEmpty(value)) return "Hidden"; + if (value.Length > maxLength) value = value.Substring(0, maxLength - 3) + "..."; + return value; + } + + public static string BuildESPInfoLine(NetworkedPlayerInfo info) + { + if (info == null) return string.Empty; + + int level = 0; + string platform = "Unknown"; + bool isHost = false; + + try { level = (int)info.PlayerLevel + 1; } catch { } + + try + { + var client = AmongUsClient.Instance.GetClientFromPlayerInfo(info); + if (client != null) + { + platform = GetPlatform(client); + isHost = AmongUsClient.Instance.GetHost() == client; + } + } + catch { } + + if (enablePlatformSpoof && + PlayerControl.LocalPlayer != null && + info.PlayerId == PlayerControl.LocalPlayer.PlayerId) + { + platform = $"{platform} spf"; + } + + string fc = CompactEspValue(GetDisplayedFriendCode(info)); + List parts = new List(); + if (isHost) parts.Add("Host"); + parts.Add($"Lv:{level}"); + parts.Add(platform); + parts.Add(fc); + return string.Join(" - ", parts); + } + + public static Color GetRoleColor(int roleId, Color fallbackColor) + { + switch (roleId) + { + case 1: return new Color32(255, 0, 0, 255); + case 2: return new Color32(0, 0, 128, 255); + case 3: return new Color32(127, 255, 212, 255); + case 4: return new Color32(176, 196, 222, 255); + case 5: return new Color32(255, 140, 0, 255); + case 8: return new Color32(255, 105, 180, 255); + case 9: return new Color32(139, 0, 0, 255); + case 10: return new Color32(106, 90, 205, 255); + case 12: return new Color32(189, 183, 107, 255); + case 18: return new Color32(173, 255, 47, 255); + default: return fallbackColor; + } + } + + public static void HandleTracer(PlayerControl target, bool enable) + { + try + { + if (target == null || target.gameObject == null) return; + + LineRenderer lr = target.GetComponent(); + + if (!enable || PlayerControl.LocalPlayer == null || target == PlayerControl.LocalPlayer || target.Data == null || target.Data.Disconnected) + { + if (lr != null) lr.enabled = false; + return; + } + + if (target.Data.IsDead && !seeGhosts && !PlayerControl.LocalPlayer.Data.IsDead) + { + if (lr != null) lr.enabled = false; + return; + } + + if (lr == null) + { + lr = target.gameObject.AddComponent(); + lr.SetVertexCount(2); + lr.SetWidth(0.02f, 0.02f); + try { if (HatManager.Instance != null) lr.material = HatManager.Instance.PlayerMaterial; } catch { } + } + + lr.enabled = true; + + Color tColor = Color.white; + try + { + if (target.Data.IsDead) + { + tColor = Color.gray; + } + else if (target.Data.Role != null) + { + tColor = GetRoleColor((int)target.Data.Role.Role, target.Data.Role.TeamColor); + } + } + catch { } + + lr.SetColors(tColor, tColor); + + lr.SetPosition(0, PlayerControl.LocalPlayer.transform.position); + lr.SetPosition(1, target.transform.position); + } + catch { } + } + + + private void DrawLobbyControls() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("LOBBY CONTROLS", headerStyle); + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(GUILayout.Width(280)); + neverEndGame = DrawToggle(neverEndGame, "Unlimited Game", 250); + GUILayout.Space(5); + noSettingLimit = DrawToggle(noSettingLimit, "No Setting Limit", 250); + GUILayout.Space(5); + noTaskMode = DrawToggle(noTaskMode, "No Task Mode", 250); + GUILayout.Space(5); + enableColorCommand = DrawToggle(enableColorCommand, "Enable /c command (Public)", 250); + GUILayout.Space(5); + blockFortegreenChat = DrawToggle(blockFortegreenChat, "Block Fortegreen Chat", 250); + GUILayout.Space(5); + blockRainbowChat = DrawToggle(blockRainbowChat, "Block Rainbow Chat", 250); + GUILayout.Space(5); + + autoChatEveryone = DrawToggle(autoChatEveryone, "Chat Everyone (Auto-Meeting)", 250); + if (autoChatEveryone) + { + GUILayout.BeginHorizontal(); + autoChatEveryoneDelay = GUILayout.HorizontalSlider(autoChatEveryoneDelay, 0f, 10f, sliderStyle, sliderThumbStyle, GUILayout.Width(240)); + GUILayout.EndHorizontal(); + } + + GUILayout.EndVertical(); + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + GUILayout.Label("HOST ACTIONS", headerStyle); + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(GUILayout.Width(280)); + if (GUILayout.Button("Insta Start", btnStyle, GUILayout.Height(25))) + { GameStartManager.Instance.startState = GameStartManager.StartingStates.Countdown; GameStartManager.Instance.countDownTimer = 0f; } + GUILayout.Space(5); + if (GUILayout.Button("Close Meeting", btnStyle, GUILayout.Height(25))) MeetingHud.Instance.RpcClose(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Spawn Lobby", activeTabStyle, GUILayout.Height(25))) SpawnLobby(); + GUILayout.Space(5); + if (GUILayout.Button("Despawn", btnStyle, GUILayout.Height(25))) DespawnLobby(); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Kill All", btnStyle, GUILayout.Height(25))) KillAll(); + GUILayout.Space(5); + if (GUILayout.Button("Kick All", btnStyle, GUILayout.Height(25))) KickAll(); + GUILayout.Space(5); + if (GUILayout.Button("Mass Morph", btnStyle, GUILayout.Height(25))) this.StartCoroutine(MassMorphCoroutine().WrapToIl2Cpp()); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.BeginVertical(GUILayout.Width(280)); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Crewmate Win", btnStyle, GUILayout.Height(25))) SmartEndGame("CrewWin"); + GUILayout.Space(5); + if (GUILayout.Button("Impostor Win", btnStyle, GUILayout.Height(25))) SmartEndGame("ImpWin"); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Imp Disconnect", btnStyle, GUILayout.Height(25))) SmartEndGame("ImpDisconnect"); + GUILayout.Space(5); + if (GUILayout.Button("H&S Disconnect", activeTabStyle, GUILayout.Height(25))) SmartEndGame("HnsImpDisconnect"); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + if (GUILayout.Button("Force End (Impostor Disconnect)", btnStyle, GUILayout.Height(25)) && GameManager.Instance != null && AmongUsClient.Instance.AmHost) + { bool tempNeverEnd = neverEndGame; neverEndGame = false; GameManager.Instance.RpcEndGame((GameOverReason)4, false); neverEndGame = tempNeverEnd; } + GUILayout.EndVertical(); + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + } + public static string GetESPNameTag(NetworkedPlayerInfo info, string originalName) + { + if (info == null) return originalName; + string newName = originalName; + if (seeRoles && info.Role != null) + { + string roleName = info.Role.Role.ToString(); + int roleId = (int)info.Role.Role; + if (roleId == 8) roleName = "Noisemaker"; + else if (roleId == 9) roleName = "Phantom"; + else if (roleId == 10) roleName = "Tracker"; + else if (roleId == 12) roleName = "Detective"; + else if (roleId == 18) roleName = "Viper"; + else if (roleName == "GuardianAngel") roleName = "Guardian Angel"; + Color customColor = GetRoleColor(roleId, info.Role.TeamColor); + string roleColor = ColorUtility.ToHtmlStringRGB(customColor); + newName = $"{roleName}\n{newName}"; + } + if (showPlayerInfo) + { + string accentHex = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); + newName = $"{BuildESPInfoLine(info)}\n{newName}"; + } + if (seeKillCooldown && info.Role != null && info.PlayerId != PlayerControl.LocalPlayer?.PlayerId) + { + int roleId = (int)info.Role.Role; + bool isImpTeam = roleId == 1 || roleId == 5 || roleId == 9 || roleId == 18; + if (isImpTeam) + { + float rem = GetRemainingKillCooldown(info.PlayerId); + string kcdText = rem > 0.01f ? $"KCD: {rem:F1}s" : "KCD: READY"; + string kcdColor = rem > 0.01f ? "FFAA33" : "55FF77"; + newName = $"{kcdText}\n{newName}"; + } + } + return newName; + } + + private static float GetConfiguredKillCooldown() + { + try + { + object opts = GameOptionsManager.Instance?.CurrentGameOptions; + if (opts == null) return 25f; + var m = opts.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(x => x.Name == "GetFloat" && x.GetParameters().Length == 1); + if (m != null) + { + Type enumType = m.GetParameters()[0].ParameterType; + if (enumType.IsEnum) + { + foreach (var val in Enum.GetValues(enumType)) + { + string n = val.ToString().ToLower(); + if (n.Contains("kill") && n.Contains("cool")) + { + object result = m.Invoke(opts, new object[] { val }); + return Convert.ToSingle(result); + } + } + } + } + } + catch { } + return 25f; + } + + private static float GetRemainingKillCooldown(byte playerId) + { + if (!lastKillTimestamps.ContainsKey(playerId)) return 0f; + float elapsed = Time.time - lastKillTimestamps[playerId]; + float rem = GetConfiguredKillCooldown() - elapsed; + return Mathf.Max(0f, rem); + } + + private static bool IsImpostorTeamForCooldown(PlayerControl pc) + { + try + { + if (pc == null || pc.Data == null) return false; + int roleId = pc.Data.Role != null ? (int)pc.Data.Role.Role : (int)pc.Data.RoleType; + return roleId == 1 || roleId == 5 || roleId == 9 || roleId == 18; + } + catch { return false; } + } + + public static void InitializeKillCooldownOnRoundStart() + { + try + { + lastKillTimestamps.Clear(); + if (PlayerControl.AllPlayerControls == null) return; + + float now = Time.time; + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.Data == null || pc.Data.Disconnected) continue; + if (!IsImpostorTeamForCooldown(pc)) continue; + lastKillTimestamps[pc.PlayerId] = now; + } + } + catch { } + } + + + [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] + public static class VersionShower_Start_Patch + { + public static void Postfix(VersionShower __instance) { if (__instance != null && __instance.text != null) __instance.text.text = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu Meowchelo & Carrot"); } + } + + [HarmonyPatch(typeof(PingTracker), nameof(PingTracker.Update))] + public static class PingTracker_Watermark_Patch + { + private static float _smoothFps = 0f; + private static int _smoothPing = 0; + private static float _updateTimer = 0f; + public static void Postfix(PingTracker __instance) + { + try + { + _updateTimer += Time.deltaTime; + if (_updateTimer >= 0.5f) { _smoothFps = 1f / Time.deltaTime; if (AmongUsClient.Instance != null) _smoothPing = AmongUsClient.Instance.Ping; _updateTimer = 0f; } + int num = Mathf.RoundToInt(_smoothFps); + string pingColor = ((_smoothPing < 80) ? "#00FF00" : ((_smoothPing < 400) ? "#FFFF00" : "#FF0000")); + + string finalString = $"PING: {_smoothPing} ms • FPS: {num}"; + + if (ElysiumModMenuGUI.showWatermark) + { + string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.0"); + finalString = $"{shimmerTitle} • " + finalString; + } + + if (AmongUsClient.Instance != null) + { + ClientData host = AmongUsClient.Instance.GetHost(); + if (host != null && host.Character != null) + { + string hostName = host.Character.Data.PlayerName ?? "Unknown"; + string shimmerHostName = ElysiumModMenuGUI.ApplyMenuShimmer(hostName); + finalString += $" • Host: {shimmerHostName}"; + if (AmongUsClient.Instance.AmHost) finalString += " (You)"; + } + } + __instance.text.text = finalString; + __instance.text.alignment = TMPro.TextAlignmentOptions.Center; + __instance.aspectPosition.enabled = false; + float zPos = MeetingHud.Instance != null && MeetingHud.Instance.gameObject.activeInHierarchy ? -100f : -10f; + __instance.transform.localPosition = new Vector3(0f, -2.3f, zPos); + } + catch { } + } + } + + [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Update))] + public static class GameStartManager_Update_Patch + { + public static void Postfix(GameStartManager __instance) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; + if (ElysiumModMenuGUI.fakeStartCounterTroll) + { + try { sbyte[] arr = { -123, -111, -100, -69, -67, -52, -42, 0, 42, 52, 67, 69, 100, 111, 123 }; sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; PlayerControl.LocalPlayer.RpcSetStartCounter(b); __instance.SetStartCounter(b); } catch { } + } + else if (ElysiumModMenuGUI.fakeStartCounterCustom && int.TryParse(ElysiumModMenuGUI.fakeStartInput, out int custom)) + { + try { PlayerControl.LocalPlayer.RpcSetStartCounter(custom); __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); } catch { } + } + } + } + + [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] + public static class InfiniteGamePatch { public static bool Prefix() { try { if (ElysiumModMenuGUI.neverEndGame && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) return false; } catch { } return true; } } + + [HarmonyPatch(typeof(IntroCutscene), "CoBegin")] + public static class IntroCutscene_CoBegin_Patch + { + public static void Prefix() + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + if (ElysiumModMenuGUI.enablePreGameRoleForce) + { + foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) + { var target = GameData.Instance.GetPlayerById(kvp.Key)?.Object; if (target != null && target.Data.RoleType != kvp.Value) target.RpcSetRole(kvp.Value); } + foreach (byte impId in ElysiumModMenuGUI.forcedImpostors) + { var target = GameData.Instance.GetPlayerById(impId)?.Object; if (target != null && target.Data.Role != null && !target.Data.Role.IsImpostor) target.RpcSetRole(RoleTypes.Impostor); } + } + } + } + + [HarmonyPatch(typeof(LogicRoleSelectionNormal), "AssignRolesForTeam")] + public static class RoleSelectionNormal_Patch + { + public static bool Prefix(Il2CppSystem.Collections.Generic.List players, IGameOptions opts, RoleTeamTypes team, ref int teamMax) + { + if (!ElysiumModMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; + try + { + if ((int)team == 1) + { + int numImps = opts.GetInt((Int32OptionNames)1); + var impRoleTypes = new HashSet { 1, 5, 9, 18 }; + List allForced = new List(ElysiumModMenuGUI.forcedImpostors); + foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) if (impRoleTypes.Contains((int)kvp.Value) && !allForced.Contains(kvp.Key)) allForced.Add(kvp.Key); + if (allForced.Count > 0) numImps = allForced.Count; + else { if (numImps >= players.Count) numImps = players.Count - 1; if (numImps < 1) numImps = 1; } + int assigned = 0; + foreach (byte impId in allForced) + { + if (players.Count == 0 || assigned >= numImps) break; + var targetInfo = players.ToArray().FirstOrDefault(p => p.PlayerId == impId); + if (targetInfo != null && targetInfo.Object != null) + { + RoleTypes role = ElysiumModMenuGUI.forcedPreGameRoles.ContainsKey(impId) ? ElysiumModMenuGUI.forcedPreGameRoles[impId] : RoleTypes.Impostor; + targetInfo.Object.RpcSetRole(role, false); + players.Remove(targetInfo); + assigned++; + } + } + while (assigned < numImps && players.Count > 0) + { + int idx = UnityEngine.Random.Range(0, players.Count); + players[idx].Object.RpcSetRole(RoleTypes.Impostor, false); + players.RemoveAt(idx); + assigned++; + } + return false; + } + else if ((int)team == 0) + { + var crewRoleTypes = new HashSet { 0, 2, 3, 4, 8, 10, 12 }; + for (int i = players.Count - 1; i >= 0; i--) + { + var p = players[i]; + if (p != null && p.Object != null) + { + RoleTypes role = RoleTypes.Crewmate; + if (ElysiumModMenuGUI.forcedPreGameRoles.ContainsKey(p.PlayerId) && crewRoleTypes.Contains((int)ElysiumModMenuGUI.forcedPreGameRoles[p.PlayerId])) + role = ElysiumModMenuGUI.forcedPreGameRoles[p.PlayerId]; + p.Object.RpcSetRole(role, false); + players.RemoveAt(i); + } + } + return false; + } + return true; + } + catch { return true; } + } + } + + [HarmonyPatch(typeof(LogicRoleSelectionHnS), "AssignRolesForTeam")] + public static class RoleSelectionHnS_Patch + { + public static bool Prefix(Il2CppSystem.Collections.Generic.List players, IGameOptions opts, RoleTeamTypes team, ref int teamMax) + { + if (!ElysiumModMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; + if ((int)team != 1) return true; + try + { + int numImps = opts.GetInt((Int32OptionNames)1); + var impRoleTypes = new HashSet { 1, 5, 9, 18 }; + List allForced = new List(ElysiumModMenuGUI.forcedImpostors); + foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) if (impRoleTypes.Contains((int)kvp.Value) && !allForced.Contains(kvp.Key)) allForced.Add(kvp.Key); + if (allForced.Count > 0) numImps = allForced.Count; + else { if (numImps >= players.Count) numImps = players.Count - 1; if (numImps < 1) numImps = 1; } + int assigned = 0; + foreach (byte impId in allForced) + { + if (players.Count == 0 || assigned >= numImps) break; + var targetInfo = players.ToArray().FirstOrDefault(p => p.PlayerId == impId); + if (targetInfo != null) { targetInfo.Object.RpcSetRole((RoleTypes)1, false); players.Remove(targetInfo); assigned++; } + } + while (assigned < numImps && players.Count > 0) + { + int idx = UnityEngine.Random.Range(0, players.Count); + players[idx].Object.RpcSetRole((RoleTypes)1, false); + players.RemoveAt(idx); + assigned++; + } + return false; + } + catch { return true; } + } + } + + [HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SelectRoles))] + public static class RoleManager_SelectRoles_Patch + { + public static bool Prefix(RoleManager __instance) + { + if (!ElysiumModMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; + try + { + var allPlayers = PlayerControl.AllPlayerControls.ToArray().Where(p => p != null && p.Data != null && !p.Data.Disconnected && !p.Data.IsDead).ToList(); + int numImps = 1; + try { numImps = GameOptionsManager.Instance.CurrentGameOptions.GetInt((Int32OptionNames)1); } catch { } + var impRoleTypes = new HashSet { 1, 5, 9, 18 }; + List impostors = new List(); + foreach (var p in allPlayers) + if (ElysiumModMenuGUI.forcedImpostors.Contains(p.PlayerId) || (ElysiumModMenuGUI.forcedPreGameRoles.ContainsKey(p.PlayerId) && impRoleTypes.Contains((int)ElysiumModMenuGUI.forcedPreGameRoles[p.PlayerId]))) + impostors.Add(p); + if (impostors.Count > 0) numImps = impostors.Count; + else { if (numImps >= allPlayers.Count) numImps = allPlayers.Count - 1; if (numImps < 1) numImps = 1; } + System.Random rand = new System.Random(); + while (impostors.Count < numImps && allPlayers.Count > impostors.Count) + { + var available = allPlayers.Where(p => !impostors.Contains(p)).ToList(); + impostors.Add(available[rand.Next(available.Count)]); + } + List crewmates = allPlayers.Where(p => !impostors.Contains(p)).ToList(); + var impData = new Il2CppSystem.Collections.Generic.List(); + foreach (var i in impostors) impData.Add(i.Data); + var crewData = new Il2CppSystem.Collections.Generic.List(); + foreach (var c in crewmates) crewData.Add(c.Data); + IGameOptions opts = GameOptionsManager.Instance.CurrentGameOptions; + GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(impData, opts, (RoleTeamTypes)1, int.MaxValue, new Il2CppSystem.Nullable()); + GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(crewData, opts, (RoleTeamTypes)0, int.MaxValue, new Il2CppSystem.Nullable((RoleTypes)0)); + foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) + { + if (kvp.Value != RoleTypes.Crewmate && kvp.Value != RoleTypes.Impostor) + { + var pc = allPlayers.FirstOrDefault(p => p.PlayerId == kvp.Key); + if (pc != null) RoleManager.Instance.SetRole(pc, kvp.Value); + } + } + foreach (var pc in allPlayers) if (pc.Data.Role != null) pc.Data.Role.Initialize(pc); + return false; + } + catch { return true; } + } + } + + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.TurnOnProtection))] + public static class PlayerControl_TurnOnProtection_Patch + { + public static void Prefix(ref bool visible) + { + if (ElysiumModMenuGUI.seeGhosts || ElysiumModMenuGUI.seeProtections) visible = true; + } + } + + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.LateUpdate))] + public static class PlayerVisuals_LateUpdate_Patch + { + public static void Postfix(PlayerPhysics __instance) + { + if (__instance == null || __instance.myPlayer == null || __instance.myPlayer.Data == null) return; + try + { + if (ElysiumModMenuGUI.seeGhosts && __instance.myPlayer.Data.IsDead && PlayerControl.LocalPlayer != null && !PlayerControl.LocalPlayer.Data.IsDead) + { + __instance.myPlayer.Visible = true; + var rend = __instance.myPlayer.GetComponent(); + if (rend != null) { Color c = rend.color; rend.color = new Color(c.r, c.g, c.b, 0.4f); } + } + var cosmetics = __instance.myPlayer.cosmetics; + var outfit = __instance.myPlayer.CurrentOutfit; + if (cosmetics != null && cosmetics.nameText != null && outfit != null) + { + cosmetics.SetName(ElysiumModMenuGUI.GetESPNameTag(__instance.myPlayer.Data, outfit.PlayerName)); + if (ElysiumModMenuGUI.seeRoles && ElysiumModMenuGUI.showPlayerInfo) cosmetics.nameText.transform.localPosition = new Vector3(0f, 0.186f, 0f); + else if (ElysiumModMenuGUI.seeRoles || ElysiumModMenuGUI.showPlayerInfo) cosmetics.nameText.transform.localPosition = new Vector3(0f, 0.093f, 0f); + else cosmetics.nameText.transform.localPosition = new Vector3(0f, 0f, 0f); + } + } + catch { } + } + } + + [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Update))] + public static class ESP_MeetingHud + { + public static void Postfix(MeetingHud __instance) + { + try + { + if (__instance.playerStates == null) return; + foreach (var state in __instance.playerStates) + { + if (state == null) continue; + var data = GameData.Instance.GetPlayerById(state.TargetPlayerId); + if (data != null && !data.Disconnected && data.DefaultOutfit != null && state.NameText != null) + { + string espName = ElysiumModMenuGUI.GetESPNameTag(data, data.DefaultOutfit.PlayerName ?? "???"); + if (!ElysiumModMenuGUI.seeRoles && ElysiumModMenuGUI.revealMeetingRoles && data.Role != null) + { + string roleName = data.Role.Role.ToString(); + int roleId = (int)data.Role.Role; + if (roleId == 8) roleName = "Noisemaker"; + else if (roleId == 9) roleName = "Phantom"; + else if (roleId == 10) roleName = "Tracker"; + else if (roleId == 12) roleName = "Detective"; + else if (roleId == 18) roleName = "Viper"; + else if (roleName == "GuardianAngel") roleName = "Guardian Angel"; + Color customColor = ElysiumModMenuGUI.GetRoleColor(roleId, data.Role.TeamColor); + string roleColor = ColorUtility.ToHtmlStringRGB(customColor); + espName = $"{roleName}\n{espName}"; + } + state.NameText.text = espName; + bool showingExtra = ElysiumModMenuGUI.seeRoles || ElysiumModMenuGUI.revealMeetingRoles; + if (showingExtra && ElysiumModMenuGUI.showPlayerInfo) { state.NameText.transform.localPosition = new Vector3(0.33f, 0.08f, 0f); state.NameText.transform.localScale = new Vector3(0.75f, 0.75f, 0.75f); } + else if (showingExtra || ElysiumModMenuGUI.showPlayerInfo) { state.NameText.transform.localPosition = new Vector3(0.3384f, 0.1125f, -0.1f); state.NameText.transform.localScale = new Vector3(0.9f, 1f, 1f); } + else { state.NameText.transform.localPosition = new Vector3(0.3384f, 0.0311f, -0.1f); state.NameText.transform.localScale = new Vector3(0.9f, 1f, 1f); } + } + } + } + catch { } + } + } + [HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetName))] + public static class ChatBubble_SetName_Patch + { + public static void Postfix(ChatBubble __instance) + { + if (!ElysiumModMenuGUI.showPlayerInfo || __instance.playerInfo == null) return; + try + { + string accentHex = ColorUtility.ToHtmlStringRGB(ElysiumModMenuGUI.currentAccentColor); + string extra = $" {ElysiumModMenuGUI.BuildESPInfoLine(__instance.playerInfo)}"; + + if (!__instance.NameText.text.Contains("Lv:")) __instance.NameText.text += extra; + } + catch { } + } + } + + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcMurderPlayer))] + public static class KillCooldownTrackerPatch + { + public static void Prefix(PlayerControl __instance, PlayerControl target, bool didSucceed) + { + try + { + if (!didSucceed || __instance == null || __instance.Data == null) return; + ElysiumModMenuGUI.lastKillTimestamps[__instance.PlayerId] = Time.time; + } + catch { } + } + } + + [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] + public static class FullBright_Patch + { + public static void Postfix(HudManager __instance) + { + try + { + if (__instance == null || __instance.ShadowQuad == null || __instance.ShadowQuad.gameObject == null) return; + __instance.ShadowQuad.gameObject.SetActive(!ElysiumModMenuGUI.fullBright); + } + catch { } + } + } + + [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] + public static class HudManager_Update_Patch + { + public static void Postfix(HudManager __instance) + { + try + { + if (ElysiumModMenuGUI.alwaysChat && __instance.Chat != null) + __instance.Chat.gameObject.SetActive(true); + } + catch { } + } + } + + [HarmonyPatch(typeof(PlatformSpecificData), nameof(PlatformSpecificData.Serialize))] + public static class PlatformSpooferPatch { public static void Prefix(PlatformSpecificData __instance) { try { if (ElysiumModMenuGUI.enablePlatformSpoof && __instance != null) __instance.Platform = ElysiumModMenuGUI.platformValues[ElysiumModMenuGUI.currentPlatformIndex]; } catch { } } } + + [HarmonyPatch(typeof(FullAccount), nameof(FullAccount.CanSetCustomName))] + public static class FullAccount_CanSetCustomName_Patch { public static void Prefix(ref bool canSetName) { try { if (ElysiumModMenuGUI.unlockFeatures) canSetName = true; } catch { } } } + + [HarmonyPatch(typeof(AccountManager), nameof(AccountManager.CanPlayOnline))] + public static class AccountManager_CanPlayOnline_Patch { public static void Postfix(ref bool __result) { try { if (ElysiumModMenuGUI.unlockFeatures) __result = true; } catch { } } } + + [HarmonyPatch(typeof(EngineerRole), "FixedUpdate")] + public static class EngineerCheatsPatch + { + public static void Postfix(EngineerRole __instance) + { + if (__instance.Player != PlayerControl.LocalPlayer) return; + if (ElysiumModMenuGUI.endlessVentTime) __instance.inVentTimeRemaining = float.MaxValue; + if (ElysiumModMenuGUI.noVentCooldown && __instance.cooldownSecondsRemaining > 0f) + { + __instance.cooldownSecondsRemaining = 0f; + var btn = DestroyableSingleton.Instance?.AbilityButton; + if (btn != null) { btn.ResetCoolDown(); btn.SetCooldownFill(0f); } + } + } + } + + [HarmonyPatch(typeof(PlayerControl), "MurderPlayer")] + public static class KillCooldownTrackerPatch2 + { + public static void Prefix(PlayerControl __instance, PlayerControl target) + { + try + { + if (__instance == null || __instance.Data == null) return; + ElysiumModMenuGUI.lastKillTimestamps[__instance.PlayerId] = Time.time; + + if (!ElysiumModMenuGUI.spamReportBodies) return; + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null || PlayerControl.LocalPlayer.Data.IsDead) return; + if (target == null || target.Data == null || !target.Data.IsDead) return; + + PlayerControl.LocalPlayer.CmdReportDeadBody(target.Data); + } + catch { } + } + } + + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetKillTimer))] + public static class KillAuraNoKillCooldownPatch + { + public static void Prefix(PlayerControl __instance, ref float time) + { + try + { + if (!ElysiumModMenuGUI.noKillCooldownHostOnly) return; + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + if (__instance != PlayerControl.LocalPlayer) return; + time = 0f; + } + catch { } + } + } + + [HarmonyPatch(typeof(ScientistRole), "Update")] + public static class ScientistCheatsPatch + { + public static void Postfix(ScientistRole __instance) + { + if (__instance.Player != PlayerControl.LocalPlayer) return; + if (ElysiumModMenuGUI.noVitalsCooldown) __instance.currentCooldown = 0f; + if (ElysiumModMenuGUI.endlessBattery) __instance.currentCharge = float.MaxValue; + } + } + + [HarmonyPatch(typeof(ShapeshifterRole), "FixedUpdate")] + public static class ShapeshifterDurationPatch + { + public static void Postfix(ShapeshifterRole __instance) { if (__instance.Player == PlayerControl.LocalPlayer && ElysiumModMenuGUI.endlessSsDuration) __instance.durationSecondsRemaining = float.MaxValue; } + } + + [HarmonyPatch(typeof(ImpostorRole), "FindClosestTarget")] + public static class ImpostorRangePatch + { + public static bool Prefix(ImpostorRole __instance, ref PlayerControl __result) + { + if (!ElysiumModMenuGUI.killReach) return true; + try + { + var target = PlayerControl.AllPlayerControls.ToArray() + .Where(p => p != null && __instance.IsValidTarget(p.Data) && !p.Data.IsDead && !p.Data.Disconnected) + .OrderBy(p => Vector2.Distance(p.transform.position, PlayerControl.LocalPlayer.transform.position)) + .FirstOrDefault(); + if (target != null) __result = target; + return false; + } + catch { return true; } + } + } + + [HarmonyPatch(typeof(ImpostorRole), "IsValidTarget")] + public static class ImpostorKillAnyonePatch + { + public static void Postfix(NetworkedPlayerInfo target, ref bool __result) { try { if (ElysiumModMenuGUI.killAnyone && target != null && target.PlayerId != PlayerControl.LocalPlayer.PlayerId && !target.IsDead) __result = true; } catch { } } + } + + private void teleportToPlayer(PlayerControl t) + { + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.NetTransform == null || t == null) return; + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(t.transform.position); + } + + [HarmonyPatch(typeof(DetectiveRole), "FindClosestTarget")] + public static class DetectiveRangePatch + { + public static bool Prefix(DetectiveRole __instance, ref PlayerControl __result) + { + if (!ElysiumModMenuGUI.UnlimitedInterrogateRange) return true; + try + { + var target = PlayerControl.AllPlayerControls.ToArray() + .Where(p => p != null && __instance.IsValidTarget(p.Data) && !p.Data.IsDead && !p.Data.Disconnected) + .OrderBy(p => Vector2.Distance(p.transform.position, PlayerControl.LocalPlayer.transform.position)) + .FirstOrDefault(); + if (target != null) __result = target; + return false; + } + catch { return true; } + } + } + + [HarmonyPatch(typeof(DoorBreakerGame), nameof(DoorBreakerGame.Start))] + public static class DoorBreakerGame_Start_Patch + { + public static bool Prefix(DoorBreakerGame __instance) + { + if (!ElysiumModMenuGUI.autoOpenDoors) return true; + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(__instance.MyDoor.Id | 64)); } catch { } + __instance.MyDoor.SetDoorway(true); __instance.Close(); + return false; + } + } + [HarmonyPatch(typeof(DoorCardSwipeGame), nameof(DoorCardSwipeGame.Begin))] + public static class DoorCardSwipeGame_Begin_Patch + { + public static bool Prefix(DoorCardSwipeGame __instance) + { + if (!ElysiumModMenuGUI.autoOpenDoors) return true; + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(__instance.MyDoor.Id | 64)); } catch { } + __instance.MyDoor.SetDoorway(true); __instance.Close(); + return false; + } + } + [HarmonyPatch(typeof(MushroomDoorSabotageMinigame), nameof(MushroomDoorSabotageMinigame.Begin))] + public static class MushroomDoorSabotageMinigame_Begin_Patch + { + public static bool Prefix(MushroomDoorSabotageMinigame __instance) { if (ElysiumModMenuGUI.autoOpenDoors) { __instance.FixDoorAndCloseMinigame(); return false; } return true; } + } + + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetTasks))] + public static class NoTaskMode_Patch { public static bool Prefix(PlayerControl __instance) { if (ElysiumModMenuGUI.noTaskMode) return false; return true; } } + [HarmonyPatch(typeof(ChatController), nameof(ChatController.SendChat))] + public static class ChatController_SendChat_Patch + { + public static bool Prefix(ChatController __instance) + { + if (__instance.freeChatField == null || __instance.freeChatField.textArea == null) return true; + string text = __instance.freeChatField.textArea.text; + if (string.IsNullOrWhiteSpace(text)) return true; + + if (ElysiumModMenuGUI.enableChatHistory) + { + ChatHistory.Remember(text); + } + + ElysiumModMenuGUI.TrySpellCheckNotify(text); + + string lowerChat = text.ToLower().Trim(); + + if (ElysiumModMenuGUI.enableColorCommand) + { + if (lowerChat == "/rainbow" || lowerChat == "!rainbow" || lowerChat == "/lgbt" || lowerChat == "!lgbt") + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + if (ElysiumModMenuGUI.rainbowPlayers.Contains(PlayerControl.LocalPlayer.PlayerId)) + { + ElysiumModMenuGUI.rainbowPlayers.Remove(PlayerControl.LocalPlayer.PlayerId); + ElysiumModMenuGUI.ShowNotification("[SERVER] Ваша радуга ВЫКЛ."); + } + else + { + ElysiumModMenuGUI.rainbowPlayers.Add(PlayerControl.LocalPlayer.PlayerId); + ElysiumModMenuGUI.ShowNotification("[SERVER] Ваша радуга ВКЛ."); + } + } + else + { + if (HudManager.Instance?.Chat != null) + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Эта команда только для Хоста!"); + } + __instance.freeChatField.textArea.SetText("", ""); + return false; + } + + if (lowerChat.StartsWith("/color ") || lowerChat.StartsWith("/c ") || lowerChat.StartsWith("/col ") || + lowerChat.StartsWith("!color ") || lowerChat.StartsWith("!c ") || lowerChat.StartsWith("!col ")) + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + string arg = lowerChat.Substring(lowerChat.IndexOf(' ') + 1).Trim(); + int colorId = -1; + + if (int.TryParse(arg, out int parsed)) colorId = parsed; + else colorId = ElysiumModMenuGUI.GetColorIdByName(arg); + + if (colorId >= 0 && colorId <= 18 && PlayerControl.LocalPlayer != null) + { + PlayerControl.LocalPlayer.RpcSetColor((byte)colorId); + } + else if (HudManager.Instance?.Chat != null) + { + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Используйте ID (0-18) или названия (красн, син, зел...)"); + } + } + else + { + if (HudManager.Instance?.Chat != null) + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Смена цвета доступна только Хосту!"); + } + __instance.freeChatField.textArea.SetText("", ""); + return false; + } + } + + if (lowerChat.StartsWith("/w ") || lowerChat.StartsWith("/pm ") || + lowerChat.StartsWith("/msg ") || lowerChat.StartsWith("/am ")) + { + string[] parts = text.Split(new char[] { ' ' }, 3); + if (parts.Length >= 3) + { + + string targetInput = parts[1].ToLower().Trim(); + string message = parts[2]; + PlayerControl target = null; + + if (byte.TryParse(targetInput, out byte pid)) + { + target = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p.PlayerId == pid); + } + + if (target == null && PlayerControl.AllPlayerControls != null) + { + PlayerControl exactMatch = null; + PlayerControl partialMatch = null; + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.Data == null || pc.Data.Disconnected || pc == PlayerControl.LocalPlayer) continue; + + string rawName = Regex.Replace(pc.Data.PlayerName, "<.*?>", string.Empty).ToLower().Trim(); + int cId = (int)pc.Data.DefaultOutfit.ColorId; + int targetColorId = ElysiumModMenuGUI.GetColorIdByName(targetInput); + + if (rawName == targetInput || (targetColorId != -1 && cId == targetColorId)) + { + exactMatch = pc; + break; + } + if (rawName.StartsWith(targetInput)) + { + if (partialMatch == null) partialMatch = pc; + } + } + target = exactMatch ?? partialMatch; + } + + if (target != null && target != PlayerControl.LocalPlayer) + { + string safeMessage = Regex.Replace(message, "<.*?>", string.Empty).Replace("<", "").Replace(">", ""); + string networkMsg = $"шепчет вам:\n{safeMessage}"; + + if (AmongUsClient.Instance != null && PlayerControl.LocalPlayer != null) + { + MessageWriter msgWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, 13, Hazel.SendOption.Reliable, target.OwnerId); + msgWriter.Write(networkMsg); + AmongUsClient.Instance.FinishRpcImmediately(msgWriter); + } + + string targetClean = Regex.Replace(target.Data.PlayerName, "<.*?>", string.Empty); + if (HudManager.Instance?.Chat != null) + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, $"Вы шепчете {targetClean}:\n{safeMessage}"); + } + else if (HudManager.Instance?.Chat != null) + { + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Игрок не найден! Введите ID, Цвет или Имя."); + } + } + __instance.freeChatField.textArea.SetText("", ""); + return false; + } + + return true; + } + } + + public static void Postfix(GameStartManager __instance) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; + if (ElysiumModMenuGUI.customStartTimer > 0f) return; + + if (ElysiumModMenuGUI.fakeStartCounterTroll) + { + try + { + sbyte[] arr = { -123, -100, -69, -42, 0, 42, 69, 100, 123 }; + sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; + PlayerControl.LocalPlayer.RpcSetStartCounter((int)b); + __instance.SetStartCounter(b); + } + catch { } + } + else if (ElysiumModMenuGUI.fakeStartCounterCustom && int.TryParse(ElysiumModMenuGUI.fakeStartInput, out int custom)) + { + try + { + PlayerControl.LocalPlayer.RpcSetStartCounter(custom); + __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); + } + catch { } + } + } + } +} + + +[HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] +public static class ChatController_Update_Patch +{ + public static void Postfix(ChatController __instance) + { + try + { + if (!ElysiumModMenuGUI.enableChatDarkMode) return; + + if (__instance.freeChatField != null && __instance.freeChatField.background != null) + { + __instance.freeChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); + if (__instance.freeChatField.textArea != null && __instance.freeChatField.textArea.outputText != null) + __instance.freeChatField.textArea.outputText.color = Color.white; + } + if (__instance.quickChatField != null && __instance.quickChatField.background != null) + { + __instance.quickChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); + if (__instance.quickChatField.text != null) + __instance.quickChatField.text.color = Color.white; + } + } + catch { } + } +} + +[HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetText))] +public static class DarkMode_ChatBubblePatch +{ + public static void Postfix(ChatBubble __instance) + { + try + { + if (!ElysiumModMenuGUI.enableChatDarkMode) return; + + Transform bg = __instance.transform.Find("Background"); + if (bg != null) + { + var sr = bg.GetComponent(); + if (sr != null) sr.color = new Color32(35, 35, 35, 255); + } + if (__instance.TextArea != null) + __instance.TextArea.color = Color.white; + } + catch { } + } +} + +[HarmonyPatch(typeof(GameManager), nameof(GameManager.CheckTaskCompletion))] +public static class GameManager_CheckTaskCompletion_Patch +{ + public static bool Prefix(ref bool __result) + { + try + { + if (!ElysiumModMenuGUI.neverEndGame) return true; + __result = false; return false; + } + catch { return true; } + } +} + +[HarmonyPatch(typeof(ChatController), nameof(ChatController.SetVisible))] +public static class ChatController_SetVisible_Patch +{ + public static void Prefix(ref bool visible) + { + if (ElysiumModMenuGUI.alwaysChat) visible = true; + } +} + +[HarmonyPatch(typeof(MeetingHud), "Update")] +public static class RevealVotesPatch +{ + internal static List _votedPlayers = new List(); + public static void Prefix(MeetingHud __instance) + { + if (!ElysiumModMenuGUI.RevealVotesEnabled) return; + try + { + if ((int)__instance.state >= 4) return; + foreach (var item in __instance.playerStates) + { + if (item == null) continue; + var playerById = GameData.Instance.GetPlayerById(item.TargetPlayerId); + if (playerById == null || playerById.Disconnected || item.VotedFor == PlayerVoteArea.HasNotVoted || + item.VotedFor == PlayerVoteArea.MissedVote || item.VotedFor == PlayerVoteArea.DeadVote || _votedPlayers.Contains(item.TargetPlayerId)) continue; + _votedPlayers.Add(item.TargetPlayerId); + if (item.VotedFor != PlayerVoteArea.SkippedVote) + { + foreach (var item2 in __instance.playerStates) if (item2.TargetPlayerId == item.VotedFor) { __instance.BloopAVoteIcon(playerById, 0, item2.transform); break; } + } + else if (__instance.SkippedVoting != null) __instance.BloopAVoteIcon(playerById, 0, __instance.SkippedVoting.transform); + } + foreach (var item3 in __instance.playerStates) + { + if (item3 == null) continue; + var component = item3.transform.GetComponent(); + if (component != null) foreach (var sprite in component.Votes) sprite.gameObject.SetActive(true); + } + if (__instance.SkippedVoting != null) __instance.SkippedVoting.SetActive(true); + } + catch { } + } +} +[HarmonyPatch(typeof(MeetingHud), "PopulateResults")] +public static class RevealVotesCleanupPatch +{ + public static void Prefix(MeetingHud __instance) + { + if (!ElysiumModMenuGUI.RevealVotesEnabled) return; + try + { + foreach (var item in __instance.playerStates) + { + if (item == null) continue; + var component = item.transform.GetComponent(); + if (component != null && component.Votes.Count != 0) + { + foreach (var sprite in component.Votes) Object.DestroyImmediate(sprite.gameObject); + component.Votes.Clear(); + } + } + RevealVotesPatch._votedPlayers.Clear(); + } + catch { } + } +} + +[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increase))] +public static class NumberOption_Increase_Patch +{ + public static bool Prefix(NumberOption __instance) + { + try + { + if (!ElysiumModMenuGUI.noSettingLimit) return true; + if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && + (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) + return true; + __instance.Value += __instance.Increment; + __instance.UpdateValue(); + __instance.OnValueChanged.Invoke(__instance); + __instance.AdjustButtonsActiveState(); + return false; + } + catch { return true; } + } +} + +[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Decrease))] +public static class NumberOption_Decrease_Patch +{ + public static bool Prefix(NumberOption __instance) + { + try + { + if (!ElysiumModMenuGUI.noSettingLimit) return true; + if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && + (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) + return true; + __instance.Value -= __instance.Increment; + __instance.UpdateValue(); + __instance.OnValueChanged.Invoke(__instance); + __instance.AdjustButtonsActiveState(); + return false; + } + catch { return true; } + } +} + +[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Initialize))] +public static class NumberOption_Initialize_Patch +{ + public static void Postfix(NumberOption __instance) + { + try + { + if (!ElysiumModMenuGUI.noSettingLimit) return; + if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && + (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) + return; + __instance.ValidRange = new FloatRange(-999f, 999f); + } + catch { } + } +} + +[HarmonyPatch(typeof(IGameOptionsExtensions), nameof(IGameOptionsExtensions.GetAdjustedNumImpostors))] +public static class IGameOptionsExtensions_GetAdjustedNumImpostors_Patch +{ + public static bool Prefix(IGameOptions __instance, ref int __result) + { + try + { + if (!ElysiumModMenuGUI.noSettingLimit) return true; + __result = GameOptionsManager.Instance.CurrentGameOptions.NumImpostors; + return false; + } + catch { return true; } + } +} + +[HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.Start))] +public static class ExtendedLobbyListPatch +{ + public static Scroller scroller; + + public static bool Prefix(FindAGameManager __instance) + { + if (!ElysiumModMenuGUI.extendedLobby) return true; + try + { + if (__instance.gameContainers == null || __instance.gameContainers.Count == 0) return true; + if (__instance.gameContainers.Count > 10) return true; + + GameContainer prefab = __instance.gameContainers[0]; + GameObject holder = new GameObject("ExtendedLobbyScroller"); + holder.transform.SetParent(prefab.transform.parent); + + scroller = holder.AddComponent(); + scroller.Inner = holder.transform; + scroller.MouseMustBeOverToScroll = true; + scroller.allowY = true; + scroller.ScrollWheelSpeed = 0.4f; + scroller.SetYBoundsMin(0f); + scroller.SetYBoundsMax(4f); + + BoxCollider2D collider = prefab.transform.parent.gameObject.AddComponent(); + collider.size = new Vector2(100f, 100f); + scroller.ClickMask = collider; + + var list = new System.Collections.Generic.List(); + foreach (var gc in __instance.gameContainers) + { + gc.transform.SetParent(holder.transform); + gc.transform.localPosition = new Vector3(gc.transform.localPosition.x, gc.transform.localPosition.y, 25f); + list.Add(gc); + } + + for (int i = 0; i < 15; i++) + { + GameContainer newGc = UnityEngine.Object.Instantiate(prefab, holder.transform); + newGc.transform.localPosition = new Vector3(newGc.transform.localPosition.x, newGc.transform.localPosition.y - 0.75f * list.Count, 25f); + list.Add(newGc); + } + + __instance.gameContainers = new Il2CppReferenceArray(list.ToArray()); + return true; + } + catch { return true; } + } +} + +[HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.RefreshList))] +public static class ExtendedLobbyRefreshPatch +{ + public static void Postfix() + { + try { if (ElysiumModMenuGUI.extendedLobby && ExtendedLobbyListPatch.scroller != null) ExtendedLobbyListPatch.scroller.ScrollRelative(new Vector2(0f, -100f)); } catch { } + } +} + + +[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.FixedUpdate))] +public static class InvertControls_Patch +{ + private static void SeePlayerVent(PlayerPhysics player) + { +#pragma warning disable CS8632 + if (GameManager.Instance.IsHideAndSeek() && player.myPlayer.Data.RoleType == RoleTypes.Impostor || player == null || + AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started) + return; + if (!SeePlayersInVent) + { + if (player.myPlayer.invisibilityAlpha == 0.3f) + { + PhantomRole? role = player.myPlayer.Data.Role as PhantomRole; + if (role != null) + { + player.myPlayer.SetInvisibility(role.isInvisible); + return; + } + else + { + player.myPlayer.cosmetics.SetPhantomRoleAlpha(1f); + player.myPlayer.invisibilityAlpha = 1; + if (player.myPlayer.inVent) + { + player.myPlayer.Visible = false; + } + } + } + return; + } + + if (player.myPlayer.inVent && player.NetId != PlayerControl.LocalPlayer.MyPhysics.NetId) + { + player.myPlayer.Visible = true; + player.myPlayer.invisibilityAlpha = 0.3f; + player.myPlayer.cosmetics.SetPhantomRoleAlpha(0.3f); + } + else + { + PhantomRole? role = player.myPlayer.Data.Role as PhantomRole; + if (role != null) + { + player.myPlayer.SetInvisibility(role.isInvisible); + } + else + { + player.myPlayer.cosmetics.SetPhantomRoleAlpha(1f); + player.myPlayer.invisibilityAlpha = 1; + } + } + } + + public static void Postfix(PlayerPhysics __instance) + { + if (__instance.AmOwner && ElysiumModMenuGUI.invertControls && __instance.body != null) + { + __instance.body.velocity = -__instance.body.velocity; + } + + SeePlayerVent(__instance); + } +} +[HarmonyPatch(typeof(LobbyBehaviour), nameof(LobbyBehaviour.Start))] +public static class LobbyStart_ApplyLevelSpoof +{ + public static void Postfix() + { + if (!ElysiumModMenuGUI.isEditingLevel && uint.TryParse(ElysiumModMenuGUI.spoofLevelString, out uint parsedLvl)) + { + uint targetLevel = parsedLvl > 0 ? parsedLvl - 1 : 0; + try { AmongUs.Data.DataManager.Player.stats.level = targetLevel; } + catch { try { AmongUs.Data.DataManager.Player.Stats.Level = targetLevel; } catch { } } + AmongUs.Data.DataManager.Player.Save(); + } + } +} + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] +public static class RPCSniffer_Patch +{ + private static readonly HashSet VanillaRPCs = new HashSet + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 29, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 60, 61, 62, 63, 64, 65 + }; + + private static readonly Dictionary KnownMods = new Dictionary + { + { 157, ("RockStar", "#800000") }, + { 121, ("RockStar / Chocoo", "#800000") }, + { 167, ("TuffMenu", "#008000") }, + { 164, ("Hydra / Sicko", "#FF0000") }, + { 176, ("HostGuard / TOH", "#008000") }, + { 195, ("Polar Client", "#FFFF00") }, + { 204, ("Polar Client", "#FFFF00") }, + { 154, ("GNC", "#FF0000") }, + { 85, ("KillNet (Base)", "#FF0000") }, + { 150, ("KillNet (V2)", "#FF0000") }, + { 162, ("KNM", "#FF0000") }, + { 250, ("KillNet (Alt)", "#FF0000") }, + { 212, ("BanMod", "#008000") }, + { 213, ("BanMod", "#008000") }, + { 214, ("BanMod", "#008000") }, + { 215, ("BanMod", "#008000") }, + { 216, ("BanMod", "#008000") }, + { 217, ("BanMod", "#008000") }, + { 218, ("BanMod", "#008000") }, + { 219, ("BanMod", "#008000") }, + { 144, ("Gaff Menu", "#FF0000") }, + { 145, ("Gaff Menu", "#FF0000") }, + { 188, ("GMM", "#FF0000") }, + { 189, ("GMM", "#FF0000") }, + { 169, ("Malum", "#FF0000") }, + { 210, ("Eclipse", "#FFFF00") }, + { 173, ("Private", "#FF0000") }, + { 151, ("Better Among Us", "#008000") }, + { 152, ("Better Among Us", "#008000") }, + { 255, ("CrewMod", "#FFFF00") }, + { 111, ("AUM (BitCrackers)", "#FF0000") }, + { 231, ("SentinelAU", "#FF0000") }, + { 133, ("Lunar / ElysiumModMenu", "#00FFFF") }, + { 89, ("ElysiumModMenu Old", "#008000") } + }; + + public static bool Prefix(PlayerControl __instance, byte callId, MessageReader reader) + { + if (__instance == null) return true; + + + if (PlayerControl.LocalPlayer != null && __instance == PlayerControl.LocalPlayer) return true; + + if (ElysiumModMenuGUI.LogAllRPCs) + { + + if (!VanillaRPCs.Contains(callId)) + { + string pNameSniff = (__instance.Data != null && !string.IsNullOrEmpty(__instance.Data.PlayerName)) ? __instance.Data.PlayerName : $"Player_{__instance.PlayerId}"; + + + if (KnownMods.TryGetValue(callId, out var modInfo)) + { + ElysiumModMenuGUI.ShowNotification($"[СНИФФЕР] {pNameSniff}: {modInfo.Name} ({callId})"); + } + else + { + ElysiumModMenuGUI.ShowNotification($"[СНИФФЕР] {pNameSniff} кинул неизвестный RPC: {callId}"); + } + } + } + return true; + } +} + +[HarmonyPatch(typeof(HatManager), nameof(HatManager.Initialize))] +public static class UnlockCosmetics_HatManager_Initialize_Postfix +{ + public static void Postfix(HatManager __instance) + { + if (!ElysiumModMenuGUI.unlockCosmetics) return; + + foreach (var bundle in __instance.allBundles) bundle.Free = true; + foreach (var hat in __instance.allHats) hat.Free = true; + foreach (var nameplate in __instance.allNamePlates) nameplate.Free = true; + foreach (var pet in __instance.allPets) pet.Free = true; + foreach (var skin in __instance.allSkins) skin.Free = true; + foreach (var visor in __instance.allVisors) visor.Free = true; + foreach (var starBundle in __instance.allStarBundles) starBundle.price = 0; + } +} + +[HarmonyPatch(typeof(PlayerPurchasesData), nameof(PlayerPurchasesData.GetPurchase))] +public static class UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix +{ + public static bool Prefix(ref bool __result) + { + if (!ElysiumModMenuGUI.unlockCosmetics) return true; + __result = true; + return false; + } +} +[HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.Start))] +public static class AutoChatEveryone_Start_Patch +{ + public static void Postfix() + { + ElysiumModMenuGUI.InitializeKillCooldownOnRoundStart(); + + if (ElysiumModMenuGUI.autoChatEveryone && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + ElysiumModMenuGUI.pendingAutoMeeting = true; + ElysiumModMenuGUI.autoMeetingTimer = 0f; + } + } +} +[HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] +public static class ChatController_AddChat_Patch +{ + public static bool Prefix(PlayerControl sourcePlayer, ref string chatText, bool censor, ChatController __instance) + { + if (string.IsNullOrEmpty(chatText)) return true; + string lowerText = chatText.ToLower().Trim(); + + if (ElysiumModMenuGUI.enableColorCommand && sourcePlayer != null) + { + string[] colorCommands = { "/color ", "!color ", "/col ", "!col ", "/c ", "!c " }; + string usedCmd = colorCommands.FirstOrDefault(cmd => lowerText.StartsWith(cmd)); + + if (usedCmd != null) + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + string colorInput = lowerText.Substring(usedCmd.Length).Trim(); + int colorId = -1; + + if (int.TryParse(colorInput, out int parsedId)) { if (parsedId >= 0 && parsedId <= 18) colorId = parsedId; } + else colorId = ElysiumModMenuGUI.GetColorIdByName(colorInput); + + if (colorId != -1) + { + if (colorId == 18 && ElysiumModMenuGUI.blockFortegreenChat) + { + if (HudManager.Instance?.Chat != null) + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Цвет Fortegreen запрещен хостом!"); + } + else + { + sourcePlayer.RpcSetColor((byte)colorId); + } + } + else if (sourcePlayer == PlayerControl.LocalPlayer) + { + __instance.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Неверный цвет."); + } + } + return false; + } + + if (lowerText == "/rainbow" || lowerText == "!rainbow" || lowerText == "/lgbt" || lowerText == "!lgbt") + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + if (ElysiumModMenuGUI.blockRainbowChat) + { + if (HudManager.Instance?.Chat != null) + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Радуга запрещена хостом!"); + } + else + { + if (ElysiumModMenuGUI.rainbowPlayers.Contains(sourcePlayer.PlayerId)) + { + ElysiumModMenuGUI.rainbowPlayers.Remove(sourcePlayer.PlayerId); + ElysiumModMenuGUI.ShowNotification("[SERVER] Радуга ВЫКЛ."); + } + else + { + ElysiumModMenuGUI.rainbowPlayers.Add(sourcePlayer.PlayerId); + ElysiumModMenuGUI.ShowNotification("[SERVER] Радуга ВКЛ."); + } + } + } + return false; + } + } + + if (ShouldShowGhostMessage(sourcePlayer)) + { + return ShowGhostMessage(sourcePlayer, chatText, censor, __instance); + } + + return true; + } + + private static bool ShouldShowGhostMessage(PlayerControl sourcePlayer) + { + try + { + if (!ElysiumModMenuGUI.readGhostChat && !ElysiumModMenuGUI.seeGhosts) return false; + if (sourcePlayer == null || sourcePlayer.Data == null) return false; + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) return false; + if (PlayerControl.LocalPlayer.Data.IsDead) return false; + + return sourcePlayer.Data.IsDead; + } + catch { return false; } + } + + private static bool ShowGhostMessage(PlayerControl sourcePlayer, string chatText, bool censor, ChatController chat) + { + if (chat == null) return true; + + ChatBubble pooledBubble = null; + try + { + NetworkedPlayerInfo sourceData = sourcePlayer.Data; + if (sourceData == null) return true; + + pooledBubble = chat.GetPooledBubble(); + pooledBubble.transform.SetParent(chat.scroller.Inner); + pooledBubble.transform.localScale = Vector3.one; + + bool isLocal = sourcePlayer == PlayerControl.LocalPlayer; + if (isLocal) pooledBubble.SetRight(); + else pooledBubble.SetLeft(); + + bool didVote = MeetingHud.Instance != null && MeetingHud.Instance.DidVote(sourcePlayer.PlayerId); + pooledBubble.SetCosmetics(sourceData); + chat.SetChatBubbleName(pooledBubble, sourceData, sourceData.IsDead, didVote, PlayerNameColor.Get(sourceData), null); + + if (censor && AmongUs.Data.DataManager.Settings.Multiplayer.CensorChat) + { + chatText = BlockedWords.CensorWords(chatText, false); + } + + pooledBubble.SetText($"{chatText}"); + pooledBubble.AlignChildren(); + chat.AlignAllBubbles(); + + if (!chat.IsOpenOrOpening && chat.notificationRoutine == null) + { + chat.notificationRoutine = chat.StartCoroutine(chat.BounceDot()); + } + + if (!isLocal && !chat.IsOpenOrOpening) + { + SoundManager.Instance.PlaySound(chat.messageSound, false).pitch = 0.5f + sourcePlayer.PlayerId / 15f; + chat.chatNotification.SetUp(sourcePlayer, chatText); + } + + return false; + } + catch + { + try + { + if (pooledBubble != null) chat.chatBubblePool.Reclaim(pooledBubble); + } + catch { } + return true; + } + } + + + + public static void Postfix(GameStartManager __instance) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; + if (ElysiumModMenuGUI.customStartTimer > 0f) return; + + if (ElysiumModMenuGUI.fakeStartCounterTroll) + { + try + { + sbyte[] arr = { -123, -100, -69, -42, 0, 42, 69, 100, 123 }; + sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; + PlayerControl.LocalPlayer.RpcSetStartCounter((int)b); + __instance.SetStartCounter(b); + } + catch { } + } + else if (ElysiumModMenuGUI.fakeStartCounterCustom && int.TryParse(ElysiumModMenuGUI.fakeStartInput, out int custom)) + { + try + { + PlayerControl.LocalPlayer.RpcSetStartCounter(custom); + __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); + } + catch { } + } + } +} + +[HarmonyPatch(typeof(GameContainer), nameof(GameContainer.SetupGameInfo))] +public static class MoreLobbyInfo_GameContainer_SetupGameInfo_Postfix +{ + public static void Postfix(GameContainer __instance) + { + if (!ElysiumModMenuGUI.moreLobbyInfo) return; + + var trueHostName = __instance.gameListing.TrueHostName; + const string separator = "<#0000>000000000000000"; + var age = __instance.gameListing.Age; + var lobbyTime = $"Age: {age / 60}:{(age % 60 < 10 ? "0" : "")}{age % 60}"; + + + int platId = (int)__instance.gameListing.Platform; + string platformStr = platId switch + { + 1 => "Epic", + 2 => "Steam", + 3 => "Mac", + 4 => "Microsoft Store", + 5 => "Itch.io", + 6 => "iOS", + 7 => "Android", + 8 => "Nintendo Switch", + 9 => "Xbox", + 10 => "PlayStation", + 112 => "Starlight", + _ => "Unknown" + }; + + string hexColor = ColorUtility.ToHtmlStringRGB(ElysiumModMenuGUI.currentAccentColor); + + __instance.capacity.text = $"{separator}\n{trueHostName}\n{__instance.capacity.text}\n" + + $"{GameCode.IntToGameName(__instance.gameListing.GameId)}\n" + + $"{platformStr}\n{lobbyTime}\n{separator}"; + } +} + +[HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.HandleList))] +public static class MoreLobbyInfo_FindAGameManager_HandleList_Postfix +{ + public static void Postfix(HttpMatchmakerManager.FindGamesListFilteredResponse response, FindAGameManager __instance) + { + if (!ElysiumModMenuGUI.moreLobbyInfo) return; + + __instance.TotalText.text = response.Metadata.AllGamesCount.ToString(); + } +} +[HarmonyPatch(typeof(PlatformSpecificData), nameof(PlatformSpecificData.Serialize))] +public static class PlatformSpooferPatch +{ + public static void Prefix(PlatformSpecificData __instance) + { + try + { + if (__instance != null) + { + if (ElysiumModMenuGUI.enablePlatformSpoof) + { + __instance.Platform = ElysiumModMenuGUI.platformValues[ElysiumModMenuGUI.currentPlatformIndex]; + } + __instance.PlatformName = "ElysiumModMenu by Meowchelo (and one silly guy :p) https://github.com/meowchelo/ElysiumModMenu"; + } + } + catch { } + } +} + +[HarmonyPatch(typeof(NetworkedPlayerInfo), nameof(NetworkedPlayerInfo.Serialize))] +public static class FriendCodeSpooferPatch +{ + private static string serializeRestoreValue = null; + + public static void Prefix(NetworkedPlayerInfo __instance) + { + try + { + serializeRestoreValue = null; + if (ElysiumModMenuGUI.PrepareLocalFriendCodeForSerialize(__instance, out serializeRestoreValue)) return; + if (!ElysiumModMenuGUI.enableFriendCodeSpoof) return; + if (__instance == null || PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) return; + if (__instance.PlayerId != PlayerControl.LocalPlayer.PlayerId) return; + + string input = ElysiumModMenuGUI.spoofFriendCodeInput ?? ""; + string clean = ""; + foreach (char c in input.ToLowerInvariant()) + { + if (char.IsWhiteSpace(c)) break; + if (char.IsLetterOrDigit(c)) clean += c; + if (clean.Length >= 10) break; + } + + if (string.IsNullOrWhiteSpace(clean)) return; + __instance.FriendCode = clean; + } + catch { } + } + + public static void Postfix(NetworkedPlayerInfo __instance) + { + ElysiumModMenuGUI.RestoreLocalFriendCodeAfterSerialize(__instance, serializeRestoreValue); + serializeRestoreValue = null; + } +} +[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.KickPlayer))] +public static class AmongUsClient_KickPlayer_BanList_Patch +{ + public static void Prefix(InnerNetClient __instance, int clientId, bool ban) + { + if (ban && PlayerControl.AllPlayerControls != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + try + { + var pc = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p.OwnerId == clientId); + if (pc != null && pc.Data != null) + { + string fc = string.IsNullOrEmpty(pc.Data.FriendCode) ? "Unknown" : pc.Data.FriendCode; + string name = pc.Data.PlayerName ?? "Unknown"; + string puid = "Unknown"; + + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(pc); + if (client != null) puid = client.Id.ToString(); + } + catch { } + + ElysiumModMenuGUI.AddToBanList(fc, puid, name, "Host ban"); + ElysiumModMenuGUI.ShowNotification($"[BAN] {name} занесен в черный список!"); + } + } + catch { } + } + } +} diff --git a/NjordMenu.csproj b/ElysiumModMenu.csproj similarity index 65% rename from NjordMenu.csproj rename to ElysiumModMenu.csproj index 3f77187..98896a1 100644 --- a/NjordMenu.csproj +++ b/ElysiumModMenu.csproj @@ -1,15 +1,14 @@ - - + net6.0 enable disable true latest - + ElysiumModMenu + ElysiumModMenu C:\Program Files (x86)\Steam\steamapps\common\Among Us - $(AmongUsDir)\BepInEx\core\BepInEx.Core.dll @@ -21,38 +20,43 @@ $(AmongUsDir)\BepInEx\core\0Harmony.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Hazel.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Hazel.dll $(AmongUsDir)\BepInEx\core\Il2CppInterop.Runtime.dll - $(AmongUsDir)\BepInEx\interop\Assembly-CSharp.dll $(AmongUsDir)\BepInEx\interop\Il2Cppmscorlib.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Addressables.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.ResourceManager.dll + - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.TextMeshPro.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.TextMeshPro.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AudioModule.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AudioModule.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.CoreModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ImageConversionModule.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ImageConversionModule.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.IMGUIModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.Physics2DModule.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.Physics2DModule.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.TextRenderingModule.dll @@ -60,10 +64,8 @@ $(AmongUsDir)\BepInEx\interop\UnityEngine.InputLegacyModule.dll - $(AmongUsDir)\BepInEx\interop\UnityEngine.UI.dll - - \ No newline at end of file + diff --git a/ElysiumModMenu.slnx b/ElysiumModMenu.slnx new file mode 100644 index 0000000..ccf3de0 --- /dev/null +++ b/ElysiumModMenu.slnx @@ -0,0 +1,3 @@ + + + diff --git a/README.md b/README.md index e5e2cf0..586089f 100644 --- a/README.md +++ b/README.md @@ -1 +1,158 @@ -# ClassLibrary10 \ No newline at end of file +# ElysiumModMenu + +Advanced BepInEx IL2CPP mod menu for Among Us with host tools, anti-cheat utilities, visual ESP, account spoofing, local-only identity tweaks, chat quality-of-life features, and lobby automation. + +> [!CAUTION] +> ElysiumModMenu contains powerful host and network tools. Use it responsibly, preferably in private lobbies with consenting players. This project is not affiliated with Innersloth. + +## Release + +| Version | Status | Download | +| :--- | :--- | :--- | +| v1.3.0 | Latest | [Download ElysiumModMenu.dll](https://github.com/meowchelo/ElysiumModMenu/releases/latest) | + +## Highlights + +- Clean IMGUI menu with custom themes, RGB accent mode, background image support, custom keybinds, and clipboard-friendly text fields. +- Local name spoofing with no RPC broadcast. Supports plain text, rich text, `shimmer:Name`, and quick hex color syntax like `#68B6E7Name`. +- Local fake Friend Code spoofing for your own client UI, with any symbols or text. The local fake FC is restored before network serialization unless real FC spoof is enabled. +- Real Friend Code spoof option for serialized player info, still sanitized to guest-style codes. +- ESP player info line: `Host - Lv:X - Platform spf - FriendCode`. The `spf` marker appears when platform spoofing is active for the local player. +- Menu text fields are clipped so long values do not squeeze or break the menu layout. +- Paste/copy support across menu input fields with `Ctrl+V`, `Shift+Insert`, `Ctrl+C`, and `Ctrl+X`. + +## Features + +### Account And Local Spoofing + +- Fake level spoof. +- Platform spoof with selectable platform. +- Local name spoof that only changes your own client view. +- Local fake Friend Code for player info, history, join notifications, and local UI. +- Network Friend Code spoof for outgoing serialized data. +- Unlock cosmetics and guest account name features. + +### Visuals And ESP + +- Show ghosts. +- Reveal player roles above names. +- Show player info above names: host flag, level, platform, platform spoof marker, and Friend Code. +- Reveal meeting roles and votes. +- See players inside vents. +- Full Bright mode. +- Tracers. +- Freecam and camera zoom. +- Always show chat and read ghost chat. + +### Anti-Cheat And Protections + +- RPC protection toggles for spoof RPCs, sabotage/meeting abuse, game RPCs in lobby, chat floods, and meeting floods. +- Mod/RPC sniffer with known menu IDs. +- Pet spam local drop and optional host auto-ban. +- Anti vote-kick protection. +- Fortegreen and broken Friend Code checks. +- Persistent ban list stored locally. + +### Host And Lobby Tools + +- Auto-host system with minimum players, start delay, fast-start threshold, load wait, auto-return, and force-start controls. +- Pre-game role manager. +- Force impostors and roles before the game starts. +- Kill, kick, report, eject, revive, morph, mass morph, and task tools. +- Spawn/despawn lobby. +- Instant start and smart end-game actions. +- No task mode and no setting limits. + +### Sabotage And Door Tools + +- Trigger reactor, O2, comms, and lights sabotage. +- Fix all sabotages. +- Close, open, lock, or unlock all doors. +- Per-room door controls. + +### Chat System + +- Extended chat length. +- Fast chat. +- Links, email, and symbol support. +- Chat history navigation. +- Clipboard support in game chat text boxes. +- Local chat log saved to `ChatLog.txt`. +- Whisper/private message commands. +- Color command support. +- Host filters for rainbow/Fortegreen abuse. + +## Text Input Notes + +ElysiumModMenu has custom menu inputs instead of default text boxes. Long values are clipped visually and keep editing normally without resizing the menu. + +Supported shortcuts: + +- `Ctrl+V` or `Shift+Insert`: paste. +- `Ctrl+C`: copy current field text. +- `Ctrl+X`: cut current field text. +- `Backspace`: delete last character. +- `Esc`: stop editing. + +Local name examples: + +```text +shimmer:Elysium +#68B6E7BlueName +RichName +``` + +## Installation + +ElysiumModMenu is a BepInEx IL2CPP plugin. BepInEx must be installed first. + +1. Download BepInEx IL2CPP from the official BepInEx releases. +2. Extract BepInEx into the Among Us game folder, next to `Among Us.exe`. +3. Run the game once so BepInEx can generate its folders. +4. Close the game. +5. Download `ElysiumModMenu.dll` from releases. +6. Put `ElysiumModMenu.dll` into `Among Us/BepInEx/plugins`. +7. Launch Among Us. + +Default menu toggle: `Insert` or `Right Shift`. + +Custom background: + +```text +Among Us/BepInEx/config/ElysiumModMenu/MenuBG.png +``` + +## Game Folder Help + +- Steam: right-click Among Us in Library, then `Manage`, then `Browse local files`. +- Epic Games: open game options, then manage/open install folder. +- Itch.io: right-click Among Us, then `Manage`, then `Open folder in Explorer`. +- Xbox app: game settings, then `Manage`, then `Files`, then browse. + +## Build + +The project targets `.NET 6` and uses the Among Us BepInEx/interop assemblies. + +```powershell +dotnet build .\NjordMenu.csproj +``` + +Output: + +```text +bin/Debug/net6.0/ElysiumModMenu.dll +``` + +## Screenshots + +Anti-cheat and protections + +Host and lobby controls + +Visuals and ESP + +## Disclaimer + +This mod is not affiliated with, endorsed by, sponsored by, or approved by Innersloth LLC. Among Us and related assets belong to Innersloth LLC. + +Use ElysiumModMenu at your own risk. Misuse may disrupt other players and may result in moderation action from game services. Support is not provided for malicious use. From eecf90b3622bf815ac50d0a806bc426140676a2a Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 00:08:18 +0200 Subject: [PATCH 02/39] Add dark chat disable button --- ElysiumModMenu.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index e7d85fb..dfd1bc4 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -707,6 +707,11 @@ private void DrawChatSettingsTab() enableChatLog = DrawToggle(enableChatLog, L("Save Chat Log to File", "Сохранять лог чата в файл"), 280); GUILayout.Space(2); enableChatDarkMode = DrawToggle(enableChatDarkMode, L("Dark Chat Theme", "Темная тема чата"), 280); + if (enableChatDarkMode && GUILayout.Button(L("Turn Off Dark Chat", "Выключить темный чат"), btnStyle, GUILayout.Width(180), GUILayout.Height(24))) + { + enableChatDarkMode = false; + SaveConfig(); + } GUILayout.Space(8); @@ -3967,6 +3972,12 @@ private void DrawChatSettingsCompact() GUILayout.Space(3); enableChatDarkMode = DrawToggle(enableChatDarkMode, L("Dark Chat Theme", "Темная тема чата"), 230); GUILayout.Space(3); + if (enableChatDarkMode && GUILayout.Button(L("Turn Off Dark Chat", "Выключить темный чат"), btnStyle, GUILayout.Height(24))) + { + enableChatDarkMode = false; + SaveConfig(); + } + GUILayout.Space(3); enableColorCommand = DrawToggle(enableColorCommand, L("Enable /color", "Разрешить /color"), 230); GUILayout.Space(3); blockFortegreenChat = DrawToggle(blockFortegreenChat, L("Block Fortegreen", "Блок Fortegreen"), 230); From 3509661017a32e52be911318d5a1e8b537764145 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 00:08:54 +0200 Subject: [PATCH 03/39] Fix keybind button labels --- ElysiumModMenu.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index dfd1bc4..bc87bd9 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -3522,7 +3522,8 @@ private void DrawBindsTab() GUILayout.BeginVertical(boxStyle); try { - GUILayout.Label("?? CUSTOM KEYBINDS", headerStyle); + GUILayout.Label("CUSTOM KEYBINDS", headerStyle); + GUILayout.Label(L("Menu toggle is locked to Insert.", "Меню открывается только на Insert."), safeLineStyle); GUILayout.Space(10); DrawKeybindRow("Magnet Cursor:", ref bindMagnetCursor, ref isWaitBindMagnetCursor); @@ -3875,7 +3876,7 @@ private void DrawPlayerMovementCompact() GUILayout.Space(3); dragToCursor = DrawToggle(dragToCursor, "Drag To Cursor", 230); GUILayout.Space(3); - autoFollowCursor = DrawToggle(autoFollowCursor, "Magnet Cursor (F9)", 230); + autoFollowCursor = DrawToggle(autoFollowCursor, $"Magnet Cursor ({bindMagnetCursor})", 230); GUILayout.Space(3); noClip = DrawToggle(noClip, "True NoClip", 230); @@ -4116,7 +4117,7 @@ private void DrawPlayerMovement() GUILayout.BeginHorizontal(); try { - autoFollowCursor = DrawToggle(autoFollowCursor, "Magnet Cursor (F9)", 160); + autoFollowCursor = DrawToggle(autoFollowCursor, $"Magnet Cursor ({bindMagnetCursor})", 160); noClip = DrawToggle(noClip, "True NoClip", 160); GUILayout.FlexibleSpace(); } From 6bebb2137af7a99f8b0a9ab914ed96d303038a7b Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 00:14:15 +0200 Subject: [PATCH 04/39] Bump version to 1.3.1 --- ElysiumModMenu.cs | 6 +++--- ElysiumModMenu.csproj | 3 +++ README.md | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index bc87bd9..ba73cb1 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -35,7 +35,7 @@ using System.Runtime.CompilerServices; namespace ElysiumModMenu { - [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.0")] + [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.1")] public class Plugin : BasePlugin { public static Plugin Instance { get; private set; } = null!; @@ -3682,7 +3682,7 @@ private void DrawGeneralInfoTab() string contributorHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(109, 138, 255, 255)) : new Color32(109, 138, 255, 255)); string dangerHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(231, 76, 60, 255)) : new Color32(231, 76, 60, 255)); string safeHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(57, 255, 20, 255)) : new Color32(57, 255, 20, 255)); - string versionText = "1.3.0"; + string versionText = "1.3.1"; GUIStyle textStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 12 }; textStyle.normal.textColor = whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : new Color(0.85f, 0.85f, 0.85f, 1f); @@ -7395,7 +7395,7 @@ public static void Postfix(PingTracker __instance) if (ElysiumModMenuGUI.showWatermark) { - string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.0"); + string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.1"); finalString = $"{shimmerTitle} • " + finalString; } diff --git a/ElysiumModMenu.csproj b/ElysiumModMenu.csproj index 98896a1..b77954a 100644 --- a/ElysiumModMenu.csproj +++ b/ElysiumModMenu.csproj @@ -7,6 +7,9 @@ latest ElysiumModMenu ElysiumModMenu + 1.3.1 + 1.3.1.0 + 1.3.1.0 C:\Program Files (x86)\Steam\steamapps\common\Among Us diff --git a/README.md b/README.md index 586089f..5bec5df 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Advanced BepInEx IL2CPP mod menu for Among Us with host tools, anti-cheat utilit | Version | Status | Download | | :--- | :--- | :--- | -| v1.3.0 | Latest | [Download ElysiumModMenu.dll](https://github.com/meowchelo/ElysiumModMenu/releases/latest) | +| v1.3.1 | Latest | [Download ElysiumModMenu.dll](https://github.com/meowchelo/ElysiumModMenu/releases/latest) | ## Highlights From 5be509a97937ae34c47cfa34d79136664a3769de Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 00:45:20 +0200 Subject: [PATCH 05/39] Fix chat extra characters and auto meeting delay label --- ElysiumModMenu.cs | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index ba73cb1..b7f848a 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -289,10 +289,9 @@ public static class AllowSymbols_TextBoxTMP_Start_Patch { public static void Postfix(TextBoxTMP __instance) { - __instance.allowAllCharacters = true; - __instance.AllowSymbols = true; - - __instance.AllowEmail = true; + __instance.allowAllCharacters = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.AllowSymbols = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.AllowEmail = ElysiumModMenuGUI.allowLinksAndSymbols; } } [HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] @@ -307,9 +306,9 @@ public static void Postfix(ChatController __instance) __instance.timeSinceLastMessage = 0.9f; } - __instance.freeChatField.textArea.allowAllCharacters = true; - __instance.freeChatField.textArea.AllowSymbols = true; - __instance.freeChatField.textArea.AllowEmail = true; + __instance.freeChatField.textArea.allowAllCharacters = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.freeChatField.textArea.AllowSymbols = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.freeChatField.textArea.AllowEmail = ElysiumModMenuGUI.allowLinksAndSymbols; __instance.freeChatField.textArea.characterLimit = ElysiumModMenuGUI.enableExtendedChat ? 120 : 100; } @@ -692,7 +691,7 @@ private void DrawChatSettingsTab() GUILayout.Space(2); enableFastChat = DrawToggle(enableFastChat, L("Fast Chat (3.1 to 2.1", "Быстрый чат (c 3.1 до 2.1)"), 280); GUILayout.Space(2); - allowLinksAndSymbols = DrawToggle(allowLinksAndSymbols, L("Allow Links & Symbols", "Разрешить ссылки и символы"), 280); + allowLinksAndSymbols = DrawToggle(allowLinksAndSymbols, L("Unlock Extra Characters", "Разрешить все символы"), 280); GUILayout.Space(2); enableSpellCheck = DrawToggle(enableSpellCheck, L("Spell Check (Basic)", "Проверка орфографии (Базовая)"), 280); GUILayout.EndVertical(); @@ -3959,7 +3958,7 @@ private void DrawChatSettingsCompact() GUILayout.Space(3); enableFastChat = DrawToggle(enableFastChat, L("Fast Chat", "Быстрый чат"), 230); GUILayout.Space(3); - allowLinksAndSymbols = DrawToggle(allowLinksAndSymbols, L("Links & Symbols", "Ссылки и символы"), 230); + allowLinksAndSymbols = DrawToggle(allowLinksAndSymbols, L("Unlock Extra Characters", "Все символы"), 230); GUILayout.Space(3); enableSpellCheck = DrawToggle(enableSpellCheck, L("Spell Check", "Проверка орфографии"), 230); @@ -6932,6 +6931,13 @@ public static bool IsCharAllowed(TextBoxTMP box, ref bool result) { if (box == null) return true; + string compositionString = Input.compositionString; + if (!string.IsNullOrEmpty(compositionString)) + { + result = true; + return false; + } + string input = isPastingChatInput ? GUIUtility.systemCopyBuffer : Input.inputString; if (string.IsNullOrEmpty(input)) return true; @@ -6943,9 +6949,10 @@ public static bool IsCharAllowed(TextBoxTMP box, ref bool result) char currentChar = text[currentPasteCharPos]; currentPasteCharPos = currentPasteCharPos >= text.Length - 1 ? 0 : currentPasteCharPos + 1; - if (enableClipboard || allowLinksAndSymbols) + if (allowLinksAndSymbols) { - result = currentChar != '\b' && currentChar != '\r' && currentChar != '\n'; + HashSet blockedSymbols = new HashSet { '\b', '\r', '\n', '>', '<', '[' }; + result = !blockedSymbols.Contains(currentChar); return false; } @@ -6959,9 +6966,9 @@ public static class AllowSymbols_TextBoxTMP_Update_Patch public static void Postfix(TextBoxTMP __instance) { if (__instance == null) return; - __instance.allowAllCharacters = true; - __instance.AllowSymbols = true; - __instance.AllowEmail = true; + __instance.allowAllCharacters = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.AllowSymbols = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.AllowEmail = ElysiumModMenuGUI.allowLinksAndSymbols; } } @@ -7202,6 +7209,7 @@ private void DrawLobbyControls() if (autoChatEveryone) { GUILayout.BeginHorizontal(); + GUILayout.Label($"Delay: {autoChatEveryoneDelay:0.0}s", toggleLabelStyle, GUILayout.Width(95)); autoChatEveryoneDelay = GUILayout.HorizontalSlider(autoChatEveryoneDelay, 0f, 10f, sliderStyle, sliderThumbStyle, GUILayout.Width(240)); GUILayout.EndHorizontal(); } From bc17ed42424119eb680cfbfd08e9cf013bb7340f Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 11:05:00 +0200 Subject: [PATCH 06/39] Add map cooldown bypass and neutral RPC labels --- ElysiumModMenu.cs | 149 +++++++++++++++++++++++++++++++++------------- 1 file changed, 108 insertions(+), 41 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index b7f848a..e629ea9 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -101,7 +101,7 @@ public override void Load() } public class ElysiumModMenuGUI : MonoBehaviour { - public static string[] spoofMenuNames = { "ElysiumModMenu", "HostGuard/TOH", "Polar", "BanMod", "Better Among Us", "Sicko Menu", "GNC", "KillNetwork (V1)", "KillNetwork (V2)", "KNM" }; + public static string[] spoofMenuNames = { "Local Client", "Anti Cheat", "Lobby Tool", "Protection Tool", "Utility Client", "Network Tool", "RPC Tool", "Legacy Tool", "Alt Client", "Private Client" }; public static byte[] spoofMenuRPCs = { 89, 176, 204, 212, 151, 164, 154, 85, 150, 162 }; public static float rpcSpoofDelay = 4f; @@ -2184,7 +2184,7 @@ private static string CleanName(string value) public static byte currentColorId = 0; private Vector2 playerListScrollPos = Vector2.zero; private Vector2 playerActionScrollPos = Vector2.zero; - private byte selectedHydraPlayerId = 255; + private byte selectedAntiCheatPlayerId = 255; public static string spoofLevelString = "100"; public static string customNameInput = "хыхых"; @@ -2307,7 +2307,7 @@ public static void RemoveFromBanList(string entry) public static bool killReach = false, killAnyone = false; public static bool endlessSsDuration = false, noVitalsCooldown = false; - public static bool endlessBattery = false, endlessVentTime = false, noVentCooldown = false; + public static bool endlessBattery = false, endlessVentTime = false, noVentCooldown = false, noMapCooldowns = false; public static bool reactorSab = false, oxygenSab = false, commsSab = false, elecSab = false; public static bool autoOpenDoors = false; public static bool moonWalk = false; @@ -2922,6 +2922,7 @@ private void SaveConfig() SaveBool("M_CameraZoom", cameraZoom); SaveBool("M_RevealVotes", RevealVotesEnabled); SaveBool("M_NoTaskMode", noTaskMode); + SaveBool("M_NoMapCooldowns", noMapCooldowns); SaveBool("M_NeverEndGame", neverEndGame); SaveBool("M_RemovePenalty", removePenalty); SaveBool("M_AlwaysShowLobbyTimer", alwaysShowLobbyTimer); @@ -3076,6 +3077,7 @@ private void LoadConfig() cameraZoom = LoadBool("M_CameraZoom", cameraZoom); RevealVotesEnabled = LoadBool("M_RevealVotes", RevealVotesEnabled); noTaskMode = LoadBool("M_NoTaskMode", noTaskMode); + noMapCooldowns = LoadBool("M_NoMapCooldowns", noMapCooldowns); neverEndGame = LoadBool("M_NeverEndGame", neverEndGame); removePenalty = LoadBool("M_RemovePenalty", removePenalty); alwaysShowLobbyTimer = LoadBool("M_AlwaysShowLobbyTimer", alwaysShowLobbyTimer); @@ -3719,7 +3721,7 @@ private void DrawGeneralInfoTab() GUILayout.Label($"{L("Make sure you are using the latest version from GitHub releases.", "Убедитесь, что используете последнюю версию из GitHub releases.")}", textStyle); GUILayout.Space(8); GUILayout.Label($"{L("Quick Hotkeys", "Быстрые клавиши")}", textStyle); - GUILayout.Label(L("Insert / Right Shift: open or close menu", "Insert / Right Shift: открыть или закрыть меню"), textStyle); + GUILayout.Label(L("Insert: open or close menu", "Insert: открыть или закрыть меню"), textStyle); GUILayout.Label(L("Right Click: teleport to cursor", "ПКМ: телепорт к курсору"), textStyle); GUILayout.Label(L("F9: magnet cursor", "F9: магнит курсора"), textStyle); GUILayout.EndVertical(); @@ -3936,6 +3938,8 @@ private void DrawRolesCompact() GUILayout.Space(3); noVentCooldown = DrawToggle(noVentCooldown, "No Vent Cooldown", 230); GUILayout.Space(3); + noMapCooldowns = DrawToggle(noMapCooldowns, "No Map Cooldowns", 230); + GUILayout.Space(3); endlessBattery = DrawToggle(endlessBattery, "Endless Battery", 230); GUILayout.Space(3); noVitalsCooldown = DrawToggle(noVitalsCooldown, "No Vitals Cooldown", 230); @@ -4569,14 +4573,14 @@ private void DrawPlayersTab() if (forcedPreGameRoles.ContainsKey(pc.PlayerId)) pName += " [*]"; else if (forcedImpostors.Contains(pc.PlayerId)) pName += " [Imp]"; - bool isSelected = selectedHydraPlayerId == pc.PlayerId; + bool isSelected = selectedAntiCheatPlayerId == pc.PlayerId; GUI.contentColor = Color.white; try { GUI.contentColor = Palette.PlayerColors[pc.Data.DefaultOutfit.ColorId]; } catch { } if (GUILayout.Button(pName, isSelected ? activeTabStyle : btnStyle, GUILayout.Height(30))) { - selectedHydraPlayerId = pc.PlayerId; + selectedAntiCheatPlayerId = pc.PlayerId; } GUI.contentColor = Color.white; } @@ -4587,7 +4591,7 @@ private void DrawPlayersTab() GUILayout.BeginVertical(boxStyle, GUILayout.ExpandWidth(true)); playerActionScrollPos = GUILayout.BeginScrollView(playerActionScrollPos); - PlayerControl target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedHydraPlayerId); + PlayerControl target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedAntiCheatPlayerId); if (target != null && target.Data != null) { @@ -5162,6 +5166,8 @@ private void DrawRolesTab() endlessVentTime = DrawToggle(endlessVentTime, "Endless Vent Time", 160); GUILayout.Space(5); noVentCooldown = DrawToggle(noVentCooldown, "No Vent Cooldown", 160); + GUILayout.Space(5); + noMapCooldowns = DrawToggle(noMapCooldowns, "No Map Cooldowns", 160); GUILayout.EndVertical(); GUILayout.Space(5); @@ -7768,6 +7774,67 @@ public static void Postfix(EngineerRole __instance) } } + private static bool TrySetCooldownMember(object target, float value) + { + if (target == null) return false; + + string[] names = { "CoolDown", "_CoolDown_k__BackingField", "k__BackingField", "coolDown", "cooldown" }; + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + try + { + Type type = target.GetType(); + foreach (string name in names) + { + PropertyInfo property = type.GetProperty(name, flags); + if (property != null && property.CanWrite) + { + property.SetValue(target, value, null); + return true; + } + + FieldInfo field = type.GetField(name, flags); + if (field != null) + { + field.SetValue(target, value); + return true; + } + } + } + catch { } + + return false; + } + + [HarmonyPatch(typeof(Ladder), "SetDestinationCooldown")] + public static class Ladder_SetDestinationCooldown_Patch + { + public static bool Prefix(Ladder __instance) + { + try + { + if (!ElysiumModMenuGUI.noMapCooldowns) return true; + TrySetCooldownMember(__instance, 0f); + return false; + } + catch { return true; } + } + } + + [HarmonyPatch(typeof(ZiplineConsole), "Update")] + public static class ZiplineConsole_Update_Patch + { + public static void Postfix(ZiplineConsole __instance) + { + try + { + if (!ElysiumModMenuGUI.noMapCooldowns) return; + TrySetCooldownMember(__instance, 0f); + } + catch { } + } + } + [HarmonyPatch(typeof(PlayerControl), "MurderPlayer")] public static class KillCooldownTrackerPatch2 { @@ -8430,40 +8497,40 @@ public static class RPCSniffer_Patch private static readonly Dictionary KnownMods = new Dictionary { - { 157, ("RockStar", "#800000") }, - { 121, ("RockStar / Chocoo", "#800000") }, - { 167, ("TuffMenu", "#008000") }, - { 164, ("Hydra / Sicko", "#FF0000") }, - { 176, ("HostGuard / TOH", "#008000") }, - { 195, ("Polar Client", "#FFFF00") }, - { 204, ("Polar Client", "#FFFF00") }, - { 154, ("GNC", "#FF0000") }, - { 85, ("KillNet (Base)", "#FF0000") }, - { 150, ("KillNet (V2)", "#FF0000") }, - { 162, ("KNM", "#FF0000") }, - { 250, ("KillNet (Alt)", "#FF0000") }, - { 212, ("BanMod", "#008000") }, - { 213, ("BanMod", "#008000") }, - { 214, ("BanMod", "#008000") }, - { 215, ("BanMod", "#008000") }, - { 216, ("BanMod", "#008000") }, - { 217, ("BanMod", "#008000") }, - { 218, ("BanMod", "#008000") }, - { 219, ("BanMod", "#008000") }, - { 144, ("Gaff Menu", "#FF0000") }, - { 145, ("Gaff Menu", "#FF0000") }, - { 188, ("GMM", "#FF0000") }, - { 189, ("GMM", "#FF0000") }, - { 169, ("Malum", "#FF0000") }, - { 210, ("Eclipse", "#FFFF00") }, - { 173, ("Private", "#FF0000") }, - { 151, ("Better Among Us", "#008000") }, - { 152, ("Better Among Us", "#008000") }, - { 255, ("CrewMod", "#FFFF00") }, - { 111, ("AUM (BitCrackers)", "#FF0000") }, - { 231, ("SentinelAU", "#FF0000") }, - { 133, ("Lunar / ElysiumModMenu", "#00FFFF") }, - { 89, ("ElysiumModMenu Old", "#008000") } + { 157, ("Modded Client", "#800000") }, + { 121, ("Modded Client", "#800000") }, + { 167, ("Utility Client", "#008000") }, + { 164, ("Anti Cheat / Utility Client", "#FF0000") }, + { 176, ("Host Protection", "#008000") }, + { 195, ("Modded Client", "#FFFF00") }, + { 204, ("Modded Client", "#FFFF00") }, + { 154, ("Network Tool", "#FF0000") }, + { 85, ("RPC Tool", "#FF0000") }, + { 150, ("RPC Tool", "#FF0000") }, + { 162, ("RPC Tool", "#FF0000") }, + { 250, ("RPC Tool", "#FF0000") }, + { 212, ("Protection Tool", "#008000") }, + { 213, ("Protection Tool", "#008000") }, + { 214, ("Protection Tool", "#008000") }, + { 215, ("Protection Tool", "#008000") }, + { 216, ("Protection Tool", "#008000") }, + { 217, ("Protection Tool", "#008000") }, + { 218, ("Protection Tool", "#008000") }, + { 219, ("Protection Tool", "#008000") }, + { 144, ("Utility Client", "#FF0000") }, + { 145, ("Utility Client", "#FF0000") }, + { 188, ("Utility Client", "#FF0000") }, + { 189, ("Utility Client", "#FF0000") }, + { 169, ("Utility Client", "#FF0000") }, + { 210, ("Modded Client", "#FFFF00") }, + { 173, ("Private Client", "#FF0000") }, + { 151, ("Modded Client", "#008000") }, + { 152, ("Modded Client", "#008000") }, + { 255, ("Modded Client", "#FFFF00") }, + { 111, ("Network Tool", "#FF0000") }, + { 231, ("Anti Cheat", "#FF0000") }, + { 133, ("Local Client", "#00FFFF") }, + { 89, ("Legacy Client", "#008000") } }; public static bool Prefix(PlayerControl __instance, byte callId, MessageReader reader) From 31b76abecf66c31c3efc61120e289412523525cc Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 11:08:18 +0200 Subject: [PATCH 07/39] Keep mod labels only for spoofing and detection --- ElysiumModMenu.cs | 68 +++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index e629ea9..4b9a792 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -101,7 +101,7 @@ public override void Load() } public class ElysiumModMenuGUI : MonoBehaviour { - public static string[] spoofMenuNames = { "Local Client", "Anti Cheat", "Lobby Tool", "Protection Tool", "Utility Client", "Network Tool", "RPC Tool", "Legacy Tool", "Alt Client", "Private Client" }; + public static string[] spoofMenuNames = { "ElysiumModMenu", "HostGuard/TOH", "Polar", "BanMod", "Better Among Us", "Sicko Menu", "GNC", "KillNetwork (V1)", "KillNetwork (V2)", "KNM" }; public static byte[] spoofMenuRPCs = { 89, 176, 204, 212, 151, 164, 154, 85, 150, 162 }; public static float rpcSpoofDelay = 4f; @@ -8497,40 +8497,40 @@ public static class RPCSniffer_Patch private static readonly Dictionary KnownMods = new Dictionary { - { 157, ("Modded Client", "#800000") }, - { 121, ("Modded Client", "#800000") }, - { 167, ("Utility Client", "#008000") }, - { 164, ("Anti Cheat / Utility Client", "#FF0000") }, - { 176, ("Host Protection", "#008000") }, - { 195, ("Modded Client", "#FFFF00") }, - { 204, ("Modded Client", "#FFFF00") }, - { 154, ("Network Tool", "#FF0000") }, - { 85, ("RPC Tool", "#FF0000") }, - { 150, ("RPC Tool", "#FF0000") }, - { 162, ("RPC Tool", "#FF0000") }, - { 250, ("RPC Tool", "#FF0000") }, - { 212, ("Protection Tool", "#008000") }, - { 213, ("Protection Tool", "#008000") }, - { 214, ("Protection Tool", "#008000") }, - { 215, ("Protection Tool", "#008000") }, - { 216, ("Protection Tool", "#008000") }, - { 217, ("Protection Tool", "#008000") }, - { 218, ("Protection Tool", "#008000") }, - { 219, ("Protection Tool", "#008000") }, - { 144, ("Utility Client", "#FF0000") }, - { 145, ("Utility Client", "#FF0000") }, - { 188, ("Utility Client", "#FF0000") }, - { 189, ("Utility Client", "#FF0000") }, - { 169, ("Utility Client", "#FF0000") }, - { 210, ("Modded Client", "#FFFF00") }, + { 157, ("RockStar", "#800000") }, + { 121, ("RockStar / Chocoo", "#800000") }, + { 167, ("TuffMenu", "#008000") }, + { 164, ("Hydra / Sicko", "#FF0000") }, + { 176, ("HostGuard / TOH", "#008000") }, + { 195, ("Polar Client", "#FFFF00") }, + { 204, ("Polar Client", "#FFFF00") }, + { 154, ("GNC", "#FF0000") }, + { 85, ("KillNet (Base)", "#FF0000") }, + { 150, ("KillNet (V2)", "#FF0000") }, + { 162, ("KNM", "#FF0000") }, + { 250, ("KillNet (Alt)", "#FF0000") }, + { 212, ("BanMod", "#008000") }, + { 213, ("BanMod", "#008000") }, + { 214, ("BanMod", "#008000") }, + { 215, ("BanMod", "#008000") }, + { 216, ("BanMod", "#008000") }, + { 217, ("BanMod", "#008000") }, + { 218, ("BanMod", "#008000") }, + { 219, ("BanMod", "#008000") }, + { 144, ("Gaff Menu", "#FF0000") }, + { 145, ("Gaff Menu", "#FF0000") }, + { 188, ("GMM", "#FF0000") }, + { 189, ("GMM", "#FF0000") }, + { 169, ("Malum", "#FF0000") }, + { 210, ("Eclipse", "#FFFF00") }, { 173, ("Private Client", "#FF0000") }, - { 151, ("Modded Client", "#008000") }, - { 152, ("Modded Client", "#008000") }, - { 255, ("Modded Client", "#FFFF00") }, - { 111, ("Network Tool", "#FF0000") }, - { 231, ("Anti Cheat", "#FF0000") }, - { 133, ("Local Client", "#00FFFF") }, - { 89, ("Legacy Client", "#008000") } + { 151, ("Better Among Us", "#008000") }, + { 152, ("Better Among Us", "#008000") }, + { 255, ("CrewMod", "#FFFF00") }, + { 111, ("AUM (BitCrackers)", "#FF0000") }, + { 231, ("SentinelAU", "#FF0000") }, + { 133, ("Lunar / ElysiumModMenu", "#00FFFF") }, + { 89, ("ElysiumModMenu Old", "#008000") } }; public static bool Prefix(PlayerControl __instance, byte callId, MessageReader reader) From d712a988eb6eb1ae84e5adbadb7a170c3bdfa5dc Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 11:21:38 +0200 Subject: [PATCH 08/39] Restore configurable menu toggle key --- ElysiumModMenu.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 4b9a792..3904167 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -2873,8 +2873,9 @@ private void SaveConfig() Plugin.RpcSpoofDelayConfig.Value = rpcSpoofDelay; Plugin.MenuColorIndexConfig.Value = currentMenuColorIndex; Plugin.RgbMenuModeConfig.Value = rgbMenuMode; - Plugin.MenuKeybind.Value = KeyCode.Insert; - menuToggleKey = KeyCode.Insert; + if (menuToggleKey == KeyCode.None) menuToggleKey = KeyCode.Insert; + Plugin.MenuKeybind.Value = menuToggleKey; + PlayerPrefs.SetInt("M_MenuToggleKey", (int)menuToggleKey); SaveBool("M_WhiteTheme", whiteMenuTheme); PlayerPrefs.SetInt("M_BndMMorph", (int)bindMassMorph); PlayerPrefs.SetInt("M_BndSpawn", (int)bindSpawnLobby); @@ -3031,7 +3032,9 @@ private void LoadConfig() enableLocalFriendCodeSpoof = LoadBool("M_LocalFakeFCEnabled", enableLocalFriendCodeSpoof); if (PlayerPrefs.HasKey("M_LocalFakeFC")) localFriendCodeInput = PlayerPrefs.GetString("M_LocalFakeFC"); if (PlayerPrefs.HasKey("M_BndMagnet")) bindMagnetCursor = (KeyCode)PlayerPrefs.GetInt("M_BndMagnet"); - menuToggleKey = KeyCode.Insert; + menuToggleKey = Plugin.MenuKeybind.Value == KeyCode.None ? KeyCode.Insert : Plugin.MenuKeybind.Value; + if (PlayerPrefs.HasKey("M_MenuToggleKey")) menuToggleKey = (KeyCode)PlayerPrefs.GetInt("M_MenuToggleKey"); + if (menuToggleKey == KeyCode.None) menuToggleKey = KeyCode.Insert; if (PlayerPrefs.HasKey("M_BndMMorph")) bindMassMorph = (KeyCode)PlayerPrefs.GetInt("M_BndMMorph"); if (PlayerPrefs.HasKey("M_BndSpawn")) bindSpawnLobby = (KeyCode)PlayerPrefs.GetInt("M_BndSpawn"); if (PlayerPrefs.HasKey("M_BndDespawn")) bindDespawnLobby = (KeyCode)PlayerPrefs.GetInt("M_BndDespawn"); @@ -3100,7 +3103,7 @@ private void LoadConfig() if (PlayerPrefs.HasKey("M_AutoHostFastStartDelaySeconds")) AutoHostFastStartDelaySeconds = PlayerPrefs.GetFloat("M_AutoHostFastStartDelaySeconds"); if (PlayerPrefs.HasKey("M_WalkSpeed")) walkSpeed = PlayerPrefs.GetFloat("M_WalkSpeed"); if (PlayerPrefs.HasKey("M_EngineSpeed")) engineSpeed = PlayerPrefs.GetFloat("M_EngineSpeed"); - keyBinds["Toggle Menu"] = KeyCode.Insert; + keyBinds["Toggle Menu"] = menuToggleKey; if (PlayerPrefs.HasKey("M_SpoofName")) customNameInput = PlayerPrefs.GetString("M_SpoofName"); } catch { } @@ -3524,9 +3527,10 @@ private void DrawBindsTab() try { GUILayout.Label("CUSTOM KEYBINDS", headerStyle); - GUILayout.Label(L("Menu toggle is locked to Insert.", "Меню открывается только на Insert."), safeLineStyle); + GUILayout.Label(L("Menu toggle is configurable. Right Shift stays disabled.", "Кнопку меню можно менять. Right Shift выключен."), safeLineStyle); GUILayout.Space(10); + DrawKeybindRow("Menu Toggle:", ref menuToggleKey, ref isWaitingForBind); DrawKeybindRow("Magnet Cursor:", ref bindMagnetCursor, ref isWaitBindMagnetCursor); DrawKeybindRow("Mass Morph:", ref bindMassMorph, ref isWaitBindMassMorph); DrawKeybindRow("Spawn Lobby:", ref bindSpawnLobby, ref isWaitBindSpawnLobby); @@ -3721,7 +3725,8 @@ private void DrawGeneralInfoTab() GUILayout.Label($"{L("Make sure you are using the latest version from GitHub releases.", "Убедитесь, что используете последнюю версию из GitHub releases.")}", textStyle); GUILayout.Space(8); GUILayout.Label($"{L("Quick Hotkeys", "Быстрые клавиши")}", textStyle); - GUILayout.Label(L("Insert: open or close menu", "Insert: открыть или закрыть меню"), textStyle); + string menuKeyText = (menuToggleKey == KeyCode.None ? KeyCode.Insert : menuToggleKey).ToString(); + GUILayout.Label($"{L("Menu key", "Кнопка меню")}: {menuKeyText}", textStyle); GUILayout.Label(L("Right Click: teleport to cursor", "ПКМ: телепорт к курсору"), textStyle); GUILayout.Label(L("F9: magnet cursor", "F9: магнит курсора"), textStyle); GUILayout.EndVertical(); @@ -5875,7 +5880,8 @@ public void Update() isWaitBindEndCrew || isWaitBindEndImp || isWaitBindEndImpDC || isWaitBindEndHnsDC || isWaitBindMagnetCursor; - if (!isTypingOrBinding && Input.GetKeyDown(KeyCode.Insert)) + KeyCode activeMenuKey = menuToggleKey == KeyCode.None ? KeyCode.Insert : menuToggleKey; + if (!isTypingOrBinding && Input.GetKeyDown(activeMenuKey)) { showMenu = !showMenu; if (!showMenu) SaveConfig(); From a5ebbe8af7fc15e0c13f86a1e37ba3ee17b9106c Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 11:25:51 +0200 Subject: [PATCH 09/39] Bump version to 1.3.2 --- ElysiumModMenu.cs | 6 +++--- ElysiumModMenu.csproj | 6 +++--- README.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 3904167..8123376 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -35,7 +35,7 @@ using System.Runtime.CompilerServices; namespace ElysiumModMenu { - [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.1")] + [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.2")] public class Plugin : BasePlugin { public static Plugin Instance { get; private set; } = null!; @@ -3687,7 +3687,7 @@ private void DrawGeneralInfoTab() string contributorHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(109, 138, 255, 255)) : new Color32(109, 138, 255, 255)); string dangerHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(231, 76, 60, 255)) : new Color32(231, 76, 60, 255)); string safeHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(57, 255, 20, 255)) : new Color32(57, 255, 20, 255)); - string versionText = "1.3.1"; + string versionText = "1.3.2"; GUIStyle textStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 12 }; textStyle.normal.textColor = whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : new Color(0.85f, 0.85f, 0.85f, 1f); @@ -7415,7 +7415,7 @@ public static void Postfix(PingTracker __instance) if (ElysiumModMenuGUI.showWatermark) { - string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.1"); + string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.2"); finalString = $"{shimmerTitle} • " + finalString; } diff --git a/ElysiumModMenu.csproj b/ElysiumModMenu.csproj index b77954a..4ee0676 100644 --- a/ElysiumModMenu.csproj +++ b/ElysiumModMenu.csproj @@ -7,9 +7,9 @@ latest ElysiumModMenu ElysiumModMenu - 1.3.1 - 1.3.1.0 - 1.3.1.0 + 1.3.2 + 1.3.2.0 + 1.3.2.0 C:\Program Files (x86)\Steam\steamapps\common\Among Us diff --git a/README.md b/README.md index 5bec5df..a24c8d9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Advanced BepInEx IL2CPP mod menu for Among Us with host tools, anti-cheat utilit | Version | Status | Download | | :--- | :--- | :--- | -| v1.3.1 | Latest | [Download ElysiumModMenu.dll](https://github.com/meowchelo/ElysiumModMenu/releases/latest) | +| v1.3.2 | Latest | [Download ElysiumModMenu.dll](https://github.com/meowchelo/ElysiumModMenu/releases/latest) | ## Highlights From 1e5a9bdd794078f5085d6e75a0e8c8208be6f472 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 19:28:03 +0200 Subject: [PATCH 10/39] Add Pasos Limit empty RPC drop --- ElysiumModMenu.cs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 8123376..51570e6 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -1062,6 +1062,8 @@ private void DrawAntiCheatTab() blockMeetingFloodRpc = DrawToggle(blockMeetingFloodRpc, "Block Meeting RPC Flood", 250); GUILayout.Space(5); blockChatFloodRpc = DrawToggle(blockChatFloodRpc, "Block Chat RPC Flood", 250); + GUILayout.Space(5); + enablePasosLimit = DrawToggle(enablePasosLimit, "Pasos Limit", 250); GUILayout.Space(15); GUILayout.Label(L("OTHER PROTECTIONS", "ПРОЧАЯ ЗАЩИТА"), headerStyle); @@ -2461,6 +2463,45 @@ private void DrawVisualsInGame() public static bool enableLocalPetSpamDrop = true; public static bool enableHostPetSpamBan = false; + + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadMessage))] + public static class Shield_PasosLimit_Patch + { + private const byte RpcGameDataTag = 2; + private const byte DroppedGameDataTag = 0; + private const int PasosDropNotifyLimit = 40; + private const float PasosWindow = 0.75f; + private const float PasosNotifyCooldown = 2f; + private static readonly Queue emptyRpcDrops = new Queue(); + private static float lastPasosNotify; + + public static void Postfix(MessageReader __result) + { + if (!ElysiumModMenuGUI.enablePasosLimit || __result == null) return; + + try + { + if (__result.Tag != RpcGameDataTag || __result.BytesRemaining > 0) return; + + __result.Tag = DroppedGameDataTag; + __result.Position = __result.Length; + + float now = UnityEngine.Time.time; + while (emptyRpcDrops.Count > 0 && emptyRpcDrops.Peek() < now - PasosWindow) + emptyRpcDrops.Dequeue(); + + emptyRpcDrops.Enqueue(now); + + if (emptyRpcDrops.Count > PasosDropNotifyLimit && now - lastPasosNotify > PasosNotifyCooldown) + { + lastPasosNotify = now; + ElysiumModMenuGUI.ShowNotification($"[SHIELD] Pasos Limit: dropped empty RPC spam ({emptyRpcDrops.Count}/{PasosWindow:0.00}s)"); + } + } + catch { } + } + } + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] public static class Shield_PetSpam_Patch { @@ -2933,6 +2974,7 @@ private void SaveConfig() SaveBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); + SaveBool("M_PasosLimit", enablePasosLimit); SaveBool("M_AutoHostEnabled", AutoHostEnabled); SaveBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); SaveBool("M_AutoHostNotifications", AutoHostNotifications); @@ -3090,6 +3132,7 @@ private void LoadConfig() blockGameRpcInLobby = LoadBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); + enablePasosLimit = LoadBool("M_PasosLimit", enablePasosLimit); AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); AutoReturnLobbyAfterMatch = LoadBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); AutoHostNotifications = LoadBool("M_AutoHostNotifications", AutoHostNotifications); @@ -6791,6 +6834,7 @@ private void DrawElysiumModMenu(int windowID) public static bool blockGameRpcInLobby = true; public static bool blockChatFloodRpc = true; public static bool blockMeetingFloodRpc = true; + public static bool enablePasosLimit = true; public static bool autoBanBrokenFriendCode = false; public static int chatRpcLimit = 1; public static float chatRpcWindow = 1f; From 54a13a8d1f8e88c03143c6e95abf26d06ed7422d Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 19:40:15 +0200 Subject: [PATCH 11/39] Fix Pasos Limit empty game data freeze --- ElysiumModMenu.cs | 48 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 51570e6..8fda0ab 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -2475,6 +2475,21 @@ public static class Shield_PasosLimit_Patch private static readonly Queue emptyRpcDrops = new Queue(); private static float lastPasosNotify; + public static void RecordDrop() + { + float now = UnityEngine.Time.time; + while (emptyRpcDrops.Count > 0 && emptyRpcDrops.Peek() < now - PasosWindow) + emptyRpcDrops.Dequeue(); + + emptyRpcDrops.Enqueue(now); + + if (emptyRpcDrops.Count > PasosDropNotifyLimit && now - lastPasosNotify > PasosNotifyCooldown) + { + lastPasosNotify = now; + ElysiumModMenuGUI.ShowNotification($"[SHIELD] Pasos Limit: dropped empty RPC spam ({emptyRpcDrops.Count}/{PasosWindow:0.00}s)"); + } + } + public static void Postfix(MessageReader __result) { if (!ElysiumModMenuGUI.enablePasosLimit || __result == null) return; @@ -2486,19 +2501,34 @@ public static void Postfix(MessageReader __result) __result.Tag = DroppedGameDataTag; __result.Position = __result.Length; - float now = UnityEngine.Time.time; - while (emptyRpcDrops.Count > 0 && emptyRpcDrops.Peek() < now - PasosWindow) - emptyRpcDrops.Dequeue(); + RecordDrop(); + } + catch { } + } + } + + [HarmonyPatch] + public static class Shield_PasosLimit_HandleGameData_Patch + { + public static MethodBase TargetMethod() + { + return AccessTools.Method(typeof(InnerNetClient), "HandleGameData", new[] { typeof(MessageReader) }); + } - emptyRpcDrops.Enqueue(now); + public static bool Prefix(MessageReader parentReader) + { + if (!ElysiumModMenuGUI.enablePasosLimit || parentReader == null) return true; - if (emptyRpcDrops.Count > PasosDropNotifyLimit && now - lastPasosNotify > PasosNotifyCooldown) - { - lastPasosNotify = now; - ElysiumModMenuGUI.ShowNotification($"[SHIELD] Pasos Limit: dropped empty RPC spam ({emptyRpcDrops.Count}/{PasosWindow:0.00}s)"); - } + try + { + if (parentReader.Length > 0 && parentReader.BytesRemaining > 0) return true; + + Shield_PasosLimit_Patch.RecordDrop(); + return false; } catch { } + + return true; } } From 3ad48f35c485bc10c1807466173ce12517bade87 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 19:47:48 +0200 Subject: [PATCH 12/39] Guard Pasos Limit in game data coroutine --- ElysiumModMenu.cs | 84 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 8fda0ab..bcdebd1 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -2515,12 +2515,14 @@ public static MethodBase TargetMethod() return AccessTools.Method(typeof(InnerNetClient), "HandleGameData", new[] { typeof(MessageReader) }); } - public static bool Prefix(MessageReader parentReader) + public static bool Prefix(object[] __args) { - if (!ElysiumModMenuGUI.enablePasosLimit || parentReader == null) return true; + if (!ElysiumModMenuGUI.enablePasosLimit) return true; try { + MessageReader parentReader = __args != null && __args.Length > 0 ? __args[0] as MessageReader : null; + if (parentReader == null) return true; if (parentReader.Length > 0 && parentReader.BytesRemaining > 0) return true; Shield_PasosLimit_Patch.RecordDrop(); @@ -2532,6 +2534,84 @@ public static bool Prefix(MessageReader parentReader) } } + [HarmonyPatch] + public static class Shield_PasosLimit_HandleGameDataInner_Patch + { + public static MethodBase TargetMethod() + { + return AccessTools.Method(typeof(InnerNetClient), "HandleGameDataInner", new[] { typeof(MessageReader), typeof(int) }); + } + + public static bool Prefix(object[] __args, ref Il2CppSystem.Collections.IEnumerator __result) + { + if (!ElysiumModMenuGUI.enablePasosLimit) return true; + + try + { + MessageReader parentReader = __args != null && __args.Length > 0 ? __args[0] as MessageReader : null; + if (parentReader == null) return true; + if (parentReader.Length > 0 && parentReader.BytesRemaining > 0) return true; + + Shield_PasosLimit_Patch.RecordDrop(); + __result = EmptyPasosRoutine().WrapToIl2Cpp(); + return false; + } + catch { } + + return true; + } + + private static System.Collections.IEnumerator EmptyPasosRoutine() { yield break; } + } + + [HarmonyPatch] + public static class Shield_PasosLimit_HandleGameDataInner_MoveNext_Patch + { + public static IEnumerable TargetMethods() + { + foreach (Type nestedType in typeof(InnerNetClient).GetNestedTypes(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (!nestedType.Name.Contains("HandleGameDataInner")) continue; + + MethodInfo moveNext = AccessTools.Method(nestedType, "MoveNext"); + if (moveNext != null) + yield return moveNext; + } + } + + public static bool Prefix(object __instance, ref bool __result) + { + if (!ElysiumModMenuGUI.enablePasosLimit || __instance == null) return true; + + try + { + if (!HasEmptyGameDataReader(__instance)) return true; + + Shield_PasosLimit_Patch.RecordDrop(); + __result = false; + return false; + } + catch { } + + return true; + } + + private static bool HasEmptyGameDataReader(object routine) + { + FieldInfo[] fields = routine.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + foreach (FieldInfo field in fields) + { + if (!typeof(MessageReader).IsAssignableFrom(field.FieldType)) continue; + + MessageReader reader = field.GetValue(routine) as MessageReader; + if (reader != null && reader.Length <= 0 && reader.BytesRemaining <= 0) + return true; + } + + return false; + } + } + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] public static class Shield_PetSpam_Patch { From 4139a3b4109e0de24de92987c035e3836aa90f92 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 19:54:51 +0200 Subject: [PATCH 13/39] Guard empty Hazel reads for Pasos Limit --- ElysiumModMenu.cs | 100 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index bcdebd1..bb7df78 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -2612,6 +2612,106 @@ private static bool HasEmptyGameDataReader(object routine) } } + public static bool ShouldDropPasosEmptyRead(MessageReader reader) + { + if (!ElysiumModMenuGUI.enablePasosLimit || reader == null) return false; + + try + { + if (reader.Length > 0 || reader.BytesRemaining > 0) return false; + + Shield_PasosLimit_Patch.RecordDrop(); + return true; + } + catch { } + + return false; + } + + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadPackedInt32))] + public static class Shield_PasosLimit_ReadPackedInt32_Patch + { + public static bool Prefix(MessageReader __instance, ref int __result) + { + if (!ShouldDropPasosEmptyRead(__instance)) return true; + + __result = 0; + return false; + } + } + + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadPackedUInt32))] + public static class Shield_PasosLimit_ReadPackedUInt32_Patch + { + public static bool Prefix(MessageReader __instance, ref uint __result) + { + if (!ShouldDropPasosEmptyRead(__instance)) return true; + + __result = 0u; + return false; + } + } + + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadUInt32))] + public static class Shield_PasosLimit_ReadUInt32_Patch + { + public static bool Prefix(MessageReader __instance, ref uint __result) + { + if (!ShouldDropPasosEmptyRead(__instance)) return true; + + __result = 0u; + return false; + } + } + + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadByte))] + public static class Shield_PasosLimit_ReadByte_Patch + { + public static bool Prefix(MessageReader __instance, ref byte __result) + { + if (!ShouldDropPasosEmptyRead(__instance)) return true; + + __result = 0; + return false; + } + } + + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadSByte))] + public static class Shield_PasosLimit_ReadSByte_Patch + { + public static bool Prefix(MessageReader __instance, ref sbyte __result) + { + if (!ShouldDropPasosEmptyRead(__instance)) return true; + + __result = 0; + return false; + } + } + + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadBoolean))] + public static class Shield_PasosLimit_ReadBoolean_Patch + { + public static bool Prefix(MessageReader __instance, ref bool __result) + { + if (!ShouldDropPasosEmptyRead(__instance)) return true; + + __result = false; + return false; + } + } + + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadString))] + public static class Shield_PasosLimit_ReadString_Patch + { + public static bool Prefix(MessageReader __instance, ref string __result) + { + if (!ShouldDropPasosEmptyRead(__instance)) return true; + + __result = string.Empty; + return false; + } + } + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] public static class Shield_PetSpam_Patch { From 484efa4392d7afeb28ad5d1d5c36308759057fff Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 20:04:36 +0200 Subject: [PATCH 14/39] Drop buffered empty RPC messages --- ElysiumModMenu.cs | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index bb7df78..d8d5bd3 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -2492,14 +2492,19 @@ public static void RecordDrop() public static void Postfix(MessageReader __result) { - if (!ElysiumModMenuGUI.enablePasosLimit || __result == null) return; + DropEmptyRpcMessage(__result); + } + + public static void DropEmptyRpcMessage(MessageReader reader) + { + if (!ElysiumModMenuGUI.enablePasosLimit || reader == null) return; try { - if (__result.Tag != RpcGameDataTag || __result.BytesRemaining > 0) return; + if (reader.Tag != RpcGameDataTag || reader.BytesRemaining > 0 || reader.Length > 0) return; - __result.Tag = DroppedGameDataTag; - __result.Position = __result.Length; + reader.Tag = DroppedGameDataTag; + reader.Position = reader.Length; RecordDrop(); } @@ -2507,6 +2512,15 @@ public static void Postfix(MessageReader __result) } } + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadMessageAsNewBuffer))] + public static class Shield_PasosLimit_ReadMessageAsNewBuffer_Patch + { + public static void Postfix(MessageReader __result) + { + Shield_PasosLimit_Patch.DropEmptyRpcMessage(__result); + } + } + [HarmonyPatch] public static class Shield_PasosLimit_HandleGameData_Patch { @@ -2638,6 +2652,23 @@ public static bool Prefix(MessageReader __instance, ref int __result) __result = 0; return false; } + + public static Exception Finalizer(MessageReader __instance, Exception __exception, ref int __result) + { + if (__exception == null || !ElysiumModMenuGUI.enablePasosLimit) return __exception; + + try + { + if (__instance == null || __instance.Length > 0 || __instance.BytesRemaining > 0) return __exception; + + Shield_PasosLimit_Patch.RecordDrop(); + __result = 0; + return null; + } + catch { } + + return __exception; + } } [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadPackedUInt32))] From d24dd6398a4f615681446d4dfdcfdb3a74390ab6 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 20:15:39 +0200 Subject: [PATCH 15/39] Add Pasos Limit local and host bans --- ElysiumModMenu.cs | 205 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 200 insertions(+), 5 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index d8d5bd3..6edb3f7 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -1064,6 +1064,8 @@ private void DrawAntiCheatTab() blockChatFloodRpc = DrawToggle(blockChatFloodRpc, "Block Chat RPC Flood", 250); GUILayout.Space(5); enablePasosLimit = DrawToggle(enablePasosLimit, "Pasos Limit", 250); + GUILayout.Space(5); + enableHostPasosBan = DrawToggle(enableHostPasosBan, L("Auto-Ban Pasos Spam", "Авто-бан за Pasos Spam"), 250); GUILayout.Space(15); GUILayout.Label(L("OTHER PROTECTIONS", "ПРОЧАЯ ЗАЩИТА"), headerStyle); @@ -2469,13 +2471,26 @@ public static class Shield_PasosLimit_Patch { private const byte RpcGameDataTag = 2; private const byte DroppedGameDataTag = 0; - private const int PasosDropNotifyLimit = 40; + private const int PasosDropNotifyLimit = 1; private const float PasosWindow = 0.75f; private const float PasosNotifyCooldown = 2f; private static readonly Queue emptyRpcDrops = new Queue(); + private static readonly HashSet pasosBlockedClientIds = new HashSet(); private static float lastPasosNotify; + private static int currentPasosClientId = -1; + + public static void SetCurrentClientId(int clientId) + { + if (clientId >= 0) + currentPasosClientId = clientId; + } - public static void RecordDrop() + public static bool IsClientBlocked(int clientId) + { + return clientId >= 0 && pasosBlockedClientIds.Contains(clientId); + } + + public static void RecordDrop(int clientId = -1) { float now = UnityEngine.Time.time; while (emptyRpcDrops.Count > 0 && emptyRpcDrops.Peek() < now - PasosWindow) @@ -2483,13 +2498,74 @@ public static void RecordDrop() emptyRpcDrops.Enqueue(now); - if (emptyRpcDrops.Count > PasosDropNotifyLimit && now - lastPasosNotify > PasosNotifyCooldown) + int resolvedClientId = clientId >= 0 ? clientId : currentPasosClientId; + if (resolvedClientId >= 0 && !pasosBlockedClientIds.Contains(resolvedClientId)) + BlockPasosClient(resolvedClientId); + + if (emptyRpcDrops.Count >= PasosDropNotifyLimit && now - lastPasosNotify > PasosNotifyCooldown) { lastPasosNotify = now; ElysiumModMenuGUI.ShowNotification($"[SHIELD] Pasos Limit: dropped empty RPC spam ({emptyRpcDrops.Count}/{PasosWindow:0.00}s)"); } } + private static void BlockPasosClient(int clientId) + { + try + { + if (clientId < 0 || (AmongUsClient.Instance != null && clientId == AmongUsClient.Instance.ClientId)) return; + + pasosBlockedClientIds.Add(clientId); + + PlayerControl player = FindPlayerByClientId(clientId); + string pName = player?.Data?.PlayerName ?? $"Client {clientId}"; + ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} blocked by Pasos Limit (Local)!"); + + if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + + string fc = string.IsNullOrEmpty(player?.Data?.FriendCode) ? "Unknown" : player.Data.FriendCode; + string puid = clientId.ToString(); + + try + { + if (player != null) + { + var client = AmongUsClient.Instance.GetClientFromCharacter(player); + if (client != null) puid = client.Id.ToString(); + } + } + catch { } + + ElysiumModMenuGUI.AddToBanList(fc, puid, pName, "Auto-banned for Pasos empty RPC spam"); + AmongUsClient.Instance.KickPlayer(clientId, true); + ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} auto-banned for Pasos Spam!"); + } + catch { } + } + + public static PlayerControl FindPlayerByClientId(int clientId) + { + try + { + if (PlayerControl.AllPlayerControls == null) return null; + + foreach (PlayerControl pc in PlayerControl.AllPlayerControls) + { + if (pc == null) continue; + if ((int)pc.OwnerId == clientId) return pc; + + try + { + if (pc.Data != null && pc.Data.ClientId == clientId) return pc; + } + catch { } + } + } + catch { } + + return null; + } + public static void Postfix(MessageReader __result) { DropEmptyRpcMessage(__result); @@ -2521,6 +2597,75 @@ public static void Postfix(MessageReader __result) } } + [HarmonyPatch] + public static class Shield_PasosLimit_HandleMessageContext_Patch + { + public static IEnumerable TargetMethods() + { + foreach (MethodInfo method in typeof(InnerNetClient).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (method.Name != "HandleMessage") continue; + if (method.GetParameters().Any(p => p.ParameterType == typeof(MessageReader))) + yield return method; + } + } + + public static bool Prefix(object[] __args) + { + if (!ElysiumModMenuGUI.enablePasosLimit) return true; + + try + { + int clientId = ExtractClientId(__args); + Shield_PasosLimit_Patch.SetCurrentClientId(clientId); + + if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) + return false; + } + catch { } + + return true; + } + + private static int ExtractClientId(object[] args) + { + if (args == null) return -1; + + foreach (object arg in args) + { + int clientId = ExtractClientId(arg); + if (clientId >= 0) return clientId; + } + + return -1; + } + + private static int ExtractClientId(object source) + { + if (source == null || source is MessageReader || source is SendOption) return -1; + + try + { + if (source is int directId) return directId; + + Type type = source.GetType(); + foreach (string name in new[] { "ClientId", "Id", "clientId", "id" }) + { + PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (property != null && property.PropertyType == typeof(int)) + return (int)property.GetValue(source); + + FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (field != null && field.FieldType == typeof(int)) + return (int)field.GetValue(source); + } + } + catch { } + + return -1; + } + } + [HarmonyPatch] public static class Shield_PasosLimit_HandleGameData_Patch { @@ -2562,11 +2707,19 @@ public static bool Prefix(object[] __args, ref Il2CppSystem.Collections.IEnumera try { + int clientId = ExtractClientId(__args); + Shield_PasosLimit_Patch.SetCurrentClientId(clientId); + if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) + { + __result = EmptyPasosRoutine().WrapToIl2Cpp(); + return false; + } + MessageReader parentReader = __args != null && __args.Length > 0 ? __args[0] as MessageReader : null; if (parentReader == null) return true; if (parentReader.Length > 0 && parentReader.BytesRemaining > 0) return true; - Shield_PasosLimit_Patch.RecordDrop(); + Shield_PasosLimit_Patch.RecordDrop(clientId); __result = EmptyPasosRoutine().WrapToIl2Cpp(); return false; } @@ -2575,6 +2728,18 @@ public static bool Prefix(object[] __args, ref Il2CppSystem.Collections.IEnumera return true; } + private static int ExtractClientId(object[] args) + { + try + { + if (args == null || args.Length < 2 || args[1] == null) return -1; + return Convert.ToInt32(args[1]); + } + catch { } + + return -1; + } + private static System.Collections.IEnumerator EmptyPasosRoutine() { yield break; } } @@ -2599,9 +2764,17 @@ public static bool Prefix(object __instance, ref bool __result) try { + int clientId = ExtractClientId(__instance); + Shield_PasosLimit_Patch.SetCurrentClientId(clientId); + if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) + { + __result = false; + return false; + } + if (!HasEmptyGameDataReader(__instance)) return true; - Shield_PasosLimit_Patch.RecordDrop(); + Shield_PasosLimit_Patch.RecordDrop(clientId); __result = false; return false; } @@ -2624,6 +2797,25 @@ private static bool HasEmptyGameDataReader(object routine) return false; } + + private static int ExtractClientId(object routine) + { + try + { + FieldInfo[] fields = routine.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + foreach (FieldInfo field in fields) + { + if (field.FieldType != typeof(int)) continue; + + int value = (int)field.GetValue(routine); + if (value >= 0 && value < 256) + return value; + } + } + catch { } + + return -1; + } } public static bool ShouldDropPasosEmptyRead(MessageReader reader) @@ -3216,6 +3408,7 @@ private void SaveConfig() SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); SaveBool("M_PasosLimit", enablePasosLimit); + SaveBool("M_HostPasosBan", enableHostPasosBan); SaveBool("M_AutoHostEnabled", AutoHostEnabled); SaveBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); SaveBool("M_AutoHostNotifications", AutoHostNotifications); @@ -3374,6 +3567,7 @@ private void LoadConfig() blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); enablePasosLimit = LoadBool("M_PasosLimit", enablePasosLimit); + enableHostPasosBan = LoadBool("M_HostPasosBan", enableHostPasosBan); AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); AutoReturnLobbyAfterMatch = LoadBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); AutoHostNotifications = LoadBool("M_AutoHostNotifications", AutoHostNotifications); @@ -7076,6 +7270,7 @@ private void DrawElysiumModMenu(int windowID) public static bool blockChatFloodRpc = true; public static bool blockMeetingFloodRpc = true; public static bool enablePasosLimit = true; + public static bool enableHostPasosBan = false; public static bool autoBanBrokenFriendCode = false; public static int chatRpcLimit = 1; public static float chatRpcWindow = 1f; From 01aed2ead7203a6311ba1cdd179dcf19de1c071e Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 20:18:12 +0200 Subject: [PATCH 16/39] Make Anti-Pasos bans temporary --- ElysiumModMenu.cs | 120 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 22 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 6edb3f7..a7cecdc 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -1065,7 +1065,7 @@ private void DrawAntiCheatTab() GUILayout.Space(5); enablePasosLimit = DrawToggle(enablePasosLimit, "Pasos Limit", 250); GUILayout.Space(5); - enableHostPasosBan = DrawToggle(enableHostPasosBan, L("Auto-Ban Pasos Spam", "Авто-бан за Pasos Spam"), 250); + enableHostPasosBan = DrawToggle(enableHostPasosBan, L("Anti-Pasos Ban (3m)", "Анти-Pasos бан (3 мин)"), 250); GUILayout.Space(15); GUILayout.Label(L("OTHER PROTECTIONS", "ПРОЧАЯ ЗАЩИТА"), headerStyle); @@ -2473,12 +2473,23 @@ public static class Shield_PasosLimit_Patch private const byte DroppedGameDataTag = 0; private const int PasosDropNotifyLimit = 1; private const float PasosWindow = 0.75f; + private const float PasosBanSeconds = 180f; private const float PasosNotifyCooldown = 2f; private static readonly Queue emptyRpcDrops = new Queue(); - private static readonly HashSet pasosBlockedClientIds = new HashSet(); + private static readonly Dictionary pasosBlockedClientIds = new Dictionary(); + private static readonly List pasosTempHostBans = new List(); private static float lastPasosNotify; private static int currentPasosClientId = -1; + private class PasosTempBanEntry + { + public int ClientId; + public string FriendCode; + public string Puid; + public string Name; + public float ExpiresAt; + } + public static void SetCurrentClientId(int clientId) { if (clientId >= 0) @@ -2487,7 +2498,14 @@ public static void SetCurrentClientId(int clientId) public static bool IsClientBlocked(int clientId) { - return clientId >= 0 && pasosBlockedClientIds.Contains(clientId); + if (clientId < 0) return false; + + float now = UnityEngine.Time.realtimeSinceStartup; + if (!pasosBlockedClientIds.TryGetValue(clientId, out float expiresAt)) return false; + if (expiresAt > now) return true; + + pasosBlockedClientIds.Remove(clientId); + return false; } public static void RecordDrop(int clientId = -1) @@ -2499,7 +2517,7 @@ public static void RecordDrop(int clientId = -1) emptyRpcDrops.Enqueue(now); int resolvedClientId = clientId >= 0 ? clientId : currentPasosClientId; - if (resolvedClientId >= 0 && !pasosBlockedClientIds.Contains(resolvedClientId)) + if (resolvedClientId >= 0 && !IsClientBlocked(resolvedClientId)) BlockPasosClient(resolvedClientId); if (emptyRpcDrops.Count >= PasosDropNotifyLimit && now - lastPasosNotify > PasosNotifyCooldown) @@ -2515,11 +2533,12 @@ private static void BlockPasosClient(int clientId) { if (clientId < 0 || (AmongUsClient.Instance != null && clientId == AmongUsClient.Instance.ClientId)) return; - pasosBlockedClientIds.Add(clientId); + float expiresAt = UnityEngine.Time.realtimeSinceStartup + PasosBanSeconds; + pasosBlockedClientIds[clientId] = expiresAt; PlayerControl player = FindPlayerByClientId(clientId); string pName = player?.Data?.PlayerName ?? $"Client {clientId}"; - ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} blocked by Pasos Limit (Local)!"); + ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos blocked for 3m (Local)!"); if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; @@ -2536,9 +2555,74 @@ private static void BlockPasosClient(int clientId) } catch { } - ElysiumModMenuGUI.AddToBanList(fc, puid, pName, "Auto-banned for Pasos empty RPC spam"); - AmongUsClient.Instance.KickPlayer(clientId, true); - ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} auto-banned for Pasos Spam!"); + AddPasosTempHostBan(clientId, fc, puid, pName, expiresAt); + AmongUsClient.Instance.KickPlayer(clientId, false); + ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos host ban for 3m!"); + } + catch { } + } + + private static void AddPasosTempHostBan(int clientId, string friendCode, string puid, string name, float expiresAt) + { + try + { + pasosTempHostBans.RemoveAll(x => IsSamePasosBanTarget(x, clientId, friendCode, puid)); + pasosTempHostBans.Add(new PasosTempBanEntry + { + ClientId = clientId, + FriendCode = string.IsNullOrWhiteSpace(friendCode) ? "Unknown" : friendCode, + Puid = string.IsNullOrWhiteSpace(puid) ? clientId.ToString() : puid, + Name = string.IsNullOrWhiteSpace(name) ? $"Client {clientId}" : name, + ExpiresAt = expiresAt + }); + } + catch { } + } + + private static bool IsSamePasosBanTarget(PasosTempBanEntry entry, int clientId, string friendCode, string puid) + { + if (entry == null) return false; + if (entry.ClientId == clientId) return true; + + if (!string.IsNullOrWhiteSpace(friendCode) && friendCode != "Unknown" && + string.Equals(entry.FriendCode, friendCode, StringComparison.OrdinalIgnoreCase)) + return true; + + if (!string.IsNullOrWhiteSpace(puid) && puid != "Unknown" && + string.Equals(entry.Puid, puid, StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } + + public static void EnforceTempHostBans() + { + if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + + try + { + float now = UnityEngine.Time.realtimeSinceStartup; + pasosTempHostBans.RemoveAll(x => x == null || x.ExpiresAt <= now); + if (pasosTempHostBans.Count == 0 || PlayerControl.AllPlayerControls == null) return; + + foreach (PlayerControl pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; + + int clientId = (int)pc.OwnerId; + string fc = string.IsNullOrEmpty(pc.Data.FriendCode) ? "Unknown" : pc.Data.FriendCode; + string puid = clientId.ToString(); + + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(pc); + if (client != null) puid = client.Id.ToString(); + } + catch { } + + if (pasosTempHostBans.Any(x => IsSamePasosBanTarget(x, clientId, fc, puid))) + AmongUsClient.Instance.KickPlayer(clientId, false); + } } catch { } } @@ -2618,9 +2702,6 @@ public static bool Prefix(object[] __args) { int clientId = ExtractClientId(__args); Shield_PasosLimit_Patch.SetCurrentClientId(clientId); - - if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) - return false; } catch { } @@ -2709,11 +2790,6 @@ public static bool Prefix(object[] __args, ref Il2CppSystem.Collections.IEnumera { int clientId = ExtractClientId(__args); Shield_PasosLimit_Patch.SetCurrentClientId(clientId); - if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) - { - __result = EmptyPasosRoutine().WrapToIl2Cpp(); - return false; - } MessageReader parentReader = __args != null && __args.Length > 0 ? __args[0] as MessageReader : null; if (parentReader == null) return true; @@ -2766,11 +2842,6 @@ public static bool Prefix(object __instance, ref bool __result) { int clientId = ExtractClientId(__instance); Shield_PasosLimit_Patch.SetCurrentClientId(clientId); - if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) - { - __result = false; - return false; - } if (!HasEmptyGameDataReader(__instance)) return true; @@ -6624,6 +6695,11 @@ public void Update() } } try + { + Shield_PasosLimit_Patch.EnforceTempHostBans(); + } + catch { } + try { if (autoBanEnabled && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) { From 8c34432f80e7bde4b8798b86834ddcd3ae5aa10e Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 20:25:30 +0200 Subject: [PATCH 17/39] Restore full Anti-Pasos bans --- ElysiumModMenu.cs | 118 +++++++++------------------------------------- 1 file changed, 21 insertions(+), 97 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index a7cecdc..8eb2279 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -1065,7 +1065,7 @@ private void DrawAntiCheatTab() GUILayout.Space(5); enablePasosLimit = DrawToggle(enablePasosLimit, "Pasos Limit", 250); GUILayout.Space(5); - enableHostPasosBan = DrawToggle(enableHostPasosBan, L("Anti-Pasos Ban (3m)", "Анти-Pasos бан (3 мин)"), 250); + enableHostPasosBan = DrawToggle(enableHostPasosBan, L("Anti-Pasos Host Ban", "Анти-Pasos бан хоста"), 250); GUILayout.Space(15); GUILayout.Label(L("OTHER PROTECTIONS", "ПРОЧАЯ ЗАЩИТА"), headerStyle); @@ -2473,23 +2473,12 @@ public static class Shield_PasosLimit_Patch private const byte DroppedGameDataTag = 0; private const int PasosDropNotifyLimit = 1; private const float PasosWindow = 0.75f; - private const float PasosBanSeconds = 180f; private const float PasosNotifyCooldown = 2f; private static readonly Queue emptyRpcDrops = new Queue(); - private static readonly Dictionary pasosBlockedClientIds = new Dictionary(); - private static readonly List pasosTempHostBans = new List(); + private static readonly HashSet pasosBlockedClientIds = new HashSet(); private static float lastPasosNotify; private static int currentPasosClientId = -1; - private class PasosTempBanEntry - { - public int ClientId; - public string FriendCode; - public string Puid; - public string Name; - public float ExpiresAt; - } - public static void SetCurrentClientId(int clientId) { if (clientId >= 0) @@ -2498,14 +2487,7 @@ public static void SetCurrentClientId(int clientId) public static bool IsClientBlocked(int clientId) { - if (clientId < 0) return false; - - float now = UnityEngine.Time.realtimeSinceStartup; - if (!pasosBlockedClientIds.TryGetValue(clientId, out float expiresAt)) return false; - if (expiresAt > now) return true; - - pasosBlockedClientIds.Remove(clientId); - return false; + return clientId >= 0 && pasosBlockedClientIds.Contains(clientId); } public static void RecordDrop(int clientId = -1) @@ -2533,12 +2515,11 @@ private static void BlockPasosClient(int clientId) { if (clientId < 0 || (AmongUsClient.Instance != null && clientId == AmongUsClient.Instance.ClientId)) return; - float expiresAt = UnityEngine.Time.realtimeSinceStartup + PasosBanSeconds; - pasosBlockedClientIds[clientId] = expiresAt; + pasosBlockedClientIds.Add(clientId); PlayerControl player = FindPlayerByClientId(clientId); string pName = player?.Data?.PlayerName ?? $"Client {clientId}"; - ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos blocked for 3m (Local)!"); + ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos blocked (Local)!"); if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; @@ -2555,74 +2536,9 @@ private static void BlockPasosClient(int clientId) } catch { } - AddPasosTempHostBan(clientId, fc, puid, pName, expiresAt); - AmongUsClient.Instance.KickPlayer(clientId, false); - ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos host ban for 3m!"); - } - catch { } - } - - private static void AddPasosTempHostBan(int clientId, string friendCode, string puid, string name, float expiresAt) - { - try - { - pasosTempHostBans.RemoveAll(x => IsSamePasosBanTarget(x, clientId, friendCode, puid)); - pasosTempHostBans.Add(new PasosTempBanEntry - { - ClientId = clientId, - FriendCode = string.IsNullOrWhiteSpace(friendCode) ? "Unknown" : friendCode, - Puid = string.IsNullOrWhiteSpace(puid) ? clientId.ToString() : puid, - Name = string.IsNullOrWhiteSpace(name) ? $"Client {clientId}" : name, - ExpiresAt = expiresAt - }); - } - catch { } - } - - private static bool IsSamePasosBanTarget(PasosTempBanEntry entry, int clientId, string friendCode, string puid) - { - if (entry == null) return false; - if (entry.ClientId == clientId) return true; - - if (!string.IsNullOrWhiteSpace(friendCode) && friendCode != "Unknown" && - string.Equals(entry.FriendCode, friendCode, StringComparison.OrdinalIgnoreCase)) - return true; - - if (!string.IsNullOrWhiteSpace(puid) && puid != "Unknown" && - string.Equals(entry.Puid, puid, StringComparison.OrdinalIgnoreCase)) - return true; - - return false; - } - - public static void EnforceTempHostBans() - { - if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; - - try - { - float now = UnityEngine.Time.realtimeSinceStartup; - pasosTempHostBans.RemoveAll(x => x == null || x.ExpiresAt <= now); - if (pasosTempHostBans.Count == 0 || PlayerControl.AllPlayerControls == null) return; - - foreach (PlayerControl pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; - - int clientId = (int)pc.OwnerId; - string fc = string.IsNullOrEmpty(pc.Data.FriendCode) ? "Unknown" : pc.Data.FriendCode; - string puid = clientId.ToString(); - - try - { - var client = AmongUsClient.Instance.GetClientFromCharacter(pc); - if (client != null) puid = client.Id.ToString(); - } - catch { } - - if (pasosTempHostBans.Any(x => IsSamePasosBanTarget(x, clientId, fc, puid))) - AmongUsClient.Instance.KickPlayer(clientId, false); - } + ElysiumModMenuGUI.AddToBanList(fc, puid, pName, "Auto-banned for Pasos empty RPC spam"); + AmongUsClient.Instance.KickPlayer(clientId, true); + ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos room banned!"); } catch { } } @@ -2702,6 +2618,9 @@ public static bool Prefix(object[] __args) { int clientId = ExtractClientId(__args); Shield_PasosLimit_Patch.SetCurrentClientId(clientId); + + if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) + return false; } catch { } @@ -2790,6 +2709,11 @@ public static bool Prefix(object[] __args, ref Il2CppSystem.Collections.IEnumera { int clientId = ExtractClientId(__args); Shield_PasosLimit_Patch.SetCurrentClientId(clientId); + if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) + { + __result = EmptyPasosRoutine().WrapToIl2Cpp(); + return false; + } MessageReader parentReader = __args != null && __args.Length > 0 ? __args[0] as MessageReader : null; if (parentReader == null) return true; @@ -2842,6 +2766,11 @@ public static bool Prefix(object __instance, ref bool __result) { int clientId = ExtractClientId(__instance); Shield_PasosLimit_Patch.SetCurrentClientId(clientId); + if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) + { + __result = false; + return false; + } if (!HasEmptyGameDataReader(__instance)) return true; @@ -6695,11 +6624,6 @@ public void Update() } } try - { - Shield_PasosLimit_Patch.EnforceTempHostBans(); - } - catch { } - try { if (autoBanEnabled && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) { From be66f83c0e2360652a26550827e9efaf9955ccf4 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 20:35:18 +0200 Subject: [PATCH 18/39] Fix Anti-Pasos ban resolution --- ElysiumModMenu.cs | 159 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 123 insertions(+), 36 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 8eb2279..2a1934e 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -2472,13 +2472,18 @@ public static class Shield_PasosLimit_Patch private const byte RpcGameDataTag = 2; private const byte DroppedGameDataTag = 0; private const int PasosDropNotifyLimit = 1; - private const float PasosWindow = 0.75f; + private const float PasosWindow = 0.15f; private const float PasosNotifyCooldown = 2f; private static readonly Queue emptyRpcDrops = new Queue(); private static readonly HashSet pasosBlockedClientIds = new HashSet(); private static float lastPasosNotify; private static int currentPasosClientId = -1; + public static void BeginMessageContext(int clientId) + { + currentPasosClientId = clientId; + } + public static void SetCurrentClientId(int clientId) { if (clientId >= 0) @@ -2523,8 +2528,9 @@ private static void BlockPasosClient(int clientId) if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + int banClientId = GetKickClientId(player, clientId); string fc = string.IsNullOrEmpty(player?.Data?.FriendCode) ? "Unknown" : player.Data.FriendCode; - string puid = clientId.ToString(); + string puid = banClientId.ToString(); try { @@ -2537,12 +2543,30 @@ private static void BlockPasosClient(int clientId) catch { } ElysiumModMenuGUI.AddToBanList(fc, puid, pName, "Auto-banned for Pasos empty RPC spam"); - AmongUsClient.Instance.KickPlayer(clientId, true); + AmongUsClient.Instance.KickPlayer(banClientId, true); ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos room banned!"); } catch { } } + public static int GetKickClientId(PlayerControl player, int fallbackClientId) + { + try + { + if (player != null) + { + int ownerId = (int)player.OwnerId; + if (ownerId >= 0) return ownerId; + + if (player.Data != null && player.Data.ClientId >= 0) + return player.Data.ClientId; + } + } + catch { } + + return fallbackClientId; + } + public static PlayerControl FindPlayerByClientId(int clientId) { try @@ -2566,6 +2590,77 @@ public static PlayerControl FindPlayerByClientId(int clientId) return null; } + public static int ResolveClientId(object source) + { + return ResolveClientId(source, 0); + } + + private static int ResolveClientId(object source, int depth) + { + if (source == null || depth > 2 || source is MessageReader || source is SendOption) return -1; + + try + { + if (source is PlayerControl pc) + return GetKickClientId(pc, -1); + + int direct = ConvertNumericClientId(source); + if (direct >= 0) return direct; + + Type type = source.GetType(); + foreach (string name in new[] { "ClientId", "OwnerId", "Id", "clientId", "ownerId", "id" }) + { + PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + direct = ConvertNumericClientId(property?.GetValue(source)); + if (direct >= 0) return direct; + + FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + direct = ConvertNumericClientId(field?.GetValue(source)); + if (direct >= 0) return direct; + } + + foreach (string name in new[] { "Character", "Object", "Player", "Data", "character", "player" }) + { + PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + direct = ResolveClientId(property?.GetValue(source), depth + 1); + if (direct >= 0) return direct; + + FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + direct = ResolveClientId(field?.GetValue(source), depth + 1); + if (direct >= 0) return direct; + } + } + catch { } + + return -1; + } + + private static int ConvertNumericClientId(object value) + { + if (value == null) return -1; + + try + { + TypeCode code = Type.GetTypeCode(value.GetType()); + switch (code) + { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.UInt16: + case TypeCode.Int32: + case TypeCode.UInt32: + case TypeCode.Int64: + case TypeCode.UInt64: + long id = Convert.ToInt64(value); + return id >= 0 && id < 256 ? (int)id : -1; + } + } + catch { } + + return -1; + } + public static void Postfix(MessageReader __result) { DropEmptyRpcMessage(__result); @@ -2617,7 +2712,7 @@ public static bool Prefix(object[] __args) try { int clientId = ExtractClientId(__args); - Shield_PasosLimit_Patch.SetCurrentClientId(clientId); + Shield_PasosLimit_Patch.BeginMessageContext(clientId); if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) return false; @@ -2627,7 +2722,7 @@ public static bool Prefix(object[] __args) return true; } - private static int ExtractClientId(object[] args) + public static int ExtractClientId(object[] args) { if (args == null) return -1; @@ -2642,27 +2737,7 @@ private static int ExtractClientId(object[] args) private static int ExtractClientId(object source) { - if (source == null || source is MessageReader || source is SendOption) return -1; - - try - { - if (source is int directId) return directId; - - Type type = source.GetType(); - foreach (string name in new[] { "ClientId", "Id", "clientId", "id" }) - { - PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (property != null && property.PropertyType == typeof(int)) - return (int)property.GetValue(source); - - FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (field != null && field.FieldType == typeof(int)) - return (int)field.GetValue(source); - } - } - catch { } - - return -1; + return Shield_PasosLimit_Patch.ResolveClientId(source); } } @@ -2680,11 +2755,16 @@ public static bool Prefix(object[] __args) try { + int clientId = Shield_PasosLimit_HandleMessageContext_Patch.ExtractClientId(__args); + Shield_PasosLimit_Patch.SetCurrentClientId(clientId); + if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) + return false; + MessageReader parentReader = __args != null && __args.Length > 0 ? __args[0] as MessageReader : null; if (parentReader == null) return true; if (parentReader.Length > 0 && parentReader.BytesRemaining > 0) return true; - Shield_PasosLimit_Patch.RecordDrop(); + Shield_PasosLimit_Patch.RecordDrop(clientId); return false; } catch { } @@ -2728,12 +2808,13 @@ public static bool Prefix(object[] __args, ref Il2CppSystem.Collections.IEnumera return true; } - private static int ExtractClientId(object[] args) + public static int ExtractClientId(object[] args) { try { if (args == null || args.Length < 2 || args[1] == null) return -1; - return Convert.ToInt32(args[1]); + int clientId = Shield_PasosLimit_Patch.ResolveClientId(args[1]); + return clientId >= 0 ? clientId : Convert.ToInt32(args[1]); } catch { } @@ -2806,10 +2887,16 @@ private static int ExtractClientId(object routine) foreach (FieldInfo field in fields) { if (field.FieldType != typeof(int)) continue; - - int value = (int)field.GetValue(routine); - if (value >= 0 && value < 256) - return value; + string fieldName = field.Name.ToLowerInvariant(); + if (!fieldName.Contains("client") && + !fieldName.Contains("sender") && + !fieldName.Contains("source") && + !fieldName.Contains("player") && + !fieldName.Contains("id")) + continue; + + int value = Shield_PasosLimit_Patch.ResolveClientId(field.GetValue(routine)); + if (value >= 0) return value; } } catch { } @@ -3408,7 +3495,7 @@ private void SaveConfig() SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); SaveBool("M_PasosLimit", enablePasosLimit); - SaveBool("M_HostPasosBan", enableHostPasosBan); + SaveBool("M_AntiPasosHostBan", enableHostPasosBan); SaveBool("M_AutoHostEnabled", AutoHostEnabled); SaveBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); SaveBool("M_AutoHostNotifications", AutoHostNotifications); @@ -3567,7 +3654,7 @@ private void LoadConfig() blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); enablePasosLimit = LoadBool("M_PasosLimit", enablePasosLimit); - enableHostPasosBan = LoadBool("M_HostPasosBan", enableHostPasosBan); + enableHostPasosBan = LoadBool("M_AntiPasosHostBan", enableHostPasosBan); AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); AutoReturnLobbyAfterMatch = LoadBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); AutoHostNotifications = LoadBool("M_AutoHostNotifications", AutoHostNotifications); @@ -7270,7 +7357,7 @@ private void DrawElysiumModMenu(int windowID) public static bool blockChatFloodRpc = true; public static bool blockMeetingFloodRpc = true; public static bool enablePasosLimit = true; - public static bool enableHostPasosBan = false; + public static bool enableHostPasosBan = true; public static bool autoBanBrokenFriendCode = false; public static int chatRpcLimit = 1; public static float chatRpcWindow = 1f; From 2c30c30d60327994b84ea80f0e711345cd0467f4 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 20:46:00 +0200 Subject: [PATCH 19/39] Drop whole Pasos packets and fix bans --- ElysiumModMenu.cs | 142 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 129 insertions(+), 13 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 2a1934e..1ffab5c 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -2481,18 +2481,24 @@ public static class Shield_PasosLimit_Patch public static void BeginMessageContext(int clientId) { - currentPasosClientId = clientId; + if (IsValidClientId(clientId)) + currentPasosClientId = clientId; } public static void SetCurrentClientId(int clientId) { - if (clientId >= 0) + if (IsValidClientId(clientId)) currentPasosClientId = clientId; } public static bool IsClientBlocked(int clientId) { - return clientId >= 0 && pasosBlockedClientIds.Contains(clientId); + return IsValidClientId(clientId) && pasosBlockedClientIds.Contains(clientId); + } + + public static bool IsValidClientId(int clientId) + { + return clientId >= 0 && clientId < 256; } public static void RecordDrop(int clientId = -1) @@ -2503,8 +2509,10 @@ public static void RecordDrop(int clientId = -1) emptyRpcDrops.Enqueue(now); - int resolvedClientId = clientId >= 0 ? clientId : currentPasosClientId; - if (resolvedClientId >= 0 && !IsClientBlocked(resolvedClientId)) + int resolvedClientId = IsValidClientId(clientId) ? clientId : currentPasosClientId; + if (!IsValidClientId(resolvedClientId)) + resolvedClientId = ResolveSingleRemoteClientId(); + if (IsValidClientId(resolvedClientId) && !IsClientBlocked(resolvedClientId)) BlockPasosClient(resolvedClientId); if (emptyRpcDrops.Count >= PasosDropNotifyLimit && now - lastPasosNotify > PasosNotifyCooldown) @@ -2518,7 +2526,7 @@ private static void BlockPasosClient(int clientId) { try { - if (clientId < 0 || (AmongUsClient.Instance != null && clientId == AmongUsClient.Instance.ClientId)) return; + if (!IsValidClientId(clientId) || (AmongUsClient.Instance != null && clientId == AmongUsClient.Instance.ClientId)) return; pasosBlockedClientIds.Add(clientId); @@ -2556,15 +2564,15 @@ public static int GetKickClientId(PlayerControl player, int fallbackClientId) if (player != null) { int ownerId = (int)player.OwnerId; - if (ownerId >= 0) return ownerId; + if (IsValidClientId(ownerId)) return ownerId; - if (player.Data != null && player.Data.ClientId >= 0) + if (player.Data != null && IsValidClientId(player.Data.ClientId)) return player.Data.ClientId; } } catch { } - return fallbackClientId; + return IsValidClientId(fallbackClientId) ? fallbackClientId : -1; } public static PlayerControl FindPlayerByClientId(int clientId) @@ -2629,6 +2637,60 @@ private static int ResolveClientId(object source, int depth) direct = ResolveClientId(field?.GetValue(source), depth + 1); if (direct >= 0) return direct; } + + string typeName = type.FullName ?? type.Name; + if (typeName.IndexOf("Player", StringComparison.OrdinalIgnoreCase) >= 0 || + typeName.IndexOf("Client", StringComparison.OrdinalIgnoreCase) >= 0) + { + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (property.GetIndexParameters().Length > 0) continue; + + try + { + direct = ConvertNumericClientId(property.GetValue(source)); + if (direct >= 0) return direct; + } + catch { } + } + + foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + try + { + direct = ConvertNumericClientId(field.GetValue(source)); + if (direct >= 0) return direct; + } + catch { } + } + } + } + catch { } + + return -1; + } + + private static int ResolveSingleRemoteClientId() + { + try + { + if (PlayerControl.AllPlayerControls == null) return -1; + + int found = -1; + int count = 0; + foreach (PlayerControl pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; + + int ownerId = (int)pc.OwnerId; + if (!IsValidClientId(ownerId)) continue; + + found = ownerId; + count++; + if (count > 1) return -1; + } + + return count == 1 ? found : -1; } catch { } @@ -2661,6 +2723,45 @@ private static int ConvertNumericClientId(object value) return -1; } + public static bool TryDropWholePacketIfPasosSpam(MessageReader parentReader, int clientId = -1, bool skipGameId = false) + { + if (!ElysiumModMenuGUI.enablePasosLimit || parentReader == null) return false; + + try + { + int oldPos = parentReader.Position; + + if (skipGameId) + { + if (parentReader.BytesRemaining < 4) return false; + parentReader.ReadInt32(); + } + + while (parentReader.BytesRemaining > 0) + { + MessageReader child = parentReader.ReadMessage(); + if (child == null) continue; + + if (child.Tag == RpcGameDataTag && child.Length <= 0 && child.BytesRemaining <= 0) + { + parentReader.Position = parentReader.Length; + RecordDrop(clientId); + return true; + } + } + + parentReader.Position = oldPos; + } + catch + { + try { parentReader.Position = parentReader.Length; } catch { } + RecordDrop(clientId); + return true; + } + + return false; + } + public static void Postfix(MessageReader __result) { DropEmptyRpcMessage(__result); @@ -2762,6 +2863,8 @@ public static bool Prefix(object[] __args) MessageReader parentReader = __args != null && __args.Length > 0 ? __args[0] as MessageReader : null; if (parentReader == null) return true; + if (Shield_PasosLimit_Patch.TryDropWholePacketIfPasosSpam(parentReader, clientId, true)) + return false; if (parentReader.Length > 0 && parentReader.BytesRemaining > 0) return true; Shield_PasosLimit_Patch.RecordDrop(clientId); @@ -2797,6 +2900,11 @@ public static bool Prefix(object[] __args, ref Il2CppSystem.Collections.IEnumera MessageReader parentReader = __args != null && __args.Length > 0 ? __args[0] as MessageReader : null; if (parentReader == null) return true; + if (Shield_PasosLimit_Patch.TryDropWholePacketIfPasosSpam(parentReader, clientId)) + { + __result = EmptyPasosRoutine().WrapToIl2Cpp(); + return false; + } if (parentReader.Length > 0 && parentReader.BytesRemaining > 0) return true; Shield_PasosLimit_Patch.RecordDrop(clientId); @@ -2814,7 +2922,10 @@ public static int ExtractClientId(object[] args) { if (args == null || args.Length < 2 || args[1] == null) return -1; int clientId = Shield_PasosLimit_Patch.ResolveClientId(args[1]); - return clientId >= 0 ? clientId : Convert.ToInt32(args[1]); + if (Shield_PasosLimit_Patch.IsValidClientId(clientId)) return clientId; + + int fallback = Convert.ToInt32(args[1]); + return Shield_PasosLimit_Patch.IsValidClientId(fallback) ? fallback : -1; } catch { } @@ -2853,9 +2964,8 @@ public static bool Prefix(object __instance, ref bool __result) return false; } - if (!HasEmptyGameDataReader(__instance)) return true; + if (!HasPasosGameDataReader(__instance, clientId)) return true; - Shield_PasosLimit_Patch.RecordDrop(clientId); __result = false; return false; } @@ -2864,7 +2974,7 @@ public static bool Prefix(object __instance, ref bool __result) return true; } - private static bool HasEmptyGameDataReader(object routine) + private static bool HasPasosGameDataReader(object routine, int clientId) { FieldInfo[] fields = routine.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo field in fields) @@ -2873,6 +2983,12 @@ private static bool HasEmptyGameDataReader(object routine) MessageReader reader = field.GetValue(routine) as MessageReader; if (reader != null && reader.Length <= 0 && reader.BytesRemaining <= 0) + { + Shield_PasosLimit_Patch.RecordDrop(clientId); + return true; + } + + if (Shield_PasosLimit_Patch.TryDropWholePacketIfPasosSpam(reader, clientId)) return true; } From 413e6e9bb26f4280cd05b19590398579ec570022 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 5 Jun 2026 20:53:40 +0200 Subject: [PATCH 20/39] Restore safe Pasos drop path --- ElysiumModMenu.cs | 376 +++------------------------------------------- 1 file changed, 18 insertions(+), 358 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 1ffab5c..f9dfa04 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -1065,6 +1065,8 @@ private void DrawAntiCheatTab() GUILayout.Space(5); enablePasosLimit = DrawToggle(enablePasosLimit, "Pasos Limit", 250); GUILayout.Space(5); + enableLocalPasosBan = DrawToggle(enableLocalPasosBan, L("Anti-Pasos Local Ban", "Анти-Pasos локал бан"), 250); + GUILayout.Space(5); enableHostPasosBan = DrawToggle(enableHostPasosBan, L("Anti-Pasos Host Ban", "Анти-Pasos бан хоста"), 250); GUILayout.Space(15); @@ -2472,28 +2474,22 @@ public static class Shield_PasosLimit_Patch private const byte RpcGameDataTag = 2; private const byte DroppedGameDataTag = 0; private const int PasosDropNotifyLimit = 1; - private const float PasosWindow = 0.15f; + private const float PasosWindow = 0.01f; private const float PasosNotifyCooldown = 2f; private static readonly Queue emptyRpcDrops = new Queue(); private static readonly HashSet pasosBlockedClientIds = new HashSet(); + private static readonly HashSet pasosHostBannedClientIds = new HashSet(); private static float lastPasosNotify; private static int currentPasosClientId = -1; public static void BeginMessageContext(int clientId) { - if (IsValidClientId(clientId)) - currentPasosClientId = clientId; - } - - public static void SetCurrentClientId(int clientId) - { - if (IsValidClientId(clientId)) - currentPasosClientId = clientId; + currentPasosClientId = IsValidClientId(clientId) ? clientId : -1; } public static bool IsClientBlocked(int clientId) { - return IsValidClientId(clientId) && pasosBlockedClientIds.Contains(clientId); + return ElysiumModMenuGUI.enableLocalPasosBan && IsValidClientId(clientId) && pasosBlockedClientIds.Contains(clientId); } public static bool IsValidClientId(int clientId) @@ -2512,7 +2508,7 @@ public static void RecordDrop(int clientId = -1) int resolvedClientId = IsValidClientId(clientId) ? clientId : currentPasosClientId; if (!IsValidClientId(resolvedClientId)) resolvedClientId = ResolveSingleRemoteClientId(); - if (IsValidClientId(resolvedClientId) && !IsClientBlocked(resolvedClientId)) + if (IsValidClientId(resolvedClientId)) BlockPasosClient(resolvedClientId); if (emptyRpcDrops.Count >= PasosDropNotifyLimit && now - lastPasosNotify > PasosNotifyCooldown) @@ -2528,15 +2524,20 @@ private static void BlockPasosClient(int clientId) { if (!IsValidClientId(clientId) || (AmongUsClient.Instance != null && clientId == AmongUsClient.Instance.ClientId)) return; - pasosBlockedClientIds.Add(clientId); - PlayerControl player = FindPlayerByClientId(clientId); string pName = player?.Data?.PlayerName ?? $"Client {clientId}"; - ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos blocked (Local)!"); + + if (ElysiumModMenuGUI.enableLocalPasosBan && pasosBlockedClientIds.Add(clientId)) + { + ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos blocked (Local)!"); + } if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + if (!pasosHostBannedClientIds.Add(clientId)) return; int banClientId = GetKickClientId(player, clientId); + if (!IsValidClientId(banClientId)) return; + string fc = string.IsNullOrEmpty(player?.Data?.FriendCode) ? "Unknown" : player.Data.FriendCode; string puid = banClientId.ToString(); @@ -2723,45 +2724,6 @@ private static int ConvertNumericClientId(object value) return -1; } - public static bool TryDropWholePacketIfPasosSpam(MessageReader parentReader, int clientId = -1, bool skipGameId = false) - { - if (!ElysiumModMenuGUI.enablePasosLimit || parentReader == null) return false; - - try - { - int oldPos = parentReader.Position; - - if (skipGameId) - { - if (parentReader.BytesRemaining < 4) return false; - parentReader.ReadInt32(); - } - - while (parentReader.BytesRemaining > 0) - { - MessageReader child = parentReader.ReadMessage(); - if (child == null) continue; - - if (child.Tag == RpcGameDataTag && child.Length <= 0 && child.BytesRemaining <= 0) - { - parentReader.Position = parentReader.Length; - RecordDrop(clientId); - return true; - } - } - - parentReader.Position = oldPos; - } - catch - { - try { parentReader.Position = parentReader.Length; } catch { } - RecordDrop(clientId); - return true; - } - - return false; - } - public static void Postfix(MessageReader __result) { DropEmptyRpcMessage(__result); @@ -2784,15 +2746,6 @@ public static void DropEmptyRpcMessage(MessageReader reader) } } - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadMessageAsNewBuffer))] - public static class Shield_PasosLimit_ReadMessageAsNewBuffer_Patch - { - public static void Postfix(MessageReader __result) - { - Shield_PasosLimit_Patch.DropEmptyRpcMessage(__result); - } - } - [HarmonyPatch] public static class Shield_PasosLimit_HandleMessageContext_Patch { @@ -2842,302 +2795,6 @@ private static int ExtractClientId(object source) } } - [HarmonyPatch] - public static class Shield_PasosLimit_HandleGameData_Patch - { - public static MethodBase TargetMethod() - { - return AccessTools.Method(typeof(InnerNetClient), "HandleGameData", new[] { typeof(MessageReader) }); - } - - public static bool Prefix(object[] __args) - { - if (!ElysiumModMenuGUI.enablePasosLimit) return true; - - try - { - int clientId = Shield_PasosLimit_HandleMessageContext_Patch.ExtractClientId(__args); - Shield_PasosLimit_Patch.SetCurrentClientId(clientId); - if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) - return false; - - MessageReader parentReader = __args != null && __args.Length > 0 ? __args[0] as MessageReader : null; - if (parentReader == null) return true; - if (Shield_PasosLimit_Patch.TryDropWholePacketIfPasosSpam(parentReader, clientId, true)) - return false; - if (parentReader.Length > 0 && parentReader.BytesRemaining > 0) return true; - - Shield_PasosLimit_Patch.RecordDrop(clientId); - return false; - } - catch { } - - return true; - } - } - - [HarmonyPatch] - public static class Shield_PasosLimit_HandleGameDataInner_Patch - { - public static MethodBase TargetMethod() - { - return AccessTools.Method(typeof(InnerNetClient), "HandleGameDataInner", new[] { typeof(MessageReader), typeof(int) }); - } - - public static bool Prefix(object[] __args, ref Il2CppSystem.Collections.IEnumerator __result) - { - if (!ElysiumModMenuGUI.enablePasosLimit) return true; - - try - { - int clientId = ExtractClientId(__args); - Shield_PasosLimit_Patch.SetCurrentClientId(clientId); - if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) - { - __result = EmptyPasosRoutine().WrapToIl2Cpp(); - return false; - } - - MessageReader parentReader = __args != null && __args.Length > 0 ? __args[0] as MessageReader : null; - if (parentReader == null) return true; - if (Shield_PasosLimit_Patch.TryDropWholePacketIfPasosSpam(parentReader, clientId)) - { - __result = EmptyPasosRoutine().WrapToIl2Cpp(); - return false; - } - if (parentReader.Length > 0 && parentReader.BytesRemaining > 0) return true; - - Shield_PasosLimit_Patch.RecordDrop(clientId); - __result = EmptyPasosRoutine().WrapToIl2Cpp(); - return false; - } - catch { } - - return true; - } - - public static int ExtractClientId(object[] args) - { - try - { - if (args == null || args.Length < 2 || args[1] == null) return -1; - int clientId = Shield_PasosLimit_Patch.ResolveClientId(args[1]); - if (Shield_PasosLimit_Patch.IsValidClientId(clientId)) return clientId; - - int fallback = Convert.ToInt32(args[1]); - return Shield_PasosLimit_Patch.IsValidClientId(fallback) ? fallback : -1; - } - catch { } - - return -1; - } - - private static System.Collections.IEnumerator EmptyPasosRoutine() { yield break; } - } - - [HarmonyPatch] - public static class Shield_PasosLimit_HandleGameDataInner_MoveNext_Patch - { - public static IEnumerable TargetMethods() - { - foreach (Type nestedType in typeof(InnerNetClient).GetNestedTypes(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (!nestedType.Name.Contains("HandleGameDataInner")) continue; - - MethodInfo moveNext = AccessTools.Method(nestedType, "MoveNext"); - if (moveNext != null) - yield return moveNext; - } - } - - public static bool Prefix(object __instance, ref bool __result) - { - if (!ElysiumModMenuGUI.enablePasosLimit || __instance == null) return true; - - try - { - int clientId = ExtractClientId(__instance); - Shield_PasosLimit_Patch.SetCurrentClientId(clientId); - if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) - { - __result = false; - return false; - } - - if (!HasPasosGameDataReader(__instance, clientId)) return true; - - __result = false; - return false; - } - catch { } - - return true; - } - - private static bool HasPasosGameDataReader(object routine, int clientId) - { - FieldInfo[] fields = routine.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - foreach (FieldInfo field in fields) - { - if (!typeof(MessageReader).IsAssignableFrom(field.FieldType)) continue; - - MessageReader reader = field.GetValue(routine) as MessageReader; - if (reader != null && reader.Length <= 0 && reader.BytesRemaining <= 0) - { - Shield_PasosLimit_Patch.RecordDrop(clientId); - return true; - } - - if (Shield_PasosLimit_Patch.TryDropWholePacketIfPasosSpam(reader, clientId)) - return true; - } - - return false; - } - - private static int ExtractClientId(object routine) - { - try - { - FieldInfo[] fields = routine.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - foreach (FieldInfo field in fields) - { - if (field.FieldType != typeof(int)) continue; - string fieldName = field.Name.ToLowerInvariant(); - if (!fieldName.Contains("client") && - !fieldName.Contains("sender") && - !fieldName.Contains("source") && - !fieldName.Contains("player") && - !fieldName.Contains("id")) - continue; - - int value = Shield_PasosLimit_Patch.ResolveClientId(field.GetValue(routine)); - if (value >= 0) return value; - } - } - catch { } - - return -1; - } - } - - public static bool ShouldDropPasosEmptyRead(MessageReader reader) - { - if (!ElysiumModMenuGUI.enablePasosLimit || reader == null) return false; - - try - { - if (reader.Length > 0 || reader.BytesRemaining > 0) return false; - - Shield_PasosLimit_Patch.RecordDrop(); - return true; - } - catch { } - - return false; - } - - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadPackedInt32))] - public static class Shield_PasosLimit_ReadPackedInt32_Patch - { - public static bool Prefix(MessageReader __instance, ref int __result) - { - if (!ShouldDropPasosEmptyRead(__instance)) return true; - - __result = 0; - return false; - } - - public static Exception Finalizer(MessageReader __instance, Exception __exception, ref int __result) - { - if (__exception == null || !ElysiumModMenuGUI.enablePasosLimit) return __exception; - - try - { - if (__instance == null || __instance.Length > 0 || __instance.BytesRemaining > 0) return __exception; - - Shield_PasosLimit_Patch.RecordDrop(); - __result = 0; - return null; - } - catch { } - - return __exception; - } - } - - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadPackedUInt32))] - public static class Shield_PasosLimit_ReadPackedUInt32_Patch - { - public static bool Prefix(MessageReader __instance, ref uint __result) - { - if (!ShouldDropPasosEmptyRead(__instance)) return true; - - __result = 0u; - return false; - } - } - - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadUInt32))] - public static class Shield_PasosLimit_ReadUInt32_Patch - { - public static bool Prefix(MessageReader __instance, ref uint __result) - { - if (!ShouldDropPasosEmptyRead(__instance)) return true; - - __result = 0u; - return false; - } - } - - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadByte))] - public static class Shield_PasosLimit_ReadByte_Patch - { - public static bool Prefix(MessageReader __instance, ref byte __result) - { - if (!ShouldDropPasosEmptyRead(__instance)) return true; - - __result = 0; - return false; - } - } - - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadSByte))] - public static class Shield_PasosLimit_ReadSByte_Patch - { - public static bool Prefix(MessageReader __instance, ref sbyte __result) - { - if (!ShouldDropPasosEmptyRead(__instance)) return true; - - __result = 0; - return false; - } - } - - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadBoolean))] - public static class Shield_PasosLimit_ReadBoolean_Patch - { - public static bool Prefix(MessageReader __instance, ref bool __result) - { - if (!ShouldDropPasosEmptyRead(__instance)) return true; - - __result = false; - return false; - } - } - - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadString))] - public static class Shield_PasosLimit_ReadString_Patch - { - public static bool Prefix(MessageReader __instance, ref string __result) - { - if (!ShouldDropPasosEmptyRead(__instance)) return true; - - __result = string.Empty; - return false; - } - } - [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] public static class Shield_PetSpam_Patch { @@ -3611,6 +3268,7 @@ private void SaveConfig() SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); SaveBool("M_PasosLimit", enablePasosLimit); + SaveBool("M_AntiPasosLocalBan", enableLocalPasosBan); SaveBool("M_AntiPasosHostBan", enableHostPasosBan); SaveBool("M_AutoHostEnabled", AutoHostEnabled); SaveBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); @@ -3770,6 +3428,7 @@ private void LoadConfig() blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); enablePasosLimit = LoadBool("M_PasosLimit", enablePasosLimit); + enableLocalPasosBan = LoadBool("M_AntiPasosLocalBan", enableLocalPasosBan); enableHostPasosBan = LoadBool("M_AntiPasosHostBan", enableHostPasosBan); AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); AutoReturnLobbyAfterMatch = LoadBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); @@ -7473,6 +7132,7 @@ private void DrawElysiumModMenu(int windowID) public static bool blockChatFloodRpc = true; public static bool blockMeetingFloodRpc = true; public static bool enablePasosLimit = true; + public static bool enableLocalPasosBan = true; public static bool enableHostPasosBan = true; public static bool autoBanBrokenFriendCode = false; public static int chatRpcLimit = 1; From 2d2c38ac334dcf909d56cdc8ec41ceda45639cd3 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Sun, 7 Jun 2026 00:49:19 +0200 Subject: [PATCH 21/39] Add friend code line to ESP --- ElysiumModMenu.cs | 398 ++++++---------------------------------------- 1 file changed, 47 insertions(+), 351 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index f9dfa04..7215446 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -1063,11 +1063,6 @@ private void DrawAntiCheatTab() GUILayout.Space(5); blockChatFloodRpc = DrawToggle(blockChatFloodRpc, "Block Chat RPC Flood", 250); GUILayout.Space(5); - enablePasosLimit = DrawToggle(enablePasosLimit, "Pasos Limit", 250); - GUILayout.Space(5); - enableLocalPasosBan = DrawToggle(enableLocalPasosBan, L("Anti-Pasos Local Ban", "Анти-Pasos локал бан"), 250); - GUILayout.Space(5); - enableHostPasosBan = DrawToggle(enableHostPasosBan, L("Anti-Pasos Host Ban", "Анти-Pasos бан хоста"), 250); GUILayout.Space(15); GUILayout.Label(L("OTHER PROTECTIONS", "ПРОЧАЯ ЗАЩИТА"), headerStyle); @@ -2468,333 +2463,6 @@ private void DrawVisualsInGame() public static bool enableLocalPetSpamDrop = true; public static bool enableHostPetSpamBan = false; - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadMessage))] - public static class Shield_PasosLimit_Patch - { - private const byte RpcGameDataTag = 2; - private const byte DroppedGameDataTag = 0; - private const int PasosDropNotifyLimit = 1; - private const float PasosWindow = 0.01f; - private const float PasosNotifyCooldown = 2f; - private static readonly Queue emptyRpcDrops = new Queue(); - private static readonly HashSet pasosBlockedClientIds = new HashSet(); - private static readonly HashSet pasosHostBannedClientIds = new HashSet(); - private static float lastPasosNotify; - private static int currentPasosClientId = -1; - - public static void BeginMessageContext(int clientId) - { - currentPasosClientId = IsValidClientId(clientId) ? clientId : -1; - } - - public static bool IsClientBlocked(int clientId) - { - return ElysiumModMenuGUI.enableLocalPasosBan && IsValidClientId(clientId) && pasosBlockedClientIds.Contains(clientId); - } - - public static bool IsValidClientId(int clientId) - { - return clientId >= 0 && clientId < 256; - } - - public static void RecordDrop(int clientId = -1) - { - float now = UnityEngine.Time.time; - while (emptyRpcDrops.Count > 0 && emptyRpcDrops.Peek() < now - PasosWindow) - emptyRpcDrops.Dequeue(); - - emptyRpcDrops.Enqueue(now); - - int resolvedClientId = IsValidClientId(clientId) ? clientId : currentPasosClientId; - if (!IsValidClientId(resolvedClientId)) - resolvedClientId = ResolveSingleRemoteClientId(); - if (IsValidClientId(resolvedClientId)) - BlockPasosClient(resolvedClientId); - - if (emptyRpcDrops.Count >= PasosDropNotifyLimit && now - lastPasosNotify > PasosNotifyCooldown) - { - lastPasosNotify = now; - ElysiumModMenuGUI.ShowNotification($"[SHIELD] Pasos Limit: dropped empty RPC spam ({emptyRpcDrops.Count}/{PasosWindow:0.00}s)"); - } - } - - private static void BlockPasosClient(int clientId) - { - try - { - if (!IsValidClientId(clientId) || (AmongUsClient.Instance != null && clientId == AmongUsClient.Instance.ClientId)) return; - - PlayerControl player = FindPlayerByClientId(clientId); - string pName = player?.Data?.PlayerName ?? $"Client {clientId}"; - - if (ElysiumModMenuGUI.enableLocalPasosBan && pasosBlockedClientIds.Add(clientId)) - { - ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos blocked (Local)!"); - } - - if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; - if (!pasosHostBannedClientIds.Add(clientId)) return; - - int banClientId = GetKickClientId(player, clientId); - if (!IsValidClientId(banClientId)) return; - - string fc = string.IsNullOrEmpty(player?.Data?.FriendCode) ? "Unknown" : player.Data.FriendCode; - string puid = banClientId.ToString(); - - try - { - if (player != null) - { - var client = AmongUsClient.Instance.GetClientFromCharacter(player); - if (client != null) puid = client.Id.ToString(); - } - } - catch { } - - ElysiumModMenuGUI.AddToBanList(fc, puid, pName, "Auto-banned for Pasos empty RPC spam"); - AmongUsClient.Instance.KickPlayer(banClientId, true); - ElysiumModMenuGUI.ShowNotification($"[SHIELD] {pName} Anti-Pasos room banned!"); - } - catch { } - } - - public static int GetKickClientId(PlayerControl player, int fallbackClientId) - { - try - { - if (player != null) - { - int ownerId = (int)player.OwnerId; - if (IsValidClientId(ownerId)) return ownerId; - - if (player.Data != null && IsValidClientId(player.Data.ClientId)) - return player.Data.ClientId; - } - } - catch { } - - return IsValidClientId(fallbackClientId) ? fallbackClientId : -1; - } - - public static PlayerControl FindPlayerByClientId(int clientId) - { - try - { - if (PlayerControl.AllPlayerControls == null) return null; - - foreach (PlayerControl pc in PlayerControl.AllPlayerControls) - { - if (pc == null) continue; - if ((int)pc.OwnerId == clientId) return pc; - - try - { - if (pc.Data != null && pc.Data.ClientId == clientId) return pc; - } - catch { } - } - } - catch { } - - return null; - } - - public static int ResolveClientId(object source) - { - return ResolveClientId(source, 0); - } - - private static int ResolveClientId(object source, int depth) - { - if (source == null || depth > 2 || source is MessageReader || source is SendOption) return -1; - - try - { - if (source is PlayerControl pc) - return GetKickClientId(pc, -1); - - int direct = ConvertNumericClientId(source); - if (direct >= 0) return direct; - - Type type = source.GetType(); - foreach (string name in new[] { "ClientId", "OwnerId", "Id", "clientId", "ownerId", "id" }) - { - PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - direct = ConvertNumericClientId(property?.GetValue(source)); - if (direct >= 0) return direct; - - FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - direct = ConvertNumericClientId(field?.GetValue(source)); - if (direct >= 0) return direct; - } - - foreach (string name in new[] { "Character", "Object", "Player", "Data", "character", "player" }) - { - PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - direct = ResolveClientId(property?.GetValue(source), depth + 1); - if (direct >= 0) return direct; - - FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - direct = ResolveClientId(field?.GetValue(source), depth + 1); - if (direct >= 0) return direct; - } - - string typeName = type.FullName ?? type.Name; - if (typeName.IndexOf("Player", StringComparison.OrdinalIgnoreCase) >= 0 || - typeName.IndexOf("Client", StringComparison.OrdinalIgnoreCase) >= 0) - { - foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (property.GetIndexParameters().Length > 0) continue; - - try - { - direct = ConvertNumericClientId(property.GetValue(source)); - if (direct >= 0) return direct; - } - catch { } - } - - foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - try - { - direct = ConvertNumericClientId(field.GetValue(source)); - if (direct >= 0) return direct; - } - catch { } - } - } - } - catch { } - - return -1; - } - - private static int ResolveSingleRemoteClientId() - { - try - { - if (PlayerControl.AllPlayerControls == null) return -1; - - int found = -1; - int count = 0; - foreach (PlayerControl pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; - - int ownerId = (int)pc.OwnerId; - if (!IsValidClientId(ownerId)) continue; - - found = ownerId; - count++; - if (count > 1) return -1; - } - - return count == 1 ? found : -1; - } - catch { } - - return -1; - } - - private static int ConvertNumericClientId(object value) - { - if (value == null) return -1; - - try - { - TypeCode code = Type.GetTypeCode(value.GetType()); - switch (code) - { - case TypeCode.Byte: - case TypeCode.SByte: - case TypeCode.Int16: - case TypeCode.UInt16: - case TypeCode.Int32: - case TypeCode.UInt32: - case TypeCode.Int64: - case TypeCode.UInt64: - long id = Convert.ToInt64(value); - return id >= 0 && id < 256 ? (int)id : -1; - } - } - catch { } - - return -1; - } - - public static void Postfix(MessageReader __result) - { - DropEmptyRpcMessage(__result); - } - - public static void DropEmptyRpcMessage(MessageReader reader) - { - if (!ElysiumModMenuGUI.enablePasosLimit || reader == null) return; - - try - { - if (reader.Tag != RpcGameDataTag || reader.BytesRemaining > 0 || reader.Length > 0) return; - - reader.Tag = DroppedGameDataTag; - reader.Position = reader.Length; - - RecordDrop(); - } - catch { } - } - } - - [HarmonyPatch] - public static class Shield_PasosLimit_HandleMessageContext_Patch - { - public static IEnumerable TargetMethods() - { - foreach (MethodInfo method in typeof(InnerNetClient).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (method.Name != "HandleMessage") continue; - if (method.GetParameters().Any(p => p.ParameterType == typeof(MessageReader))) - yield return method; - } - } - - public static bool Prefix(object[] __args) - { - if (!ElysiumModMenuGUI.enablePasosLimit) return true; - - try - { - int clientId = ExtractClientId(__args); - Shield_PasosLimit_Patch.BeginMessageContext(clientId); - - if (Shield_PasosLimit_Patch.IsClientBlocked(clientId)) - return false; - } - catch { } - - return true; - } - - public static int ExtractClientId(object[] args) - { - if (args == null) return -1; - - foreach (object arg in args) - { - int clientId = ExtractClientId(arg); - if (clientId >= 0) return clientId; - } - - return -1; - } - - private static int ExtractClientId(object source) - { - return Shield_PasosLimit_Patch.ResolveClientId(source); - } - } - [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] public static class Shield_PetSpam_Patch { @@ -3266,11 +2934,7 @@ private void SaveConfig() SaveBool("M_BlockSabotageRPC", blockSabotageRPC); SaveBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); - SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); - SaveBool("M_PasosLimit", enablePasosLimit); - SaveBool("M_AntiPasosLocalBan", enableLocalPasosBan); - SaveBool("M_AntiPasosHostBan", enableHostPasosBan); - SaveBool("M_AutoHostEnabled", AutoHostEnabled); + SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); SaveBool("M_AutoHostEnabled", AutoHostEnabled); SaveBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); SaveBool("M_AutoHostNotifications", AutoHostNotifications); SaveBool("M_AutoHostForceLastMinute", AutoHostForceLastMinute); @@ -3426,11 +3090,7 @@ private void LoadConfig() blockSabotageRPC = LoadBool("M_BlockSabotageRPC", blockSabotageRPC); blockGameRpcInLobby = LoadBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); - blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); - enablePasosLimit = LoadBool("M_PasosLimit", enablePasosLimit); - enableLocalPasosBan = LoadBool("M_AntiPasosLocalBan", enableLocalPasosBan); - enableHostPasosBan = LoadBool("M_AntiPasosHostBan", enableHostPasosBan); - AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); + blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); AutoReturnLobbyAfterMatch = LoadBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); AutoHostNotifications = LoadBool("M_AutoHostNotifications", AutoHostNotifications); AutoHostForceLastMinute = LoadBool("M_AutoHostForceLastMinute", AutoHostForceLastMinute); @@ -5152,7 +4812,7 @@ private void DrawPlayersHistoryTab() { GUILayout.BeginVertical(boxStyle); GUILayout.Label($"{e.Name} ({e.Platform})", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 13 }); - GUILayout.Label($"FC: {e.FriendCode} | PUID: {e.Puid}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); + GUILayout.Label($"FC: {e.FriendCode}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); GUILayout.Label($"Lv: {e.Level} | Last: {e.LastSeenUtc:HH:mm:ss}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); GUILayout.EndVertical(); GUILayout.Space(2); @@ -7130,11 +6790,7 @@ private void DrawElysiumModMenu(int windowID) public static bool blockSabotageRPC = true; public static bool blockGameRpcInLobby = true; public static bool blockChatFloodRpc = true; - public static bool blockMeetingFloodRpc = true; - public static bool enablePasosLimit = true; - public static bool enableLocalPasosBan = true; - public static bool enableHostPasosBan = true; - public static bool autoBanBrokenFriendCode = false; + public static bool blockMeetingFloodRpc = true; public static bool autoBanBrokenFriendCode = false; public static int chatRpcLimit = 1; public static float chatRpcWindow = 1f; public static int meetingRpcLimit = 2; @@ -7431,6 +7087,37 @@ private static string CompactEspValue(string value, int maxLength = 24) return value; } + private static int? GetClientPing(ClientData client) + { + try + { + if (client == null) return null; + + string[] names = { "Ping", "ping", "Latency", "latency", "RoundTripTime", "roundTripTime", "Rtt", "RTT" }; + Type type = client.GetType(); + + foreach (string name in names) + { + PropertyInfo prop = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (prop != null) + { + object value = prop.GetValue(client); + if (value != null) return Mathf.RoundToInt(Convert.ToSingle(value)); + } + + FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (field != null) + { + object value = field.GetValue(client); + if (value != null) return Mathf.RoundToInt(Convert.ToSingle(value)); + } + } + } + catch { } + + return null; + } + public static string BuildESPInfoLine(NetworkedPlayerInfo info) { if (info == null) return string.Empty; @@ -7438,6 +7125,7 @@ public static string BuildESPInfoLine(NetworkedPlayerInfo info) int level = 0; string platform = "Unknown"; bool isHost = false; + int? ping = null; try { level = (int)info.PlayerLevel + 1; } catch { } @@ -7448,6 +7136,7 @@ public static string BuildESPInfoLine(NetworkedPlayerInfo info) { platform = GetPlatform(client); isHost = AmongUsClient.Instance.GetHost() == client; + ping = GetClientPing(client); } } catch { } @@ -7459,15 +7148,22 @@ public static string BuildESPInfoLine(NetworkedPlayerInfo info) platform = $"{platform} spf"; } - string fc = CompactEspValue(GetDisplayedFriendCode(info)); List parts = new List(); if (isHost) parts.Add("Host"); parts.Add($"Lv:{level}"); parts.Add(platform); - parts.Add(fc); + if (ping.HasValue) parts.Add($"Ping:{ping.Value}ms"); return string.Join(" - ", parts); } + public static string BuildESPInfoBlock(NetworkedPlayerInfo info) + { + if (info == null) return string.Empty; + + string fc = CompactEspValue(GetDisplayedFriendCode(info, "No Friend Code"), 18); + return $"{BuildESPInfoLine(info)}\nFC: {fc}"; + } + public static Color GetRoleColor(int roleId, Color fallbackColor) { switch (roleId) @@ -7649,7 +7345,7 @@ public static string GetESPNameTag(NetworkedPlayerInfo info, string originalName if (showPlayerInfo) { string accentHex = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); - newName = $"{BuildESPInfoLine(info)}\n{newName}"; + newName = $"{BuildESPInfoBlock(info)}\n{newName}"; } if (seeKillCooldown && info.Role != null && info.PlayerId != PlayerControl.LocalPlayer?.PlayerId) { From d3fecd1a980ae950dcfecff410651c5511fe81fe Mon Sep 17 00:00:00 2001 From: meowchelo Date: Sun, 7 Jun 2026 01:08:16 +0200 Subject: [PATCH 22/39] Remove meeting flood limiter --- ElysiumModMenu.cs | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 7215446..4b5c2bc 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -1059,8 +1059,6 @@ private void DrawAntiCheatTab() GUILayout.Space(5); blockGameRpcInLobby = DrawToggle(blockGameRpcInLobby, "Block Game RPC in Lobby", 250); GUILayout.Space(5); - blockMeetingFloodRpc = DrawToggle(blockMeetingFloodRpc, "Block Meeting RPC Flood", 250); - GUILayout.Space(5); blockChatFloodRpc = DrawToggle(blockChatFloodRpc, "Block Chat RPC Flood", 250); GUILayout.Space(5); @@ -1198,7 +1196,6 @@ public static void Flag(PlayerControl player, string reason) public static class Anticheat_PlayerControl_RPC { private static readonly Dictionary> chatRpcTimes = new Dictionary>(); - private static readonly Dictionary> meetingRpcTimes = new Dictionary>(); private static readonly HashSet lobbyGameRpcs = new HashSet { (byte)RpcCalls.MurderPlayer, @@ -1231,8 +1228,7 @@ public static bool Prefix(PlayerControl __instance, byte callId, Hazel.MessageRe if (!ElysiumModMenuGUI.blockSpoofRPC && !ElysiumModMenuGUI.blockSabotageRPC && !ElysiumModMenuGUI.blockGameRpcInLobby && - !ElysiumModMenuGUI.blockChatFloodRpc && - !ElysiumModMenuGUI.blockMeetingFloodRpc) return true; + !ElysiumModMenuGUI.blockChatFloodRpc) return true; if (__instance == null || __instance == PlayerControl.LocalPlayer || __instance.Data == null) return true; int oldPos = reader.Position; @@ -1260,16 +1256,6 @@ public static bool Prefix(PlayerControl __instance, byte callId, Hazel.MessageRe } } - if (!isCheat && ElysiumModMenuGUI.blockMeetingFloodRpc && - (callId == (byte)RpcCalls.StartMeeting || callId == (byte)RpcCalls.ReportDeadBody)) - { - if (IsFlooded(meetingRpcTimes, __instance.PlayerId, ElysiumModMenuGUI.meetingRpcLimit, ElysiumModMenuGUI.meetingRpcWindow)) - { - isCheat = true; - cheatReason = "Meeting RPC flood"; - } - } - if (!isCheat && ElysiumModMenuGUI.blockSpoofRPC) { if (callId == (byte)RpcCalls.SetColor) @@ -2934,7 +2920,7 @@ private void SaveConfig() SaveBool("M_BlockSabotageRPC", blockSabotageRPC); SaveBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); - SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); SaveBool("M_AutoHostEnabled", AutoHostEnabled); + SaveBool("M_AutoHostEnabled", AutoHostEnabled); SaveBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); SaveBool("M_AutoHostNotifications", AutoHostNotifications); SaveBool("M_AutoHostForceLastMinute", AutoHostForceLastMinute); @@ -3090,7 +3076,7 @@ private void LoadConfig() blockSabotageRPC = LoadBool("M_BlockSabotageRPC", blockSabotageRPC); blockGameRpcInLobby = LoadBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); - blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); + AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); AutoReturnLobbyAfterMatch = LoadBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); AutoHostNotifications = LoadBool("M_AutoHostNotifications", AutoHostNotifications); AutoHostForceLastMinute = LoadBool("M_AutoHostForceLastMinute", AutoHostForceLastMinute); @@ -6790,11 +6776,9 @@ private void DrawElysiumModMenu(int windowID) public static bool blockSabotageRPC = true; public static bool blockGameRpcInLobby = true; public static bool blockChatFloodRpc = true; - public static bool blockMeetingFloodRpc = true; public static bool autoBanBrokenFriendCode = false; + public static bool autoBanBrokenFriendCode = false; public static int chatRpcLimit = 1; public static float chatRpcWindow = 1f; - public static int meetingRpcLimit = 2; - public static float meetingRpcWindow = 9999f; [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleAnimation))] public static class PlayerPhysics_HandleAnimation From af928ff2b9b3ed370ca0c91a307f78a9fca7c1e8 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 12 Jun 2026 17:46:49 +0200 Subject: [PATCH 23/39] Add ghost chat colors and force role controls --- ElysiumModMenu.cs | 2908 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 2481 insertions(+), 427 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 4b5c2bc..924b672 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -19,7 +19,9 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Reflection; +using System.Text; using System.Text.RegularExpressions; using TMPro; using UnityEngine; @@ -55,6 +57,9 @@ public class Plugin : BasePlugin public static ConfigEntry UnlockCosmeticsConfig; public static ConfigEntry MoreLobbyInfoConfig; public static ConfigEntry EnableChatDarkModeConfig; + public static ConfigEntry GhostChatColorConfig; + public static ConfigEntry EnableAnomalyLogReportsConfig; + public static ConfigEntry ShowEspFriendCodeConfig; public override void Load() { @@ -72,7 +77,15 @@ public override void Load() System.IO.File.Create(banFile).Dispose(); } - MenuConfig = new ConfigFile(System.IO.Path.Combine(ElysiumFolder, "ElysiumModMenu.cfg"), true); + string friendEspFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumFriendEspIgnore.txt"); + if (!System.IO.File.Exists(friendEspFile)) + { + System.IO.File.WriteAllText(friendEspFile, "# One nickname, Friend Code, or PUID per line. Matching players will not show ESP info.\n"); + } + + string configPath = System.IO.Path.Combine(ElysiumFolder, "ElysiumModMenu.cfg"); + RemoveLegacyPlaintextWebhookConfig(configPath); + MenuConfig = new ConfigFile(configPath, true); RpcSpoofDelayConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "RpcDelay", 4f, ""); MenuKeybind = MenuConfig.Bind("ElysiumModMenu.GUI", "Keybind", KeyCode.Insert, ""); SpoofedLevel = MenuConfig.Bind("ElysiumModMenu.Spoofing", "Level", "100", ""); @@ -87,7 +100,9 @@ public override void Load() UnlockCosmeticsConfig = MenuConfig.Bind("ElysiumModMenu.General", "UnlockCosmetics", true, ""); MoreLobbyInfoConfig = MenuConfig.Bind("ElysiumModMenu.Visuals", "MoreLobbyInfo", true, ""); EnableChatDarkModeConfig = MenuConfig.Bind("ElysiumModMenu.Chat", "EnableChatDarkMode", true, "Turns the custom dark chat input and bubble colors on/off."); - + GhostChatColorConfig = MenuConfig.Bind("ElysiumModMenu.Chat", "GhostChatColor", "#D7B8FF", "Hex color for visible ghost chat messages."); + EnableAnomalyLogReportsConfig = MenuConfig.Bind("ElysiumModMenu.Diagnostics", "EnableAnomalyLogReports", true, "Yes/No: sending freeze/overload logs to the mod author. Note: this does not affect your performance, nor does it steal your data or anything like that. It is strictly needed for quick anti-cheat fixes."); + ShowEspFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Visuals", "ShowEspFriendCode", true, "Show Friend Code in ESP player info."); ClassInjector.RegisterTypeInIl2Cpp(); var guiObject = new GameObject("ElysiumModMenu_Object"); @@ -98,7 +113,375 @@ public override void Load() var harmony = new Harmony("com.elysiummodmenu.harmony"); harmony.PatchAll(); } + + private static void RemoveLegacyPlaintextWebhookConfig(string configPath) + { + try + { + if (string.IsNullOrWhiteSpace(configPath) || !System.IO.File.Exists(configPath)) return; + + List lines = System.IO.File.ReadAllLines(configPath).ToList(); + string[] legacyKeys = { "AnomalyLogWebhookUrl", "EnableDiagnostics", "EndpointUrl", "AuthKey", "IncludePuid" }; + bool changed = false; + + for (int i = lines.Count - 1; i >= 0; i--) + { + string trimmed = lines[i].TrimStart(); + if (!legacyKeys.Any(key => trimmed.StartsWith(key, StringComparison.OrdinalIgnoreCase))) continue; + + int start = i; + while (start > 0) + { + string previous = lines[start - 1].TrimStart(); + if (!previous.StartsWith("#") && previous.Length != 0) break; + start--; + if (previous.Length == 0) break; + } + + lines.RemoveRange(start, i - start + 1); + changed = true; + } + + if (!changed) return; + System.IO.File.WriteAllLines(configPath, lines.ToArray()); + } + catch { } + } + } + + public static class DiscordStatusReporter + { + private const bool Enabled = true; + private const bool IncludePuid = true; + private const byte WebhookXorKey = 0x37; + private const int MaxDiagnosticAttachments = 5; + private const long MaxDiagnosticAttachmentBytes = 7L * 1024L * 1024L; + private const long MaxDiagnosticTotalAttachmentBytes = 20L * 1024L * 1024L; + private const long LargeLogTailBytes = 1024L * 1024L; + private const int MaxDiagnosticExcerptLines = 5000; + private const int DiagnosticExcerptContextBefore = 8; + private const int DiagnosticExcerptContextAfter = 10; + private static string decodedWebhookUrl; + private static readonly byte[] EncodedWebhookUrl = new byte[] + { + 0x5F, 0x43, 0x43, 0x47, 0x44, 0x0D, 0x18, 0x18, 0x53, 0x5E, 0x44, 0x54, 0x58, 0x45, 0x53, 0x19, + 0x54, 0x58, 0x5A, 0x18, 0x56, 0x47, 0x5E, 0x18, 0x40, 0x52, 0x55, 0x5F, 0x58, 0x58, 0x5C, 0x44, + 0x18, 0x06, 0x02, 0x06, 0x04, 0x0E, 0x01, 0x03, 0x06, 0x0E, 0x02, 0x0F, 0x0E, 0x0E, 0x01, 0x03, + 0x06, 0x0F, 0x01, 0x01, 0x18, 0x6E, 0x75, 0x5B, 0x5B, 0x56, 0x5D, 0x7A, 0x4E, 0x42, 0x64, 0x0E, + 0x40, 0x02, 0x50, 0x00, 0x50, 0x5E, 0x72, 0x5B, 0x6F, 0x0F, 0x46, 0x5C, 0x5B, 0x7C, 0x5A, 0x4E, + 0x02, 0x7C, 0x44, 0x54, 0x56, 0x05, 0x0F, 0x5E, 0x66, 0x4F, 0x43, 0x58, 0x56, 0x50, 0x00, 0x6E, + 0x62, 0x40, 0x52, 0x54, 0x5D, 0x4F, 0x7D, 0x58, 0x0E, 0x78, 0x63, 0x6D, 0x42, 0x05, 0x4E, 0x72, + 0x7B, 0x79, 0x6D, 0x07, 0x64, 0x78, 0x50, 0x56, 0x52 + }; + private static readonly HttpClient Client = new HttpClient { Timeout = TimeSpan.FromSeconds(8) }; + + public static bool IsEnabled => Enabled; + public static bool IncludeLocalPuid => IncludePuid; + public static string ConfiguredWebhookUrl => decodedWebhookUrl ??= DecodeWebhookUrl(); + + private static string DecodeWebhookUrl() + { + byte[] decoded = new byte[EncodedWebhookUrl.Length]; + for (int i = 0; i < EncodedWebhookUrl.Length; i++) + decoded[i] = (byte)(EncodedWebhookUrl[i] ^ WebhookXorKey); + return Encoding.UTF8.GetString(decoded); + } + + public static bool IsValidWebhookUrl(string webhookUrl) + { + if (string.IsNullOrWhiteSpace(webhookUrl)) return false; + string value = webhookUrl.Trim(); + return value.StartsWith("https://discord.com/api/webhooks/", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("https://discordapp.com/api/webhooks/", StringComparison.OrdinalIgnoreCase); + } + + public static void SendLaunchStatus(string webhookUrl, string nickname, string friendCode, string puid, string platform, int level, string roomCode, bool includePuid) + { + if (!IsValidWebhookUrl(webhookUrl)) return; + _ = SendLaunchStatusAsync(webhookUrl.Trim(), nickname, friendCode, puid, platform, level, roomCode, includePuid); + } + + public static void SendDiagnosticAlert(string title, string message) + { + string webhookUrl = ConfiguredWebhookUrl; + SendDiagnosticAlert(webhookUrl, title, message); + } + + public static void SendDiagnosticAlert(string webhookUrl, string title, string message, bool waitForCompletion = false) + { + SendDiagnosticAlert(webhookUrl, title, message, waitForCompletion, null); + } + + public static void SendDiagnosticAlert(string webhookUrl, string title, string message, bool waitForCompletion, IEnumerable attachmentPaths) + { + if (!IsValidWebhookUrl(webhookUrl)) return; + System.Threading.Tasks.Task sendTask = SendDiagnosticAlertAsync(webhookUrl.Trim(), title, message, attachmentPaths); + if (!waitForCompletion) return; + + try + { + if (!sendTask.Wait(TimeSpan.FromSeconds(10))) + System.Console.WriteLine("[ElysiumModMenu] Diagnostic webhook timeout after 10s."); + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Diagnostic webhook wait failed: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static async System.Threading.Tasks.Task SendLaunchStatusAsync(string webhookUrl, string nickname, string friendCode, string puid, string platform, int level, string roomCode, bool includePuid) + { + try + { + StringBuilder fields = new StringBuilder(); + AppendField(fields, "Статус", "Запущено", true); + AppendField(fields, "Ник", string.IsNullOrWhiteSpace(nickname) ? "Unknown" : nickname, true); + AppendField(fields, "Friend Code", string.IsNullOrWhiteSpace(friendCode) ? "Hidden" : friendCode, true); + if (includePuid) AppendField(fields, "PUID", string.IsNullOrWhiteSpace(puid) ? "Unknown" : puid, true); + AppendField(fields, "Платформа", string.IsNullOrWhiteSpace(platform) ? "Unknown" : platform, true); + AppendField(fields, "Уровень", level > 0 ? level.ToString() : "Unknown", true); + AppendField(fields, "Комната", string.IsNullOrWhiteSpace(roomCode) ? "Нет" : roomCode, true); + + string payload = + "{" + + "\"username\":\"ElysiumModMenu\"," + + "\"embeds\":[{" + + "\"title\":\"ElysiumModMenu запущен\"," + + "\"color\":16755228," + + "\"timestamp\":\"" + DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + "\"," + + "\"fields\":[" + fields + "]" + + "}]" + + "}"; + + using StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); + using HttpResponseMessage response = await Client.PostAsync(webhookUrl, content); + System.Console.WriteLine($"[ElysiumModMenu] Launch webhook result: {(int)response.StatusCode} {response.ReasonPhrase}"); + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Diagnostic webhook failed: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static async System.Threading.Tasks.Task SendDiagnosticAlertAsync(string webhookUrl, string title, string message, IEnumerable attachmentPaths = null) + { + try + { + string safeTitle = string.IsNullOrWhiteSpace(title) ? "Diagnostic alert" : title.Trim(); + string safeMessage = string.IsNullOrWhiteSpace(message) ? "No details" : message.Trim(); + if (safeMessage.Length > 3500) safeMessage = safeMessage.Substring(0, 3500); + + string payload = + "{" + + "\"username\":\"ElysiumModMenu\"," + + "\"content\":\"Elysium freeze/overload log detected. See summary below.\"," + + "\"embeds\":[{" + + "\"title\":\"" + JsonEscape(safeTitle) + "\"," + + "\"description\":\"" + JsonEscape(safeMessage) + "\"," + + "\"color\":16724787," + + "\"timestamp\":\"" + DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + "\"" + + "}]" + + "}"; + + HttpResponseMessage response; + List attachments = PrepareLogAttachments(attachmentPaths); + if (attachments.Count > 0) + { + using MultipartFormDataContent form = new MultipartFormDataContent(); + form.Add(new StringContent(payload, Encoding.UTF8, "application/json"), "payload_json"); + + for (int i = 0; i < attachments.Count; i++) + { + ByteArrayContent fileContent = new ByteArrayContent(attachments[i].Bytes); + fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain"); + form.Add(fileContent, $"files[{i}]", attachments[i].FileName); + } + + response = await Client.PostAsync(webhookUrl, form); + } + else + { + using StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); + response = await Client.PostAsync(webhookUrl, content); + } + + using (response) + System.Console.WriteLine($"[ElysiumModMenu] Diagnostic webhook result: {(int)response.StatusCode} {response.ReasonPhrase}. Attachments={attachments.Count}"); + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Diagnostic webhook failed: {ex.GetType().Name}: {ex.Message}"); + } + } + + private sealed class LogAttachment + { + public string FileName; + public byte[] Bytes; + } + + private static List PrepareLogAttachments(IEnumerable attachmentPaths) + { + List attachments = new List(); + if (attachmentPaths == null) return attachments; + + long totalBytes = 0; + foreach (string path in attachmentPaths.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().Take(MaxDiagnosticAttachments)) + { + try + { + if (!System.IO.File.Exists(path)) continue; + long remainingBytes = MaxDiagnosticTotalAttachmentBytes - totalBytes; + if (remainingBytes <= 0) break; + + byte[] bytes = BuildRelevantLogExcerptBytes(path, out int matchedLines); + bool truncated = false; + if (bytes == null || bytes.Length == 0) + { + bytes = ReadLogAttachmentBytes(path, remainingBytes, out truncated); + } + + if (bytes == null || bytes.Length == 0) continue; + + string fileName = SanitizeAttachmentFileName(System.IO.Path.GetFileName(path)); + if (matchedLines > 0) fileName += ".anomaly-excerpt.txt"; + else if (truncated) fileName += ".tail.txt"; + attachments.Add(new LogAttachment { FileName = fileName, Bytes = bytes }); + totalBytes += bytes.Length; + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Failed to attach log file {System.IO.Path.GetFileName(path)}: {ex.GetType().Name}: {ex.Message}"); + } + } + + return attachments; + } + + private static byte[] BuildRelevantLogExcerptBytes(string path, out int matchedLines) + { + matchedLines = 0; + try + { + if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path)) return null; + + Queue before = new Queue(); + Queue output = new Queue(); + int afterRemaining = 0; + long fileLength = 0; + try { fileLength = new System.IO.FileInfo(path).Length; } catch { } + + void AddOutput(string value) + { + output.Enqueue(value ?? string.Empty); + while (output.Count > MaxDiagnosticExcerptLines) + output.Dequeue(); + } + + using (System.IO.FileStream stream = new System.IO.FileStream(path, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete)) + using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8, true)) + { + string line; + int lineNo = 0; + while ((line = reader.ReadLine()) != null) + { + lineNo++; + bool match = ElysiumModMenuGUI.IsRelevantAnomalyLine(line); + if (match) + { + matchedLines++; + AddOutput(""); + AddOutput($"--- {System.IO.Path.GetFileName(path)} line {lineNo} ---"); + foreach (string context in before) + AddOutput(context); + AddOutput(line); + afterRemaining = DiagnosticExcerptContextAfter; + } + else if (afterRemaining > 0) + { + AddOutput(line); + afterRemaining--; + } + + before.Enqueue(line); + while (before.Count > DiagnosticExcerptContextBefore) + before.Dequeue(); + } + } + + if (matchedLines <= 0) return null; + + StringBuilder builder = new StringBuilder(); + builder.AppendLine("Elysium anomaly log excerpt"); + builder.AppendLine($"Source: {path}"); + builder.AppendLine($"SourceBytes: {fileLength}"); + builder.AppendLine($"MatchedLines: {matchedLines}"); + builder.AppendLine($"GeneratedUtc: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}"); + builder.AppendLine(); + foreach (string line in output) + builder.AppendLine(line); + + return Encoding.UTF8.GetBytes(builder.ToString()); + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Failed to build anomaly excerpt {System.IO.Path.GetFileName(path)}: {ex.GetType().Name}: {ex.Message}"); + return null; + } + } + + private static byte[] ReadLogAttachmentBytes(string path, long remainingBytes, out bool truncated) + { + truncated = false; + using System.IO.FileStream stream = new System.IO.FileStream(path, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete); + long length = stream.Length; + long fullFileLimit = Math.Min(MaxDiagnosticAttachmentBytes, remainingBytes); + if (length <= fullFileLimit) + { + byte[] bytes = new byte[length]; + int read = stream.Read(bytes, 0, bytes.Length); + if (read == bytes.Length) return bytes; + return bytes.Take(read).ToArray(); + } + + truncated = true; + int tailSize = (int)Math.Min(Math.Min(LargeLogTailBytes, remainingBytes), length); + stream.Seek(-tailSize, System.IO.SeekOrigin.End); + byte[] tail = new byte[tailSize]; + int tailRead = stream.Read(tail, 0, tail.Length); + return tailRead == tail.Length ? tail : tail.Take(tailRead).ToArray(); + } + + private static string SanitizeAttachmentFileName(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) return "LogOutput.txt"; + foreach (char invalid in System.IO.Path.GetInvalidFileNameChars()) + fileName = fileName.Replace(invalid, '_'); + return fileName.Length > 80 ? fileName.Substring(fileName.Length - 80) : fileName; + } + + private static void AppendField(StringBuilder builder, string name, string value, bool inline) + { + if (builder.Length > 0) builder.Append(','); + builder.Append("{\"name\":\"") + .Append(JsonEscape(name)) + .Append("\",\"value\":\"") + .Append(JsonEscape(value)) + .Append("\",\"inline\":") + .Append(inline ? "true" : "false") + .Append('}'); + } + + private static string JsonEscape(string value) + { + if (value == null) return string.Empty; + return value.Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\r", "\\r") + .Replace("\n", "\\n"); + } } + public class ElysiumModMenuGUI : MonoBehaviour { public static string[] spoofMenuNames = { "ElysiumModMenu", "HostGuard/TOH", "Polar", "BanMod", "Better Among Us", "Sicko Menu", "GNC", "KillNetwork (V1)", "KillNetwork (V2)", "KNM" }; @@ -142,6 +525,27 @@ public static string L(string eng, string rus) public static KeyCode bindEndImp = KeyCode.None; public static KeyCode bindEndImpDC = KeyCode.None; public static KeyCode bindEndHnsDC = KeyCode.None; + public static KeyCode bindToggleTracers = KeyCode.None; + public static KeyCode bindToggleNoClip = KeyCode.None; + public static KeyCode bindToggleFreecam = KeyCode.None; + public static KeyCode bindToggleCameraZoom = KeyCode.None; + public static KeyCode bindKillAll = KeyCode.None; + public static KeyCode bindCallMeeting = KeyCode.None; + public static KeyCode bindTogglePlayerInfo = KeyCode.None; + public static KeyCode bindToggleSeeRoles = KeyCode.None; + public static KeyCode bindToggleSeeGhosts = KeyCode.None; + public static KeyCode bindToggleFullBright = KeyCode.None; + public static KeyCode bindKickAll = KeyCode.None; + public static KeyCode bindFixSabotages = KeyCode.None; + public static KeyCode bindSetAllGhost = KeyCode.None; + public static KeyCode bindSetAllGhostImp = KeyCode.None; + + public static readonly HashSet VanillaRpcIds = new HashSet + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 29, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 60, 61, 62, 63, 64, 65 + }; private bool isScannerActiveFlag = false; private bool isCamsActiveFlag = false; @@ -155,6 +559,20 @@ public static string L(string eng, string rus) public static bool isWaitBindEndImp = false; public static bool isWaitBindEndImpDC = false; public static bool isWaitBindEndHnsDC = false; + public static bool isWaitBindToggleTracers = false; + public static bool isWaitBindToggleNoClip = false; + public static bool isWaitBindToggleFreecam = false; + public static bool isWaitBindToggleCameraZoom = false; + public static bool isWaitBindKillAll = false; + public static bool isWaitBindCallMeeting = false; + public static bool isWaitBindTogglePlayerInfo = false; + public static bool isWaitBindToggleSeeRoles = false; + public static bool isWaitBindToggleSeeGhosts = false; + public static bool isWaitBindToggleFullBright = false; + public static bool isWaitBindKickAll = false; + public static bool isWaitBindFixSabotages = false; + public static bool isWaitBindSetAllGhost = false; + public static bool isWaitBindSetAllGhostImp = false; public static bool SpoofMenuEnabled = false; public static int selectedSpoofMenuIndex = 0; private float uiSpoofTimer = 0f; @@ -166,15 +584,20 @@ public static string L(string eng, string rus) public static bool DetailedJoinInfo = true; private static List lastPlayerIds = new List(); private static Dictionary pendingJoinTimers = new Dictionary(); + private static Dictionary playerHistoryKeysById = new Dictionary(); public class PlayerHistoryEntry { public string Name; public string FriendCode; public string Puid; public string Platform; + public string CustomPlatform; public int Level; public DateTime FirstSeenUtc; public DateTime LastSeenUtc; + public DateTime? LeftUtc; + public bool IsOnline; + public List RpcCalls = new List(); } private static List playerHistoryEntries = new List(); private Vector2 playersHistoryScroll = Vector2.zero; @@ -189,11 +612,11 @@ public class PlayerHistoryEntry public static RoleTypes[] forceRoleOptions = { RoleTypes.Crewmate, RoleTypes.Impostor, RoleTypes.Engineer, RoleTypes.Scientist, RoleTypes.Shapeshifter, RoleTypes.GuardianAngel }; public static RoleTypes[] roleAssignOptions = { RoleTypes.Crewmate, RoleTypes.Impostor, RoleTypes.Engineer, RoleTypes.Scientist, RoleTypes.Shapeshifter, RoleTypes.GuardianAngel, - (RoleTypes)8, (RoleTypes)9, (RoleTypes)10, (RoleTypes)12, (RoleTypes)18 + (RoleTypes)8, (RoleTypes)9, (RoleTypes)10, (RoleTypes)12, (RoleTypes)18, RoleTypes.Crewmate, RoleTypes.Impostor }; public static string[] roleAssignNames = { "Crewmate", "Impostor", "Engineer", "Scientist", "Shapeshifter", "Guardian Angel", - "Noisemaker", "Phantom", "Tracker", "Detective", "Viper" + "Noisemaker", "Phantom", "Tracker", "Detective", "Viper", "Ghost", "Ghost Imp" }; private int targetRoleAssignIdx = 0; private int allPlayersRoleAssignIdx = 0; @@ -373,21 +796,51 @@ public static class VoteBanSystemPatch { public static bool Prefix(VoteBanSystem __instance, byte callId, Hazel.MessageReader reader) { - if (!AmongUsClient.Instance.AmHost || !ElysiumModMenuGUI.disableVoteKicks) + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || !ElysiumModMenuGUI.disableVoteKicks) return true; if (callId == 26) { - reader.ReadInt32(); - reader.ReadInt32(); - - ElysiumModMenuGUI.ShowNotification("[SHIELD] Заблокирована попытка Vote-Kick'а!"); + try + { + int targetClientId = reader.ReadInt32(); + int voterClientId = reader.ReadInt32(); + string targetName = ResolveVoteClientName(targetClientId); + string voterName = ResolveVoteClientName(voterClientId); + ElysiumModMenuGUI.ShowNotification($"[VOTEKICK BLOCK] {voterName} tried to vote-kick {targetName}"); + } + catch + { + ElysiumModMenuGUI.ShowNotification("[VOTEKICK BLOCK] Vote-kick blocked, sender could not be resolved."); + } return false; } return true; } + + private static string ResolveVoteClientName(int clientId) + { + try + { + if (PlayerControl.AllPlayerControls != null) + { + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.Data == null) continue; + if (pc.Data.ClientId == clientId || (int)pc.OwnerId == clientId) + { + string name = string.IsNullOrWhiteSpace(pc.Data.PlayerName) ? "Unknown" : pc.Data.PlayerName; + return $"{name} ({clientId})"; + } + } + } + } + catch { } + + return $"client {clientId}"; + } } public static bool disableVoteKicks = false; @@ -412,27 +865,18 @@ private void SpawnMap(int mapId) try { if ((UnityEngine.Object)(object)AmongUsClient.Instance == (UnityEngine.Object)null || AmongUsClient.Instance.ShipPrefabs == null) - { - System.Console.WriteLine("[MAP] AmongUsClient or ShipPrefabs is null"); return; - } int realMapId = mapId; if (mapId == 3) realMapId = 4; if (mapId == 4) realMapId = 5; if (realMapId >= AmongUsClient.Instance.ShipPrefabs.Count) - { - System.Console.WriteLine("[MAP] Invalid map ID"); return; - } BepInEx.Unity.IL2CPP.Utils.MonoBehaviourExtensions.StartCoroutine(this, CoSpawnMap(realMapId)); } - catch (Exception ex) - { - System.Console.WriteLine("[MAP ERROR] Failed to spawn map: " + ex.Message); - } + catch { } } [HideFromIl2Cpp] @@ -444,7 +888,6 @@ private System.Collections.IEnumerator CoSpawnMap(int mapId) ShipStatus.Instance = AmongUsClient.Instance.ShipLoadingAsyncHandle.Result.GetComponent(); ((InnerNetClient)AmongUsClient.Instance).Spawn(((Component)ShipStatus.Instance).GetComponent(), -2, (SpawnFlags)0); - System.Console.WriteLine($"[MAP] Map ID: {mapId} spawned successfully"); } private void DespawnMap() @@ -454,13 +897,9 @@ private void DespawnMap() if (ShipStatus.Instance != null) { ShipStatus.Instance.Despawn(); - System.Console.WriteLine("[MAP] Map despawned successfully"); } } - catch (Exception ex) - { - System.Console.WriteLine("[MAP ERROR] Failed to despawn map: " + ex.Message); - } + catch { } } private void DespawnCurrentMap() @@ -686,6 +1125,8 @@ private void DrawChatSettingsTab() alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 280); GUILayout.Space(2); readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 280); + GUILayout.Space(4); + DrawGhostChatColorControl(280f); GUILayout.Space(2); enableExtendedChat = DrawToggle(enableExtendedChat, L("Extended Chat (120 chars)", "Длинный чат (120 симв.)"), 280); GUILayout.Space(2); @@ -906,6 +1347,7 @@ private static void UpsertPlayerHistory(PlayerControl pc) string fc = GetDisplayedFriendCode(pc.Data); string puid = "Unknown"; string platform = "Unknown"; + string customPlatform = ""; int level = 1; try @@ -921,33 +1363,200 @@ private static void UpsertPlayerHistory(PlayerControl pc) if (client != null) { platform = GetPlatform(client); - puid = client.Id.ToString(); + customPlatform = GetCustomPlatformName(client); + puid = GetClientPuid(client); } } catch { } string key = $"{fc}|{puid}|{name}"; var item = playerHistoryEntries.FirstOrDefault(x => $"{x.FriendCode}|{x.Puid}|{x.Name}" == key); + bool changed = false; if (item == null) { - playerHistoryEntries.Add(new PlayerHistoryEntry + item = new PlayerHistoryEntry { Name = name, FriendCode = fc, Puid = puid, Platform = platform, + CustomPlatform = customPlatform, Level = level, FirstSeenUtc = DateTime.UtcNow, - LastSeenUtc = DateTime.UtcNow - }); + LastSeenUtc = DateTime.UtcNow, + IsOnline = true + }; + playerHistoryEntries.Add(item); + changed = true; } else { + changed = item.Name != name || + item.FriendCode != fc || + item.Puid != puid || + item.Platform != platform || + item.CustomPlatform != customPlatform || + item.Level != level || + !item.IsOnline || + item.LeftUtc.HasValue; item.Name = name; + item.FriendCode = fc; + item.Puid = puid; item.Platform = platform; + item.CustomPlatform = customPlatform; item.Level = level; item.LastSeenUtc = DateTime.UtcNow; + item.LeftUtc = null; + item.IsOnline = true; + } + playerHistoryKeysById[pc.PlayerId] = key; + if (changed) WritePlayerHistoryFile(); + } + catch { } + } + + private static string GetCustomPlatformName(ClientData client) + { + try + { + string value = client?.PlatformData?.PlatformName; + if (string.IsNullOrWhiteSpace(value)) return ""; + value = Regex.Replace(value, "<.*?>", string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(value)) return ""; + + string platform = GetPlatform(client); + if (value.Equals(platform, StringComparison.OrdinalIgnoreCase)) return ""; + if (value.Equals(client.PlatformData.Platform.ToString(), StringComparison.OrdinalIgnoreCase)) return ""; + return value; + } + catch { return ""; } + } + + public static string GetClientPuid(ClientData client) + { + if (client == null) return "Unknown"; + + try + { + string direct = client.ProductUserId; + if (!string.IsNullOrWhiteSpace(direct)) return direct.Trim(); + } + catch { } + + string[] memberNames = { "ProductUserId", "productUserId", "Puid", "PUID", "puid", "EosId", "EOSId", "ProductId", "PlayerId" }; + foreach (string memberName in memberNames) + { + try + { + PropertyInfo prop = client.GetType().GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + object value = prop?.GetValue(client, null); + if (value != null && !string.IsNullOrWhiteSpace(value.ToString())) return value.ToString().Trim(); + } + catch { } + + try + { + FieldInfo field = client.GetType().GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + object value = field?.GetValue(client); + if (value != null && !string.IsNullOrWhiteSpace(value.ToString())) return value.ToString().Trim(); + } + catch { } + } + + return "Unknown"; + } + + private static string FormatPlatformHistory(PlayerHistoryEntry entry) + { + if (entry == null) return "Unknown"; + return string.IsNullOrWhiteSpace(entry.CustomPlatform) + ? entry.Platform + : $"{entry.Platform} + custom: {entry.CustomPlatform}"; + } + + private static string PlayerHistoryFilePath() + { + string folder = string.IsNullOrWhiteSpace(Plugin.ElysiumFolder) + ? System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu") + : Plugin.ElysiumFolder; + return System.IO.Path.Combine(folder, "ElysiumPlayerHistory.txt"); + } + + private static void MarkPlayerHistoryLeft(byte playerId) + { + try + { + if (!playerHistoryKeysById.TryGetValue(playerId, out string key)) return; + var item = playerHistoryEntries.FirstOrDefault(x => $"{x.FriendCode}|{x.Puid}|{x.Name}" == key); + if (item == null || !item.IsOnline) return; + + item.IsOnline = false; + item.LeftUtc = DateTime.UtcNow; + item.LastSeenUtc = item.LeftUtc.Value; + WritePlayerHistoryFile(); + } + catch { } + } + + public static void RecordPlayerRpc(PlayerControl pc, byte callId) + { + try + { + if (VanillaRpcIds.Contains(callId)) return; + if (pc == null || pc.Data == null) return; + UpsertPlayerHistory(pc); + + if (!playerHistoryKeysById.TryGetValue(pc.PlayerId, out string key)) return; + var item = playerHistoryEntries.FirstOrDefault(x => $"{x.FriendCode}|{x.Puid}|{x.Name}" == key); + if (item == null) return; + + if (!item.RpcCalls.Contains(callId)) + { + item.RpcCalls.Add(callId); + item.RpcCalls.Sort(); + WritePlayerHistoryFile(); + } + } + catch { } + } + + private static string FormatRpcHistory(PlayerHistoryEntry entry) + { + if (entry == null || entry.RpcCalls == null || entry.RpcCalls.Count == 0) return "нет"; + byte[] customRpcCalls = entry.RpcCalls.Where(x => !VanillaRpcIds.Contains(x)).Distinct().OrderBy(x => x).ToArray(); + if (customRpcCalls.Length == 0) return "нет"; + return string.Join(", ", customRpcCalls.Select(x => x.ToString()).ToArray()); + } + + private static void WritePlayerHistoryFile() + { + try + { + string path = PlayerHistoryFilePath(); + System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)); + + List lines = new List + { + "ElysiumModMenu Player History", + $"Updated UTC: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}", + "" + }; + + foreach (var e in playerHistoryEntries.OrderByDescending(x => x.LastSeenUtc)) + { + string left = e.LeftUtc.HasValue ? e.LeftUtc.Value.ToString("yyyy-MM-dd HH:mm:ss") : "online"; + lines.Add($"Nick: {e.Name}"); + lines.Add($"Level: {e.Level}"); + lines.Add($"FriendCode: {e.FriendCode}"); + lines.Add($"PUID: {e.Puid}"); + lines.Add($"Joined UTC: {e.FirstSeenUtc:yyyy-MM-dd HH:mm:ss}"); + lines.Add($"Left UTC: {left}"); + lines.Add($"Platform: {FormatPlatformHistory(e)}"); + lines.Add($"RPC calls: {FormatRpcHistory(e)}"); + lines.Add(new string('-', 48)); } + + System.IO.File.WriteAllLines(path, lines.ToArray(), Encoding.UTF8); } catch { } } @@ -1059,10 +1668,16 @@ private void DrawAntiCheatTab() GUILayout.Space(5); blockGameRpcInLobby = DrawToggle(blockGameRpcInLobby, "Block Game RPC in Lobby", 250); GUILayout.Space(5); + blockMeetingFloodRpc = DrawToggle(blockMeetingFloodRpc, "Block Meeting RPC Flood", 250); + GUILayout.Space(5); blockChatFloodRpc = DrawToggle(blockChatFloodRpc, "Block Chat RPC Flood", 250); GUILayout.Space(5); - - GUILayout.Space(15); + enablePasosLimit = DrawToggle(enablePasosLimit, "Message Block Freeze", 250); + GUILayout.Space(5); + enableLocalPasosBan = DrawToggle(enableLocalPasosBan, "Message Block Freeze Local Ban", 250); + GUILayout.Space(5); + enableHostPasosBan = DrawToggle(enableHostPasosBan, "Message Block Freeze Host Ban", 250); + GUILayout.Space(15); GUILayout.Label(L("OTHER PROTECTIONS", "ПРОЧАЯ ЗАЩИТА"), headerStyle); disableVoteKicks = DrawToggle(disableVoteKicks, L("Disable Vote Kicks (Host)", "Запрет кика голосованием (Хост)"), 250); @@ -1097,6 +1712,7 @@ private void DrawAntiCheatTab() if (DrawPseudoInputButton(banValue, isEditingBan, 25f, 46)) { isEditingBan = !isEditingBan; + isEditingGhostChatColor = false; ResetAllBindWaits(); } @@ -1164,7 +1780,6 @@ public static void Flag(PlayerControl player, string reason) if (mode >= 1) { ElysiumModMenuGUI.ShowNotification($"[ANTICHEAT] {pName}: {reason}"); - System.Console.WriteLine($"[Anticheat] {pName} flagged for: {reason}"); } if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) @@ -1180,7 +1795,7 @@ public static void Flag(PlayerControl player, string reason) try { var client = AmongUsClient.Instance.GetClientFromCharacter(player); - if (client != null) puid = client.Id.ToString(); + if (client != null) puid = GetClientPuid(client); } catch { } @@ -1196,6 +1811,7 @@ public static void Flag(PlayerControl player, string reason) public static class Anticheat_PlayerControl_RPC { private static readonly Dictionary> chatRpcTimes = new Dictionary>(); + private static readonly Dictionary> meetingRpcTimes = new Dictionary>(); private static readonly HashSet lobbyGameRpcs = new HashSet { (byte)RpcCalls.MurderPlayer, @@ -1228,7 +1844,8 @@ public static bool Prefix(PlayerControl __instance, byte callId, Hazel.MessageRe if (!ElysiumModMenuGUI.blockSpoofRPC && !ElysiumModMenuGUI.blockSabotageRPC && !ElysiumModMenuGUI.blockGameRpcInLobby && - !ElysiumModMenuGUI.blockChatFloodRpc) return true; + !ElysiumModMenuGUI.blockChatFloodRpc && + !ElysiumModMenuGUI.blockMeetingFloodRpc) return true; if (__instance == null || __instance == PlayerControl.LocalPlayer || __instance.Data == null) return true; int oldPos = reader.Position; @@ -1256,6 +1873,16 @@ public static bool Prefix(PlayerControl __instance, byte callId, Hazel.MessageRe } } + if (!isCheat && ElysiumModMenuGUI.blockMeetingFloodRpc && + (callId == (byte)RpcCalls.StartMeeting || callId == (byte)RpcCalls.ReportDeadBody)) + { + if (IsFlooded(meetingRpcTimes, __instance.PlayerId, ElysiumModMenuGUI.meetingRpcLimit, ElysiumModMenuGUI.meetingRpcWindow)) + { + isCheat = true; + cheatReason = "Meeting RPC flood"; + } + } + if (!isCheat && ElysiumModMenuGUI.blockSpoofRPC) { if (callId == (byte)RpcCalls.SetColor) @@ -2177,18 +2804,43 @@ private static string CleanName(string value) public static string customNameInput = "хыхых"; public static string spoofFriendCodeInput = "crewmate01"; public static string localFriendCodeInput = "Steam#Local"; + public static string ghostChatColorHex = "#D7B8FF"; public static bool isEditingLevel = false; public static bool isEditingName = false; public static bool isEditingFriendCode = false; public static bool isEditingLocalFriendCode = false; + public static bool isEditingGhostChatColor = false; + private static bool discordLaunchStatusSent = false; + private static bool discordInvalidWebhookNotified = false; + private static float discordLaunchStatusNextTryAt = 0f; + private static readonly string relaySessionId = Guid.NewGuid().ToString("N").Substring(0, 12); + private static readonly Dictionary watchedLogLineCounts = new Dictionary(); + private static readonly DateTime logMonitorStartedUtc = DateTime.UtcNow; + private static readonly object anomalyLogMonitorLock = new object(); + private static System.Threading.Timer anomalyLogMonitorTimer; + private static string anomalyReportDetailsCache = $"sessionId={relaySessionId}\nclientId=Unknown\nnetworkMode=Unknown\nhost=Unknown\nplatform=Unknown\ninGame=Unknown"; + private static float logMonitorNextScanAt = 0f; + private static float logBurstWindowStartedAt = -1f; + private static float logBurstCooldownUntil = 0f; + private static int logBurstLineCount = 0; + private static bool anomalyLogWatchNotified = false; + private const int LogBurstLineThreshold = 15; + private const int InitialLogTailLineLimit = 120; + private const float LogBurstWindowSeconds = 5f; + private const float LogBurstScanIntervalSeconds = 1f; + private const float LogBurstAlertCooldownSeconds = 60f; public static bool enableLocalNameSpoof = false; public static bool enableLocalFriendCodeSpoof = false; public static bool enableFriendCodeSpoof = false; public static bool enablePlatformSpoof = true; + public static bool enableAnomalyLogReports = true; + public static bool showEspFriendCode = true; public static int currentPlatformIndex = 1; private static float localNameRefreshTimer = 0f; private static float localFriendCodeRefreshTimer = 0f; private static string originalLocalFriendCode = null; + private static float friendEspIgnoreNextLoadAt = 0f; + private static readonly HashSet friendEspIgnoreTokens = new HashSet(StringComparer.OrdinalIgnoreCase); private float brokenFcScanTimer = 0f; private static readonly HashSet brokenFcPunishedOwners = new HashSet(); @@ -2407,6 +3059,12 @@ private void DrawVisualsInGame() GUILayout.EndHorizontal(); GUILayout.Space(5); + GUILayout.BeginHorizontal(); + showEspFriendCode = DrawToggle(showEspFriendCode, L("Show FC In ESP", "FriendCode в ESP"), 210); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + GUILayout.BeginHorizontal(); removePenalty = DrawToggle(removePenalty, L("No Disconnect Penalty", "Нет штрафа за выход"), 210); alwaysShowLobbyTimer = DrawToggle(alwaysShowLobbyTimer, L("Always Show Lobby Timer", "Всегда показывать таймер лобби"), 210); @@ -2449,228 +3107,734 @@ private void DrawVisualsInGame() public static bool enableLocalPetSpamDrop = true; public static bool enableHostPetSpamBan = false; - [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] - public static class Shield_PetSpam_Patch + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadMessage))] + public static class Shield_PasosLimit_Patch { - public static System.Collections.Generic.HashSet petSpamBlockedPlayers = new System.Collections.Generic.HashSet(); - - public static System.Collections.Generic.Dictionary> petSpamTrackers = new System.Collections.Generic.Dictionary>(); + private const byte RpcGameDataTag = 2; + private const byte DroppedGameDataTag = 0; + private const int PasosBanPacketThreshold = 3; + private const float PasosWindow = 0.25f; + private const float PasosNotifyCooldown = 2f; + private static readonly Queue emptyRpcDrops = new Queue(); + private static readonly Dictionary> pasosDropTrackers = new Dictionary>(); + private static readonly HashSet pasosBlockedClientIds = new HashSet(); + private static readonly HashSet pasosHostBannedClientIds = new HashSet(); + private static float lastPasosNotify; + private static int currentPasosClientId = -1; - public static bool Prefix(PlayerPhysics __instance, byte callId, Hazel.MessageReader reader) + public static void BeginMessageContext(int clientId) { - if (!ElysiumModMenuGUI.enableLocalPetSpamDrop && !ElysiumModMenuGUI.enableHostPetSpamBan) return true; - - if (callId == 49 || callId == 50) - { - try - { - if (__instance == null || __instance.myPlayer == null) return true; + currentPasosClientId = IsValidClientId(clientId) ? clientId : -1; + } - if (__instance.myPlayer == PlayerControl.LocalPlayer) return true; + public static bool IsClientBlocked(int clientId) + { + return ElysiumModMenuGUI.enableLocalPasosBan && IsValidClientId(clientId) && pasosBlockedClientIds.Contains(clientId); + } - byte pId = __instance.myPlayer.PlayerId; + public static bool IsEmptyGameDataReader(MessageReader reader) + { + if (!ElysiumModMenuGUI.enablePasosLimit || reader == null) return false; - if (petSpamBlockedPlayers.Contains(pId)) - { - if (ElysiumModMenuGUI.enableLocalPetSpamDrop) return false; - } + try + { + return reader.Length <= 0 || (reader.Position <= 0 && reader.BytesRemaining <= 0); + } + catch { } - float now = UnityEngine.Time.time; + return false; + } - if (!petSpamTrackers.ContainsKey(pId)) - petSpamTrackers[pId] = new System.Collections.Generic.Queue(); + public static bool IsValidClientId(int clientId) + { + return clientId >= 0 && clientId < 256; + } - var q = petSpamTrackers[pId]; + public static void RecordDrop(int clientId = -1) + { + float now = UnityEngine.Time.time; + while (emptyRpcDrops.Count > 0 && emptyRpcDrops.Peek() < now - PasosWindow) + emptyRpcDrops.Dequeue(); - while (q.Count > 0 && q.Peek() < now - 0.75f) - q.Dequeue(); + emptyRpcDrops.Enqueue(now); - q.Enqueue(now); + int resolvedClientId = IsValidClientId(clientId) ? clientId : currentPasosClientId; + if (!IsValidClientId(resolvedClientId)) + resolvedClientId = ResolveSingleRemoteClientId(); - if (q.Count > 160) - { - petSpamBlockedPlayers.Add(pId); + int clientDropCount = 0; + if (IsValidClientId(resolvedClientId)) + { + clientDropCount = TrackPasosClientDrop(resolvedClientId, now); + if (clientDropCount >= PasosBanPacketThreshold) + BlockPasosClient(resolvedClientId, clientDropCount); + } - string pName = __instance.myPlayer.Data?.PlayerName ?? "Unknown"; + if (emptyRpcDrops.Count >= PasosBanPacketThreshold && now - lastPasosNotify > PasosNotifyCooldown) + lastPasosNotify = now; + } - if (ElysiumModMenuGUI.enableLocalPetSpamDrop) - { - ElysiumModMenuGUI.ShowNotification($"[SHIELD] Игрок {pName} заблокирован за Pet Spam (Локально)!"); - } + private static int TrackPasosClientDrop(int clientId, float now) + { + if (!IsValidClientId(clientId)) return 0; - if (ElysiumModMenuGUI.enableHostPetSpamBan && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - string fc = string.IsNullOrEmpty(__instance.myPlayer.Data?.FriendCode) ? "Unknown" : __instance.myPlayer.Data.FriendCode; - string puid = "Unknown"; + if (!pasosDropTrackers.TryGetValue(clientId, out Queue drops)) + { + drops = new Queue(); + pasosDropTrackers[clientId] = drops; + } - try - { - var client = AmongUsClient.Instance.GetClientFromCharacter(__instance.myPlayer); - if (client != null) puid = client.Id.ToString(); - } - catch { } + while (drops.Count > 0 && drops.Peek() < now - PasosWindow) + drops.Dequeue(); - ElysiumModMenuGUI.AddToBanList(fc, puid, pName, "Auto-banned for Pet Spam"); + drops.Enqueue(now); + return drops.Count; + } - AmongUsClient.Instance.KickPlayer(__instance.myPlayer.OwnerId, true); + private static void BlockPasosClient(int clientId, int packetCount) + { + try + { + if (!IsValidClientId(clientId) || (AmongUsClient.Instance != null && clientId == AmongUsClient.Instance.ClientId)) return; - ElysiumModMenuGUI.ShowNotification($"[SHIELD] Игрок {pName} АВТОМАТИЧЕСКИ ЗАБАНЕН за спам петом!"); - } + PlayerControl player = FindPlayerByClientId(clientId); + string pName = player?.Data?.PlayerName ?? $"Client {clientId}"; + int banClientId = GetKickClientId(player, clientId); + string fc = string.IsNullOrEmpty(player?.Data?.FriendCode) ? "Unknown" : player.Data.FriendCode; + string puid = IsValidClientId(banClientId) ? banClientId.ToString() : clientId.ToString(); - return false; + try + { + if (player != null && AmongUsClient.Instance != null) + { + var client = AmongUsClient.Instance.GetClientFromCharacter(player); + if (client != null) puid = GetClientPuid(client); } } catch { } - } - - return true; - } - } - public static int GetColorIdByName(string name) - { - string[] names = { "red", "blue", "green", "pink", "orange", "yellow", "black", "white", "purple", "brown", "cyan", "lime", "maroon", "rose", "banana", "gray", "tan", "coral", "fortegreen" }; - for (int i = 0; i < names.Length; i++) - if (names[i] == name.ToLower().Trim()) return i; - return -1; - } - private IEnumerator AttemptShapeshiftFrame(PlayerControl target, PlayerControl morphInto) - { - if (target == null || morphInto == null || PlayerControl.LocalPlayer == null || AmongUsClient.Instance == null) yield break; - - bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); - - if (target.Data.RoleType != RoleTypes.Shapeshifter && hasAnticheat) - { - RoleTypes currentRole = target.Data.RoleType; - target.RpcSetRole(RoleTypes.Shapeshifter, true); - yield return new WaitForSeconds(0.5f); - - target.RpcShapeshift(morphInto, true); + if (ElysiumModMenuGUI.enableLocalPasosBan && pasosBlockedClientIds.Add(clientId)) + { + ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Local Message Block Freeze blacklist after {packetCount} packets"); + } - yield return new WaitForSeconds(0.5f); + if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + if (!pasosHostBannedClientIds.Add(clientId)) return; + if (!IsValidClientId(banClientId)) return; - target.RpcSetRole(currentRole, true); - } - else - { - target.RpcShapeshift(morphInto, true); + ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Host auto-ban for Message Block Freeze empty RPC spam after {packetCount} packets"); + AmongUsClient.Instance.KickPlayer(banClientId, true); + } + catch { } } - ShowNotification($"[MORPH] {target.Data.PlayerName} превращен в {morphInto.Data.PlayerName}!"); - } - private IEnumerator MassMorphCoroutine() - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) yield break; + public static int GetKickClientId(PlayerControl player, int fallbackClientId) + { + try + { + if (player != null) + { + int ownerId = (int)player.OwnerId; + if (IsValidClientId(ownerId)) return ownerId; - bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); + if (player.Data != null && IsValidClientId(player.Data.ClientId)) + return player.Data.ClientId; + } + } + catch { } - Dictionary originalRoles = new Dictionary(); + return IsValidClientId(fallbackClientId) ? fallbackClientId : -1; + } - foreach (var pc in PlayerControl.AllPlayerControls) + public static PlayerControl FindPlayerByClientId(int clientId) { - if (pc != null && pc.Data != null && !pc.Data.Disconnected) + try { - originalRoles[pc.PlayerId] = pc.Data.RoleType; + if (PlayerControl.AllPlayerControls == null) return null; - if (hasAnticheat && pc.Data.RoleType != RoleTypes.Shapeshifter) + foreach (PlayerControl pc in PlayerControl.AllPlayerControls) { - pc.RpcSetRole(RoleTypes.Shapeshifter, true); + if (pc == null) continue; + if ((int)pc.OwnerId == clientId) return pc; + + try + { + if (pc.Data != null && pc.Data.ClientId == clientId) return pc; + } + catch { } } } - } + catch { } - if (hasAnticheat) yield return new UnityEngine.WaitForSeconds(0.5f); + return null; + } - PlayerControl targetToMorphInto = null; - if (selectedMorphTargetId != 255) + public static int ResolveClientId(object source) { - targetToMorphInto = GameData.Instance.GetPlayerById(selectedMorphTargetId)?.Object; + return ResolveClientId(source, 0); } - foreach (var pc in PlayerControl.AllPlayerControls) + private static int ResolveClientId(object source, int depth) { - if (pc != null && pc.Data != null && !pc.Data.Disconnected) + if (source == null || depth > 2 || source is MessageReader || source is SendOption) return -1; + + try { - PlayerControl morphTarget = targetToMorphInto != null ? targetToMorphInto : pc; - pc.RpcShapeshift(morphTarget, true); - } - } + if (source is PlayerControl pc) + return GetKickClientId(pc, -1); + int direct = ConvertNumericClientId(source); + if (direct >= 0) return direct; - if (hasAnticheat) yield return new UnityEngine.WaitForSeconds(0.5f); + Type type = source.GetType(); + foreach (string name in new[] { "ClientId", "OwnerId", "Id", "clientId", "ownerId", "id" }) + { + PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + direct = ConvertNumericClientId(property?.GetValue(source)); + if (direct >= 0) return direct; - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null && pc.Data != null && !pc.Data.Disconnected) - { - if (hasAnticheat && originalRoles.ContainsKey(pc.PlayerId)) + FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + direct = ConvertNumericClientId(field?.GetValue(source)); + if (direct >= 0) return direct; + } + + foreach (string name in new[] { "Character", "Object", "Player", "Data", "character", "player" }) { - pc.RpcSetRole(originalRoles[pc.PlayerId], true); + PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + direct = ResolveClientId(property?.GetValue(source), depth + 1); + if (direct >= 0) return direct; + + FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + direct = ResolveClientId(field?.GetValue(source), depth + 1); + if (direct >= 0) return direct; + } + + string typeName = type.FullName ?? type.Name; + if (typeName.IndexOf("Player", StringComparison.OrdinalIgnoreCase) >= 0 || + typeName.IndexOf("Client", StringComparison.OrdinalIgnoreCase) >= 0) + { + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (property.GetIndexParameters().Length > 0) continue; + + try + { + direct = ConvertNumericClientId(property.GetValue(source)); + if (direct >= 0) return direct; + } + catch { } + } + + foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + try + { + direct = ConvertNumericClientId(field.GetValue(source)); + if (direct >= 0) return direct; + } + catch { } + } } } + catch { } + + return -1; } - string notifText = targetToMorphInto != null ? targetToMorphInto.Data.PlayerName : "Egg"; - ShowNotification($"[MASS MORPH] {notifText}"); - } + private static int ResolveSingleRemoteClientId() + { + try + { + if (PlayerControl.AllPlayerControls == null) return -1; + int found = -1; + int count = 0; + foreach (PlayerControl pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; - private void ForceMeetingAsPlayer(PlayerControl target) - { - if (target == null || AmongUsClient.Instance == null) return; - if (!AmongUsClient.Instance.AmHost) return; + int ownerId = (int)pc.OwnerId; + if (!IsValidClientId(ownerId)) continue; - try - { - MeetingRoomManager.Instance.AssignSelf(target, null); - target.RpcStartMeeting(null); - DestroyableSingleton.Instance.OpenMeetingRoom(target); + found = ownerId; + count++; + if (count > 1) return -1; + } + + return count == 1 ? found : -1; + } + catch { } + + return -1; } - catch { } - } - private void KillAll() - { - if (PlayerControl.LocalPlayer == null || PlayerControl.AllPlayerControls == null) return; - Vector3 op = PlayerControl.LocalPlayer.transform.position; - foreach (var t in PlayerControl.AllPlayerControls) + private static int ConvertNumericClientId(object value) { - if (t != null && t != PlayerControl.LocalPlayer && !t.Data.IsDead && !t.Data.Disconnected) + if (value == null) return -1; + + try { - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(t.transform.position); - PlayerControl.LocalPlayer.CmdCheckMurder(t); - PlayerControl.LocalPlayer.RpcMurderPlayer(t, true); + TypeCode code = Type.GetTypeCode(value.GetType()); + switch (code) + { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.UInt16: + case TypeCode.Int32: + case TypeCode.UInt32: + case TypeCode.Int64: + case TypeCode.UInt64: + long id = Convert.ToInt64(value); + return id >= 0 && id < 256 ? (int)id : -1; + } } + catch { } + + return -1; } - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(op); - } - private void KickAll() - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) + public static void Postfix(MessageReader __result) { - foreach (var pc in PlayerControl.AllPlayerControls) - if (pc != null && pc != PlayerControl.LocalPlayer && !pc.Data.Disconnected) - AmongUsClient.Instance.KickPlayer((int)pc.OwnerId, false); + DropEmptyRpcMessage(__result); } - } - private void DespawnLobby() - { - try + public static void DropEmptyRpcMessage(MessageReader reader) { - if (LobbyBehaviour.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + if (!ElysiumModMenuGUI.enablePasosLimit || reader == null) return; + + try { - LobbyBehaviour.Instance.Cast().Despawn(); + if (reader.Tag != RpcGameDataTag || reader.BytesRemaining > 0 || reader.Length > 0) return; + + reader.Tag = DroppedGameDataTag; + reader.Position = reader.Length; + + RecordDrop(); } + catch { } } - catch { } } - private void SpawnLobby() + public static class Shield_PasosLimit_GameDataGuard { - try + private static readonly Dictionary coroutineReaderProperties = new Dictionary(); + + public static bool ShouldDrop(object[] args) + { + if (!ElysiumModMenuGUI.enablePasosLimit) return false; + + try + { + MessageReader reader = FindReader(args); + + if (!Shield_PasosLimit_Patch.IsEmptyGameDataReader(reader)) + return false; + + Shield_PasosLimit_Patch.RecordDrop(); + return true; + } + catch { } + + return false; + } + + public static bool ShouldDropCoroutine(object instance) + { + if (!ElysiumModMenuGUI.enablePasosLimit) return false; + + try + { + MessageReader reader = GetCoroutineReader(instance); + + if (!Shield_PasosLimit_Patch.IsEmptyGameDataReader(reader)) + return false; + + Shield_PasosLimit_Patch.RecordDrop(); + return true; + } + catch { } + + return false; + } + + private static MessageReader FindReader(object[] args) + { + if (args == null) return null; + + foreach (object arg in args) + { + if (arg is MessageReader reader) + return reader; + } + + return null; + } + + private static MessageReader GetCoroutineReader(object instance) + { + if (instance == null) return null; + + try + { + Type type = instance.GetType(); + if (!coroutineReaderProperties.TryGetValue(type, out PropertyInfo property)) + { + property = type.GetProperty("reader", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + coroutineReaderProperties[type] = property; + } + + return property?.GetValue(instance) as MessageReader; + } + catch { } + + return null; + } + + private static System.Collections.IEnumerator EmptyGameDataCoroutine() + { + yield break; + } + + public static Il2CppSystem.Collections.IEnumerator EmptyGameDataCoroutineIl2Cpp() + { + return EmptyGameDataCoroutine().WrapToIl2Cpp(); + } + } + + [HarmonyPatch] + public static class Shield_PasosLimit_HandleGameData_Patch + { + public static IEnumerable TargetMethods() + { + foreach (MethodInfo method in typeof(InnerNetClient).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (method.Name == "HandleGameData" && method.ReturnType == typeof(void) && + method.GetParameters().Any(p => p.ParameterType == typeof(MessageReader))) + { + yield return method; + } + } + } + + public static bool Prefix(object[] __args) + { + return !Shield_PasosLimit_GameDataGuard.ShouldDrop(__args); + } + } + + [HarmonyPatch] + public static class Shield_PasosLimit_HandleGameDataInner_Patch + { + public static IEnumerable TargetMethods() + { + foreach (MethodInfo method in typeof(InnerNetClient).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (method.Name == "HandleGameDataInner" && + method.GetParameters().Any(p => p.ParameterType == typeof(MessageReader))) + { + yield return method; + } + } + } + + public static bool Prefix(object[] __args, ref Il2CppSystem.Collections.IEnumerator __result) + { + if (!Shield_PasosLimit_GameDataGuard.ShouldDrop(__args)) + return true; + + __result = Shield_PasosLimit_GameDataGuard.EmptyGameDataCoroutineIl2Cpp(); + return false; + } + } + + [HarmonyPatch] + public static class Shield_PasosLimit_GameDataCoroutine_Patch + { + public static IEnumerable TargetMethods() + { + foreach (Type nestedType in typeof(InnerNetClient).GetNestedTypes(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (nestedType.Name.IndexOf("HandleGameDataInner", StringComparison.OrdinalIgnoreCase) < 0) continue; + + MethodInfo moveNext = nestedType.GetMethod("MoveNext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (moveNext != null && moveNext.ReturnType == typeof(bool)) + yield return moveNext; + } + } + + public static bool Prefix(object __instance, ref bool __result) + { + if (!Shield_PasosLimit_GameDataGuard.ShouldDropCoroutine(__instance)) + return true; + + __result = false; + return false; + } + } + + [HarmonyPatch] + public static class Shield_PasosLimit_HandleMessageContext_Patch + { + public static IEnumerable TargetMethods() + { + foreach (MethodInfo method in typeof(InnerNetClient).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (method.Name != "HandleMessage") continue; + if (method.GetParameters().Any(p => p.ParameterType == typeof(MessageReader))) + yield return method; + } + } + + public static bool Prefix(object[] __args) + { + if (!ElysiumModMenuGUI.enablePasosLimit) return true; + + try + { + int clientId = ExtractClientId(__args); + Shield_PasosLimit_Patch.BeginMessageContext(clientId); + } + catch { } + + return true; + } + + public static int ExtractClientId(object[] args) + { + if (args == null) return -1; + + foreach (object arg in args) + { + int clientId = ExtractClientId(arg); + if (clientId >= 0) return clientId; + } + + return -1; + } + + private static int ExtractClientId(object source) + { + return Shield_PasosLimit_Patch.ResolveClientId(source); + } + } + + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] + public static class Shield_PetSpam_Patch + { + public static System.Collections.Generic.HashSet petSpamBlockedPlayers = new System.Collections.Generic.HashSet(); + + public static System.Collections.Generic.Dictionary> petSpamTrackers = new System.Collections.Generic.Dictionary>(); + + public static bool Prefix(PlayerPhysics __instance, byte callId, Hazel.MessageReader reader) + { + if (!ElysiumModMenuGUI.enableLocalPetSpamDrop && !ElysiumModMenuGUI.enableHostPetSpamBan) return true; + + if (callId == 49 || callId == 50) + { + try + { + if (__instance == null || __instance.myPlayer == null) return true; + + if (__instance.myPlayer == PlayerControl.LocalPlayer) return true; + + byte pId = __instance.myPlayer.PlayerId; + + if (petSpamBlockedPlayers.Contains(pId)) + { + if (ElysiumModMenuGUI.enableLocalPetSpamDrop) return false; + } + + float now = UnityEngine.Time.time; + + if (!petSpamTrackers.ContainsKey(pId)) + petSpamTrackers[pId] = new System.Collections.Generic.Queue(); + + var q = petSpamTrackers[pId]; + + while (q.Count > 0 && q.Peek() < now - 0.75f) + q.Dequeue(); + + q.Enqueue(now); + + if (q.Count > 160) + { + petSpamBlockedPlayers.Add(pId); + + string pName = __instance.myPlayer.Data?.PlayerName ?? "Unknown"; + + if (ElysiumModMenuGUI.enableHostPetSpamBan && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + string fc = string.IsNullOrEmpty(__instance.myPlayer.Data?.FriendCode) ? "Unknown" : __instance.myPlayer.Data.FriendCode; + string puid = "Unknown"; + + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(__instance.myPlayer); + if (client != null) puid = GetClientPuid(client); + } + catch { } + + ElysiumModMenuGUI.AddToBanList(fc, puid, pName, "Auto-banned for Pet Spam"); + + AmongUsClient.Instance.KickPlayer(__instance.myPlayer.OwnerId, true); + + } + + return false; + } + } + catch { } + } + + return true; + } + } + public static int GetColorIdByName(string name) + { + string[] names = { "red", "blue", "green", "pink", "orange", "yellow", "black", "white", "purple", "brown", "cyan", "lime", "maroon", "rose", "banana", "gray", "tan", "coral", "fortegreen" }; + for (int i = 0; i < names.Length; i++) + if (names[i] == name.ToLower().Trim()) return i; + return -1; + } + private IEnumerator AttemptShapeshiftFrame(PlayerControl target, PlayerControl morphInto) + { + if (target == null || morphInto == null || PlayerControl.LocalPlayer == null || AmongUsClient.Instance == null) yield break; + + bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); + + if (target.Data.RoleType != RoleTypes.Shapeshifter && hasAnticheat) + { + RoleTypes currentRole = target.Data.RoleType; + target.RpcSetRole(RoleTypes.Shapeshifter, true); + + yield return new WaitForSeconds(0.5f); + + target.RpcShapeshift(morphInto, true); + + yield return new WaitForSeconds(0.5f); + + target.RpcSetRole(currentRole, true); + } + else + { + target.RpcShapeshift(morphInto, true); + } + ShowNotification($"[MORPH] {target.Data.PlayerName} превращен в {morphInto.Data.PlayerName}!"); + } + + private IEnumerator MassMorphCoroutine() + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) yield break; + + bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); + + Dictionary originalRoles = new Dictionary(); + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + originalRoles[pc.PlayerId] = pc.Data.RoleType; + + if (hasAnticheat && pc.Data.RoleType != RoleTypes.Shapeshifter) + { + pc.RpcSetRole(RoleTypes.Shapeshifter, true); + } + } + } + + if (hasAnticheat) yield return new UnityEngine.WaitForSeconds(0.5f); + + PlayerControl targetToMorphInto = null; + if (selectedMorphTargetId != 255) + { + targetToMorphInto = GameData.Instance.GetPlayerById(selectedMorphTargetId)?.Object; + } + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + PlayerControl morphTarget = targetToMorphInto != null ? targetToMorphInto : pc; + pc.RpcShapeshift(morphTarget, true); + } + } + + + if (hasAnticheat) yield return new UnityEngine.WaitForSeconds(0.5f); + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + if (hasAnticheat && originalRoles.ContainsKey(pc.PlayerId)) + { + pc.RpcSetRole(originalRoles[pc.PlayerId], true); + } + } + } + + string notifText = targetToMorphInto != null ? targetToMorphInto.Data.PlayerName : "Egg"; + ShowNotification($"[MASS MORPH] {notifText}"); + } + + + private void ForceMeetingAsPlayer(PlayerControl target) + { + if (target == null || AmongUsClient.Instance == null) return; + if (!AmongUsClient.Instance.AmHost) return; + + try + { + MeetingRoomManager.Instance.AssignSelf(target, null); + target.RpcStartMeeting(null); + DestroyableSingleton.Instance.OpenMeetingRoom(target); + } + catch { } + } + + private void KillAll() + { + if (PlayerControl.LocalPlayer == null || PlayerControl.AllPlayerControls == null) return; + Vector3 op = PlayerControl.LocalPlayer.transform.position; + foreach (var t in PlayerControl.AllPlayerControls) + { + if (t != null && t != PlayerControl.LocalPlayer && !t.Data.IsDead && !t.Data.Disconnected) + { + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(t.transform.position); + PlayerControl.LocalPlayer.CmdCheckMurder(t); + PlayerControl.LocalPlayer.RpcMurderPlayer(t, true); + } + } + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(op); + } + + private void KickAll() + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) + { + foreach (var pc in PlayerControl.AllPlayerControls) + if (pc != null && pc != PlayerControl.LocalPlayer && !pc.Data.Disconnected) + AmongUsClient.Instance.KickPlayer((int)pc.OwnerId, false); + } + } + + private void DespawnLobby() + { + try + { + if (LobbyBehaviour.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + LobbyBehaviour.Instance.Cast().Despawn(); + } + } + catch { } + } + + private void SpawnLobby() + { + try { if (GameStartManager.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) { @@ -2858,6 +4022,9 @@ private void SaveConfig() Plugin.UnlockCosmeticsConfig.Value = unlockCosmetics; Plugin.MoreLobbyInfoConfig.Value = moreLobbyInfo; Plugin.EnableChatDarkModeConfig.Value = enableChatDarkMode; + Plugin.GhostChatColorConfig.Value = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); + Plugin.EnableAnomalyLogReportsConfig.Value = enableAnomalyLogReports; + Plugin.ShowEspFriendCodeConfig.Value = showEspFriendCode; Plugin.RpcSpoofDelayConfig.Value = rpcSpoofDelay; Plugin.MenuColorIndexConfig.Value = currentMenuColorIndex; Plugin.RgbMenuModeConfig.Value = rgbMenuMode; @@ -2874,6 +4041,20 @@ private void SaveConfig() PlayerPrefs.SetInt("M_BndEndImp", (int)bindEndImp); PlayerPrefs.SetInt("M_BndEndImpDC", (int)bindEndImpDC); PlayerPrefs.SetInt("M_BndEndHnsDC", (int)bindEndHnsDC); + PlayerPrefs.SetInt("M_BndToggleTracers", (int)bindToggleTracers); + PlayerPrefs.SetInt("M_BndToggleNoClip", (int)bindToggleNoClip); + PlayerPrefs.SetInt("M_BndToggleFreecam", (int)bindToggleFreecam); + PlayerPrefs.SetInt("M_BndToggleCameraZoom", (int)bindToggleCameraZoom); + PlayerPrefs.SetInt("M_BndKillAll", (int)bindKillAll); + PlayerPrefs.SetInt("M_BndCallMeeting", (int)bindCallMeeting); + PlayerPrefs.SetInt("M_BndTogglePlayerInfo", (int)bindTogglePlayerInfo); + PlayerPrefs.SetInt("M_BndToggleSeeRoles", (int)bindToggleSeeRoles); + PlayerPrefs.SetInt("M_BndToggleSeeGhosts", (int)bindToggleSeeGhosts); + PlayerPrefs.SetInt("M_BndToggleFullBright", (int)bindToggleFullBright); + PlayerPrefs.SetInt("M_BndKickAll", (int)bindKickAll); + PlayerPrefs.SetInt("M_BndFixSabotages", (int)bindFixSabotages); + PlayerPrefs.SetInt("M_BndSetAllGhost", (int)bindSetAllGhost); + PlayerPrefs.SetInt("M_BndSetAllGhostImp", (int)bindSetAllGhostImp); SaveBool("M_AutoKickBugs", autoKickBugs); PlayerPrefs.SetFloat("M_AutoKickTimer", autoKickTimer); SaveBool("M_DisableVoteKicks", disableVoteKicks); @@ -2920,6 +4101,10 @@ private void SaveConfig() SaveBool("M_BlockSabotageRPC", blockSabotageRPC); SaveBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); + SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); + SaveBool("M_PasosLimit", enablePasosLimit); + SaveBool("M_AntiPasosLocalBan", enableLocalPasosBan); + SaveBool("M_AntiPasosHostBan", enableHostPasosBan); SaveBool("M_AutoHostEnabled", AutoHostEnabled); SaveBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); SaveBool("M_AutoHostNotifications", AutoHostNotifications); @@ -3008,6 +4193,9 @@ private void LoadConfig() unlockCosmetics = Plugin.UnlockCosmeticsConfig.Value; moreLobbyInfo = Plugin.MoreLobbyInfoConfig.Value; enableChatDarkMode = Plugin.EnableChatDarkModeConfig.Value; + ghostChatColorHex = SanitizeHexColor(Plugin.GhostChatColorConfig.Value, "#D7B8FF"); + enableAnomalyLogReports = Plugin.EnableAnomalyLogReportsConfig.Value; + showEspFriendCode = Plugin.ShowEspFriendCodeConfig.Value; rpcSpoofDelay = Plugin.RpcSpoofDelayConfig.Value; currentMenuColorIndex = Plugin.MenuColorIndexConfig.Value; rgbMenuMode = Plugin.RgbMenuModeConfig.Value; @@ -3031,6 +4219,20 @@ private void LoadConfig() if (PlayerPrefs.HasKey("M_BndEndImp")) bindEndImp = (KeyCode)PlayerPrefs.GetInt("M_BndEndImp"); if (PlayerPrefs.HasKey("M_BndEndImpDC")) bindEndImpDC = (KeyCode)PlayerPrefs.GetInt("M_BndEndImpDC"); if (PlayerPrefs.HasKey("M_BndEndHnsDC")) bindEndHnsDC = (KeyCode)PlayerPrefs.GetInt("M_BndEndHnsDC"); + if (PlayerPrefs.HasKey("M_BndToggleTracers")) bindToggleTracers = (KeyCode)PlayerPrefs.GetInt("M_BndToggleTracers"); + if (PlayerPrefs.HasKey("M_BndToggleNoClip")) bindToggleNoClip = (KeyCode)PlayerPrefs.GetInt("M_BndToggleNoClip"); + if (PlayerPrefs.HasKey("M_BndToggleFreecam")) bindToggleFreecam = (KeyCode)PlayerPrefs.GetInt("M_BndToggleFreecam"); + if (PlayerPrefs.HasKey("M_BndToggleCameraZoom")) bindToggleCameraZoom = (KeyCode)PlayerPrefs.GetInt("M_BndToggleCameraZoom"); + if (PlayerPrefs.HasKey("M_BndKillAll")) bindKillAll = (KeyCode)PlayerPrefs.GetInt("M_BndKillAll"); + if (PlayerPrefs.HasKey("M_BndCallMeeting")) bindCallMeeting = (KeyCode)PlayerPrefs.GetInt("M_BndCallMeeting"); + if (PlayerPrefs.HasKey("M_BndTogglePlayerInfo")) bindTogglePlayerInfo = (KeyCode)PlayerPrefs.GetInt("M_BndTogglePlayerInfo"); + if (PlayerPrefs.HasKey("M_BndToggleSeeRoles")) bindToggleSeeRoles = (KeyCode)PlayerPrefs.GetInt("M_BndToggleSeeRoles"); + if (PlayerPrefs.HasKey("M_BndToggleSeeGhosts")) bindToggleSeeGhosts = (KeyCode)PlayerPrefs.GetInt("M_BndToggleSeeGhosts"); + if (PlayerPrefs.HasKey("M_BndToggleFullBright")) bindToggleFullBright = (KeyCode)PlayerPrefs.GetInt("M_BndToggleFullBright"); + if (PlayerPrefs.HasKey("M_BndKickAll")) bindKickAll = (KeyCode)PlayerPrefs.GetInt("M_BndKickAll"); + if (PlayerPrefs.HasKey("M_BndFixSabotages")) bindFixSabotages = (KeyCode)PlayerPrefs.GetInt("M_BndFixSabotages"); + if (PlayerPrefs.HasKey("M_BndSetAllGhost")) bindSetAllGhost = (KeyCode)PlayerPrefs.GetInt("M_BndSetAllGhost"); + if (PlayerPrefs.HasKey("M_BndSetAllGhostImp")) bindSetAllGhostImp = (KeyCode)PlayerPrefs.GetInt("M_BndSetAllGhostImp"); if (!rgbMenuMode && currentMenuColorIndex >= 0 && currentMenuColorIndex < menuColors.Length) { @@ -3076,6 +4278,10 @@ private void LoadConfig() blockSabotageRPC = LoadBool("M_BlockSabotageRPC", blockSabotageRPC); blockGameRpcInLobby = LoadBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); + blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); + enablePasosLimit = LoadBool("M_PasosLimit", enablePasosLimit); + enableLocalPasosBan = LoadBool("M_AntiPasosLocalBan", enableLocalPasosBan); + enableHostPasosBan = LoadBool("M_AntiPasosHostBan", enableHostPasosBan); AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); AutoReturnLobbyAfterMatch = LoadBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); AutoHostNotifications = LoadBool("M_AutoHostNotifications", AutoHostNotifications); @@ -3527,6 +4733,20 @@ private void DrawBindsTab() DrawKeybindRow("End: Impostor Win:", ref bindEndImp, ref isWaitBindEndImp); DrawKeybindRow("End: Imp Disconnect:", ref bindEndImpDC, ref isWaitBindEndImpDC); DrawKeybindRow("End: H&S Disconnect:", ref bindEndHnsDC, ref isWaitBindEndHnsDC); + DrawKeybindRow("Toggle Tracers:", ref bindToggleTracers, ref isWaitBindToggleTracers); + DrawKeybindRow("Toggle NoClip:", ref bindToggleNoClip, ref isWaitBindToggleNoClip); + DrawKeybindRow("Toggle Freecam:", ref bindToggleFreecam, ref isWaitBindToggleFreecam); + DrawKeybindRow("Toggle Camera Zoom:", ref bindToggleCameraZoom, ref isWaitBindToggleCameraZoom); + DrawKeybindRow("Toggle Player Info:", ref bindTogglePlayerInfo, ref isWaitBindTogglePlayerInfo); + DrawKeybindRow("Toggle See Roles:", ref bindToggleSeeRoles, ref isWaitBindToggleSeeRoles); + DrawKeybindRow("Toggle See Ghosts:", ref bindToggleSeeGhosts, ref isWaitBindToggleSeeGhosts); + DrawKeybindRow("Toggle Full Bright:", ref bindToggleFullBright, ref isWaitBindToggleFullBright); + DrawKeybindRow("Kill All:", ref bindKillAll, ref isWaitBindKillAll); + DrawKeybindRow("Call Meeting:", ref bindCallMeeting, ref isWaitBindCallMeeting); + DrawKeybindRow("Kick All:", ref bindKickAll, ref isWaitBindKickAll); + DrawKeybindRow("Fix Sabotages:", ref bindFixSabotages, ref isWaitBindFixSabotages); + DrawKeybindRow("All -> Ghost:", ref bindSetAllGhost, ref isWaitBindSetAllGhost); + DrawKeybindRow("All -> Ghost Imp:", ref bindSetAllGhostImp, ref isWaitBindSetAllGhostImp); } finally { GUILayout.EndVertical(); } } @@ -3573,6 +4793,20 @@ private void ResetAllBindWaits() isWaitBindEndImpDC = false; isWaitBindEndHnsDC = false; isWaitBindMagnetCursor = false; + isWaitBindToggleTracers = false; + isWaitBindToggleNoClip = false; + isWaitBindToggleFreecam = false; + isWaitBindToggleCameraZoom = false; + isWaitBindKillAll = false; + isWaitBindCallMeeting = false; + isWaitBindTogglePlayerInfo = false; + isWaitBindToggleSeeRoles = false; + isWaitBindToggleSeeGhosts = false; + isWaitBindToggleFullBright = false; + isWaitBindKickAll = false; + isWaitBindFixSabotages = false; + isWaitBindSetAllGhost = false; + isWaitBindSetAllGhostImp = false; } private void DrawGeneralTab() @@ -3734,13 +4968,18 @@ private void DrawGeneralInfoTab() GUILayout.Label($"DEVELOPERS", textStyle); GUILayout.Space(4); GUILayout.BeginHorizontal(); - if (DrawColoredActionButton("abobanamne", new Color32(38, 194, 129, 255), 150f)) - OpenExternalLink("https://github.com/abobanamne", "abobanamne"); + if (DrawColoredActionButton("Carrot", new Color32(38, 194, 129, 255), 150f)) + OpenExternalLink("https://github.com/abobanamne", "Carrot"); GUILayout.Space(6); if (DrawColoredActionButton("wextikit", new Color32(109, 138, 255, 255), 150f)) OpenExternalLink("https://github.com/wextikit", "wextikit"); GUILayout.EndHorizontal(); + GUILayout.Space(10); + GUILayout.Label($"TESTERS", textStyle); + GUILayout.Space(4); + DrawColoredActionButton("Жена", new Color32(109, 138, 255, 255), 150f); + GUILayout.Space(10); GUILayout.Label($"{L("Repository", "Репозиторий")}", textStyle); GUILayout.Label(L( @@ -3790,7 +5029,7 @@ public static void Prefix(PlayerControl sourcePlayer, ref string chatText) var client = AmongUsClient.Instance?.GetClientFromCharacter(sourcePlayer); if (client != null) { - puid = client.Id.ToString(); + puid = GetClientPuid(client); platformStr = ElysiumModMenuGUI.GetPlatform(client); } } @@ -3948,6 +5187,8 @@ private void DrawChatSettingsCompact() alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 230); GUILayout.Space(3); readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 230); + GUILayout.Space(4); + DrawGhostChatColorControl(230f); GUILayout.Space(3); enableExtendedChat = DrawToggle(enableExtendedChat, L("Extended Chat", "Длинный чат"), 230); GUILayout.Space(3); @@ -4066,6 +5307,31 @@ private void DrawChatSettingsCompact() GUILayout.EndVertical(); } + private void DrawGhostChatColorControl(float width) + { + GUILayout.BeginHorizontal(GUILayout.Width(width)); + GUILayout.Label(L("Ghost Chat:", "Ghost Chat:"), new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(88)); + if (DrawPseudoInputButton(ghostChatColorHex, isEditingGhostChatColor, 24f, 16)) + { + isEditingGhostChatColor = !isEditingGhostChatColor; + isEditingName = false; + isEditingLevel = false; + isEditingFriendCode = false; + isEditingLocalFriendCode = false; + isEditingBan = false; + } + if (GUILayout.Button(L("Apply", "OK"), btnStyle, GUILayout.Width(52), GUILayout.Height(24))) + { + isEditingGhostChatColor = false; + ghostChatColorHex = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); + SaveConfig(); + } + GUILayout.EndHorizontal(); + + string previewHex = GetGhostChatColorHex(); + GUILayout.Label($"{L("Preview ghost chat color", "Пример цвета чата призраков")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11 }, GUILayout.Width(width)); + } + private void DrawPlayerMovement() { GUILayout.BeginVertical(boxStyle); @@ -4154,6 +5420,30 @@ private static string SanitizeSpoofFriendCode(string input) return clean; } + private static string SanitizeHexColor(string input, string fallback) + { + string value = (input ?? string.Empty).Trim(); + if (value.StartsWith("#")) value = value.Substring(1); + + string clean = ""; + foreach (char c in value) + { + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) + { + clean += char.ToUpperInvariant(c); + if (clean.Length >= 6) break; + } + } + + return clean.Length == 6 ? "#" + clean : fallback; + } + + public static string GetGhostChatColorHex() + { + ghostChatColorHex = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); + return ghostChatColorHex; + } + private static string BuildLocalNameRenderText(string input) { string value = (input ?? string.Empty).Replace("\r\n", "\n").Replace('\r', '\n'); @@ -4328,7 +5618,7 @@ private void TryAutoBanBrokenFriendCodeTick() try { var client = AmongUsClient.Instance.GetClientFromCharacter(pc); - if (client != null) puid = client.Id.ToString(); + if (client != null) puid = GetClientPuid(client); } catch { } @@ -4358,6 +5648,7 @@ private void DrawSelfSpoof() isEditingName = false; isEditingFriendCode = false; isEditingLocalFriendCode = false; + isEditingGhostChatColor = false; } if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) { @@ -4392,6 +5683,7 @@ private void DrawSelfSpoof() isEditingLevel = false; isEditingFriendCode = false; isEditingLocalFriendCode = false; + isEditingGhostChatColor = false; } if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) { @@ -4424,6 +5716,7 @@ private void DrawSelfSpoof() isEditingName = false; isEditingLevel = false; isEditingFriendCode = false; + isEditingGhostChatColor = false; } if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) { @@ -4448,6 +5741,7 @@ private void DrawSelfSpoof() isEditingName = false; isEditingLevel = false; isEditingLocalFriendCode = false; + isEditingGhostChatColor = false; } if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) { @@ -4670,10 +5964,29 @@ private void DrawPlayersTab() } else { - SetPlayerRole(target, roleAssignOptions[targetRoleAssignIdx]); - ShowNotification($"[ROLE] {target.Data.PlayerName} -> {roleAssignNames[targetRoleAssignIdx]}"); + if (IsGhostRoleSelection(targetRoleAssignIdx)) + { + MakePlayerGhost(target); + } + else if (IsGhostImpostorRoleSelection(targetRoleAssignIdx)) + { + MakePlayerGhost(target, true); + } + else + { + SetPlayerRole(target, roleAssignOptions[targetRoleAssignIdx]); + ShowNotification($"[ROLE] {target.Data.PlayerName} -> {roleAssignNames[targetRoleAssignIdx]}"); + } } } + if (GUILayout.Button("TARGET -> GHOST", btnStyle, GUILayout.Height(26))) + { + MakePlayerGhost(target); + } + GUILayout.EndHorizontal(); + + GUILayout.Space(4); + GUILayout.BeginHorizontal(); if (GUILayout.Button("REVIVE TARGET", activeTabStyle, GUILayout.Height(26))) { RevivePlayer(target); @@ -4781,9 +6094,14 @@ private void DrawPlayersHistoryTab() GUILayout.BeginHorizontal(); GUILayout.Label($"Entries: {playerHistoryEntries.Count}", new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(120)); + GUILayout.Label("File: ElysiumPlayerHistory.txt", new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(190)); GUILayout.FlexibleSpace(); if (GUILayout.Button("Clear History", btnStyle, GUILayout.Width(120), GUILayout.Height(24))) + { playerHistoryEntries.Clear(); + playerHistoryKeysById.Clear(); + WritePlayerHistoryFile(); + } GUILayout.EndHorizontal(); GUILayout.Space(6); @@ -4797,9 +6115,12 @@ private void DrawPlayersHistoryTab() foreach (var e in playerHistoryEntries.OrderByDescending(x => x.LastSeenUtc)) { GUILayout.BeginVertical(boxStyle); - GUILayout.Label($"{e.Name} ({e.Platform})", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 13 }); - GUILayout.Label($"FC: {e.FriendCode}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); - GUILayout.Label($"Lv: {e.Level} | Last: {e.LastSeenUtc:HH:mm:ss}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); + string status = e.IsOnline ? "ONLINE" : "LEFT"; + GUILayout.Label($"{e.Name} {status}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 13 }); + GUILayout.Label($"Lv: {e.Level} | FC: {e.FriendCode} | PUID: {e.Puid}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); + GUILayout.Label($"Joined: {e.FirstSeenUtc:HH:mm:ss} | Left: {(e.LeftUtc.HasValue ? e.LeftUtc.Value.ToString("HH:mm:ss") : "online")}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); + GUILayout.Label($"Platform: {FormatPlatformHistory(e)}", new GUIStyle(GUI.skin.label) { fontSize = 11, wordWrap = true }); + GUILayout.Label($"RPC: {FormatRpcHistory(e)}", new GUIStyle(GUI.skin.label) { fontSize = 11, wordWrap = true }); GUILayout.EndVertical(); GUILayout.Space(2); } @@ -4833,9 +6154,8 @@ private void ForceGlobalEject(PlayerControl target) ShowNotification($"[EJECT] Изгоняем {target.Data.PlayerName}..."); } - catch (Exception ex) + catch (Exception) { - System.Console.WriteLine("Eject Error: " + ex.Message); } } @@ -4911,9 +6231,8 @@ private static void AttemptReportBody(PlayerControl target) PlayerControl.LocalPlayer.CmdReportDeadBody(target.Data); ShowNotification($"[REPORT] Репорт {target.Data.PlayerName}"); } - catch (Exception ex) + catch (Exception) { - System.Console.WriteLine($"Report body error: {ex.Message}"); } } @@ -4938,9 +6257,8 @@ private static void FloodPlayerWithTasks(PlayerControl target) target.Data.RpcSetTasks(taskIds); ShowNotification($"[TASKS] {target.Data.PlayerName} получил flood tasks."); } - catch (Exception ex) + catch (Exception) { - System.Console.WriteLine($"Flood tasks error: {ex.Message}"); } } @@ -4963,9 +6281,8 @@ private static void ClearPlayerTasks(PlayerControl target) target.Data.RpcSetTasks(Array.Empty()); ShowNotification($"[TASKS] Задачи {target.Data.PlayerName} очищены."); } - catch (Exception ex) + catch (Exception) { - System.Console.WriteLine($"Clear tasks error: {ex.Message}"); } } @@ -4977,96 +6294,281 @@ private static string GetRoleDisplayName(RoleTypes role) return role.ToString(); } - public static void RevivePlayer(PlayerControl target) + private static bool IsGhostRoleSelection(int roleIndex) + { + return roleIndex >= 0 && + roleIndex < roleAssignNames.Length && + string.Equals(roleAssignNames[roleIndex], "Ghost", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsGhostImpostorRoleSelection(int roleIndex) + { + return roleIndex >= 0 && + roleIndex < roleAssignNames.Length && + string.Equals(roleAssignNames[roleIndex], "Ghost Imp", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsImpostorTeamRole(RoleTypes role) + { + int roleId = (int)role; + return role == RoleTypes.Impostor || role == RoleTypes.Shapeshifter || roleId == 9 || roleId == 18; + } + + public static void RevivePlayer(PlayerControl target) + { + if (target == null || target.Data == null) + { + ShowNotification("[ОШИБКА] Цель не найдена!"); + return; + } + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ОШИБКА] Требуются права хоста!"); + return; + } + if (!target.Data.IsDead) + { + ShowNotification($"{target.Data.PlayerName} уже жив!"); + return; + } + + try + { + target.Data.IsDead = false; + + if (target.Collider != null) target.Collider.enabled = true; + + if (target.MyPhysics != null) + target.MyPhysics.gameObject.layer = LayerMask.NameToLayer("Players"); + + try + { + var allBehaviours = UnityEngine.Object.FindObjectsOfType(); + foreach (var mb in allBehaviours) + { + if (mb == null || mb.gameObject == null) continue; + Type t = mb.GetType(); + if (t == null || t.Name != "DeadBody") continue; + + byte parentId = byte.MaxValue; + + var parentProp = t.GetProperty("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentProp != null) + { + object val = parentProp.GetValue(mb, null); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + else + { + var parentField = t.GetField("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentField != null) + { + object val = parentField.GetValue(mb); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + } + + if (parentId == target.PlayerId) + mb.gameObject.SetActive(false); + } + } + catch { } + + bool wasImpTeam = false; + try + { + if (target.Data.Role != null) + { + int roleId = (int)target.Data.Role.Role; + wasImpTeam = roleId == 1 || roleId == 5 || roleId == 7 || roleId == 9 || roleId == 18; + } + else + { + var rt = target.Data.RoleType; + wasImpTeam = rt == RoleTypes.Impostor || rt == RoleTypes.Shapeshifter || (int)rt == 9 || (int)rt == 18; + } + } + catch { } + + target.RpcSetRole(wasImpTeam ? RoleTypes.Impostor : RoleTypes.Crewmate, true); + + var netObj = GameData.Instance?.GetComponent(); + if (netObj != null) netObj.SetDirtyBit(uint.MaxValue); + + ShowNotification($"[ВОСКРЕШЕНИЕ] {target.Data.PlayerName} воскрешён!"); + } + catch (Exception) + { + ShowNotification("Ошибка воскрешения!"); + } + } + + public static void MakePlayerGhost(PlayerControl target, bool impostorGhost = false, bool notify = true) + { + if (target == null || target.Data == null) + { + if (notify) ShowNotification("[ОШИБКА] Цель не найдена!"); + return; + } + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + if (notify) ShowNotification("[ОШИБКА] Требуются права хоста!"); + return; + } + if (target.Data.IsDead) + { + if (!TrySetGhostRole(target, impostorGhost, out _)) + SetPlayerRole(target, impostorGhost ? RoleTypes.Impostor : (IsImpostorTeamRole(target.Data.RoleType) ? RoleTypes.Impostor : RoleTypes.Crewmate)); + if (notify) ShowNotification($"{target.Data.PlayerName} уже призрак!"); + return; + } + + try + { + string methodUsed; + if (!TrySetGhostRole(target, impostorGhost, out methodUsed)) + { + RoleTypes fallbackRole = impostorGhost ? RoleTypes.Impostor : (IsImpostorTeamRole(target.Data.RoleType) ? RoleTypes.Impostor : RoleTypes.Crewmate); + SetPlayerRole(target, fallbackRole); + TryActivateGhostState(target, out methodUsed); + } + + var netObj = GameData.Instance?.GetComponent(); + if (netObj != null) netObj.SetDirtyBit(uint.MaxValue); + + if (notify) ShowNotification($"[GHOST] {target.Data.PlayerName} стал призраком ({methodUsed})!"); + } + catch (Exception) + { + if (notify) ShowNotification("Ошибка перевода в призрака!"); + } + } + + private static bool TrySetGhostRole(PlayerControl target, bool impostorGhost, out string methodUsed) + { + methodUsed = string.Empty; + if (target == null || target.Data == null) return false; + + string[] roleNames = impostorGhost + ? new[] { "ImpostorGhost", "GhostImpostor", "ImpGhost", "Ghost" } + : new[] { "CrewmateGhost", "GhostCrewmate", "CrewGhost", "Ghost" }; + + foreach (string roleName in roleNames) + { + if (!Enum.TryParse(roleName, true, out RoleTypes ghostRole)) continue; + + try { target.RpcSetRole(ghostRole, true); } catch { } + try { RoleManager.Instance?.SetRole(target, ghostRole); } catch { } + + methodUsed = $"SetRole:{roleName}"; + return true; + } + + return false; + } + + private static bool TryActivateGhostState(PlayerControl target, out string methodUsed) + { + methodUsed = string.Empty; + if (target == null) return false; + if (target.Data != null && target.Data.IsDead) + { + methodUsed = "already_dead"; + return true; + } + + if (TryDie(target, DeathReason.Exile, true) || + TryDie(target, DeathReason.Exile, false) || + TryDie(target, DeathReason.Kill, true) || + TryDie(target, DeathReason.Kill, false)) + { + methodUsed = "Die"; + return true; + } + + if (TryInvokeNoArg(target, "Exiled") || + TryInvokeNoArg(target, "RpcExiled") || + TryInvokeNoArg(target, "RpcExiledV2") || + TryInvokeNoArg(target, "SetDead")) + { + methodUsed = "Exiled/SetDead"; + return true; + } + + if (TrySetDeadFlag(target)) + { + methodUsed = "Data.IsDead"; + return true; + } + + methodUsed = "fallback"; + return false; + } + + private static bool TryDie(PlayerControl target, DeathReason reason, bool allowAnimation) + { + try { target.Die(reason, allowAnimation); } + catch { } + return target != null && target.Data != null && target.Data.IsDead; + } + + private static bool TryInvokeNoArg(object target, string methodName) + { + if (target == null || string.IsNullOrWhiteSpace(methodName)) return false; + try + { + for (Type type = target.GetType(); type != null; type = type.BaseType) + { + MethodInfo method = type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); + if (method == null) continue; + method.Invoke(target, null); + return target is PlayerControl player && player.Data != null && player.Data.IsDead; + } + } + catch { } + return false; + } + + private static bool TrySetDeadFlag(PlayerControl target) + { + if (target == null || target.Data == null) return false; + try + { + target.Data.IsDead = true; + if (target.Collider != null) target.Collider.enabled = false; + if (target.MyPhysics != null) target.MyPhysics.gameObject.layer = LayerMask.NameToLayer("Ghost"); + } + catch { } + return target.Data.IsDead; + } + + public static void SetAllPlayersGhost() + { + SetAllPlayersGhost(false); + } + + public static void SetAllPlayersGhost(bool impostorGhost) { - if (target == null || target.Data == null) - { - ShowNotification("[ОШИБКА] Цель не найдена!"); - return; - } if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) { ShowNotification("[ОШИБКА] Требуются права хоста!"); return; } - if (!target.Data.IsDead) - { - ShowNotification($"{target.Data.PlayerName} уже жив!"); - return; - } + if (PlayerControl.AllPlayerControls == null) return; - try + int count = 0; + foreach (var pc in PlayerControl.AllPlayerControls) { - target.Data.IsDead = false; - - if (target.Collider != null) target.Collider.enabled = true; - - if (target.MyPhysics != null) - target.MyPhysics.gameObject.layer = LayerMask.NameToLayer("Players"); - - try - { - var allBehaviours = UnityEngine.Object.FindObjectsOfType(); - foreach (var mb in allBehaviours) - { - if (mb == null || mb.gameObject == null) continue; - Type t = mb.GetType(); - if (t == null || t.Name != "DeadBody") continue; - - byte parentId = byte.MaxValue; - - var parentProp = t.GetProperty("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (parentProp != null) - { - object val = parentProp.GetValue(mb, null); - if (val is byte b) parentId = b; - else if (val is int i) parentId = (byte)i; - } - else - { - var parentField = t.GetField("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (parentField != null) - { - object val = parentField.GetValue(mb); - if (val is byte b) parentId = b; - else if (val is int i) parentId = (byte)i; - } - } - - if (parentId == target.PlayerId) - mb.gameObject.SetActive(false); - } - } - catch { } - - bool wasImpTeam = false; - try + if (pc != null && pc.Data != null && !pc.Data.Disconnected) { - if (target.Data.Role != null) - { - int roleId = (int)target.Data.Role.Role; - wasImpTeam = roleId == 1 || roleId == 5 || roleId == 7 || roleId == 9 || roleId == 18; - } - else - { - var rt = target.Data.RoleType; - wasImpTeam = rt == RoleTypes.Impostor || rt == RoleTypes.Shapeshifter || (int)rt == 9 || (int)rt == 18; - } + MakePlayerGhost(pc, impostorGhost, false); + count++; } - catch { } - - target.RpcSetRole(wasImpTeam ? RoleTypes.Impostor : RoleTypes.Crewmate, true); - - var netObj = GameData.Instance?.GetComponent(); - if (netObj != null) netObj.SetDirtyBit(uint.MaxValue); - - ShowNotification($"[ВОСКРЕШЕНИЕ] {target.Data.PlayerName} воскрешён!"); - } - catch (Exception ex) - { - System.Console.WriteLine($"Revive error: {ex.Message}"); - ShowNotification("Ошибка воскрешения!"); } + + ShowNotification($"[GHOST] {count} игрок(а/ов) стали {(impostorGhost ? "ghost impostor" : "призраками")}!"); } public static void SetAllPlayersRole(RoleTypes role) @@ -5313,10 +6815,7 @@ private void callMeetingPublic() PlayerControl.LocalPlayer.CmdReportDeadBody(null); ShowNotification("[MEETING] Легально нажата кнопка собрания!"); } - catch (Exception ex) - { - Debug.Log("Public Meeting Error: " + ex.Message); - } + catch { } } private void TriggerAllSabotages() { @@ -5335,7 +6834,7 @@ private void TriggerAllSabotages() ShowNotification("[SABOTAGE] Все системы саботированы!"); } - catch (Exception ex) { Debug.Log("Trigger All Sabotages Error: " + ex.Message); } + catch { } } private void FixAllSabotages() { @@ -5366,7 +6865,7 @@ private void FixAllSabotages() try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.MushroomMixupSabotage, 0); } catch { } ShowNotification("[SABOTAGE] Все саботажи и двери починены!"); } - catch (Exception ex) { Debug.Log("Fix All Sabotages Error: " + ex.Message); } + catch { } } private void SabotageDoors() @@ -5569,7 +7068,21 @@ private void DrawPlayersRoles() GUILayout.Space(5); if (GUILayout.Button("SET ALL PLAYERS ROLE", activeTabStyle, GUILayout.Height(28))) - SetAllPlayersRole(roleAssignOptions[allPlayersRoleAssignIdx]); + { + if (IsGhostRoleSelection(allPlayersRoleAssignIdx)) + SetAllPlayersGhost(); + else if (IsGhostImpostorRoleSelection(allPlayersRoleAssignIdx)) + SetAllPlayersGhost(true); + else + SetAllPlayersRole(roleAssignOptions[allPlayersRoleAssignIdx]); + } + GUILayout.Space(4); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("ALL -> GHOST", btnStyle, GUILayout.Height(26))) + SetAllPlayersGhost(); + if (GUILayout.Button("ALL -> GHOST IMP", btnStyle, GUILayout.Height(26))) + SetAllPlayersGhost(true); + GUILayout.EndHorizontal(); GUILayout.EndVertical(); GUILayout.Space(10); @@ -5740,131 +7253,559 @@ private void DrawOutfitsTab() { foreach (var pc in lockedPlayersList) { - if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null) continue; + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null) continue; + + GUILayout.BeginHorizontal(boxStyle); + try + { + string pName = pc.Data.PlayerName ?? "Unknown"; + GUILayout.Label(pName, GUILayout.Width(150)); + + if (GUILayout.Button("Copy Outfit", btnStyle, GUILayout.Height(25))) + { + try + { + PlayerControl.LocalPlayer.RpcSetSkin(pc.Data.DefaultOutfit.SkinId); + PlayerControl.LocalPlayer.RpcSetHat(pc.Data.DefaultOutfit.HatId); + PlayerControl.LocalPlayer.RpcSetVisor(pc.Data.DefaultOutfit.VisorId); + PlayerControl.LocalPlayer.RpcSetNamePlate(pc.Data.DefaultOutfit.NamePlateId); + PlayerControl.LocalPlayer.RpcSetPet(pc.Data.DefaultOutfit.PetId); + } + catch { } + } + } + finally { GUILayout.EndHorizontal(); } + GUILayout.Space(2); + } + } + else + { + GUILayout.Label("Нет игроков для копирования."); + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + } + public static bool removePenalty = true; + public static bool alwaysShowLobbyTimer = false; + public static bool enableChatLog = true; + public static bool enableFastChat = true; + public static bool allowLinksAndSymbols = false; + + private static readonly System.Collections.Generic.Dictionary CachedSprites = new(); + + public static Sprite LoadEmbeddedSprite(string fileName, float pixelsPerUnit = 1f) + { + string path = $"ElysiumModMenu.{fileName}"; + + try + { + if (CachedSprites.TryGetValue(path + pixelsPerUnit, out var cachedSprite)) + return cachedSprite; + + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream(path); + + if (stream == null) + { + return null; + } + + var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); + using System.IO.MemoryStream ms = new System.IO.MemoryStream(); + stream.CopyTo(ms); + + ImageConversion.LoadImage(texture, ms.ToArray(), false); + + Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); + + sprite.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontSaveInEditor; + + return CachedSprites[path + pixelsPerUnit] = sprite; + } + catch (System.Exception) + { + return null; + } + } + public void Start() + { + if (enableBackground) LoadBackgroundImage(); + UnlockCosmetics(); + LoadConfig(); + LoadBanList(); + StartBackgroundAnomalyLogMonitor(); + + + try + { + int starts = UnityEngine.PlayerPrefs.GetInt("Elysium_GameStarts", 0); + starts++; + + string chatLogPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ChatLog.txt"); + + if (starts >= 3) + { + if (System.IO.File.Exists(chatLogPath)) + { + System.IO.File.WriteAllText(chatLogPath, string.Empty); + } + starts = 0; + } + + UnityEngine.PlayerPrefs.SetInt("Elysium_GameStarts", starts); + UnityEngine.PlayerPrefs.Save(); + } + catch { } + } + + public void OnApplicationQuit() + { + StopBackgroundAnomalyLogMonitor(); + SaveConfig(); + } + + public void OnDisable() + { + SaveConfig(); + } + + public static void SendAnomalyAlert(string title, string message, string dedupeKey = null, bool waitForCompletion = false, IEnumerable attachmentPaths = null) + { + if (!enableAnomalyLogReports) return; + if (!DiscordStatusReporter.IsEnabled) return; + string webhookUrl = DiscordStatusReporter.ConfiguredWebhookUrl; + if (!DiscordStatusReporter.IsValidWebhookUrl(webhookUrl)) return; + int attachmentCount = attachmentPaths == null ? 0 : attachmentPaths.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().Count(); + System.Console.WriteLine($"[ElysiumModMenu] Sending freeze/overload logs to configured webhook. Type={title}. Attachments={attachmentCount}."); + DiscordStatusReporter.SendDiagnosticAlert(webhookUrl, title, message, waitForCompletion, attachmentPaths); + } + + private static void StartBackgroundAnomalyLogMonitor() + { + try + { + if (anomalyLogMonitorTimer != null) return; + anomalyLogMonitorTimer = new System.Threading.Timer(_ => TryDetectLogBurstTick(false), null, 1000, 1000); + System.Console.WriteLine("[ElysiumModMenu] Background freeze/overload log monitor started."); + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Failed to start background log monitor: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static void StopBackgroundAnomalyLogMonitor() + { + try + { + anomalyLogMonitorTimer?.Dispose(); + anomalyLogMonitorTimer = null; + } + catch { } + } + + private static float GetLogMonitorSeconds() + { + try { return (float)(DateTime.UtcNow - logMonitorStartedUtc).TotalSeconds; } + catch { return 0f; } + } + + private static string BuildAnomalyReportDetails(bool allowUnityAccess = true) + { + if (!allowUnityAccess) + return anomalyReportDetailsCache + "\nmonitor=background"; + + string clientId = "Unknown"; + string networkMode = "Unknown"; + string inGame = "no"; + string host = "no"; + string platform = "Unknown"; + + try + { + if (AmongUsClient.Instance != null) + { + clientId = AmongUsClient.Instance.ClientId.ToString(); + networkMode = AmongUsClient.Instance.NetworkMode.ToString(); + host = AmongUsClient.Instance.AmHost ? "yes" : "no"; + + ClientData client = AmongUsClient.Instance.GetClientFromCharacter(PlayerControl.LocalPlayer); + if (client != null) + { + platform = GetPlatform(client); + } + } + } + catch { } + + try { inGame = ShipStatus.Instance != null && LobbyBehaviour.Instance == null ? "yes" : "no"; } catch { } + + string details = $"sessionId={relaySessionId}\nclientId={clientId}\nnetworkMode={networkMode}\nhost={host}\nplatform={platform}\ninGame={inGame}\nmonitor=unity"; + anomalyReportDetailsCache = details; + return details; + } + + private static void TryDetectLogBurstTick(bool allowUnityAccess = true) + { + if (!enableAnomalyLogReports) return; + lock (anomalyLogMonitorLock) + { + try + { + float now = GetLogMonitorSeconds(); + if (now < logMonitorNextScanAt) return; + logMonitorNextScanAt = now + LogBurstScanIntervalSeconds; - GUILayout.BeginHorizontal(boxStyle); - try + List watchedFiles = GetWatchedLogFiles().ToList(); + if (!anomalyLogWatchNotified) { - string pName = pc.Data.PlayerName ?? "Unknown"; - GUILayout.Label(pName, GUILayout.Width(150)); + anomalyLogWatchNotified = true; + System.Console.WriteLine($"[ElysiumModMenu] Freeze/overload log reporting is enabled. Watching: {string.Join(", ", watchedFiles.Select(System.IO.Path.GetFileName).ToArray())}. Sends only summary counters when error/red logs appear or {LogBurstLineThreshold}+ new lines arrive within {LogBurstWindowSeconds:0}s."); + } - if (GUILayout.Button("Copy Outfit", btnStyle, GUILayout.Height(25))) + int newLines = 0; + int errorLines = 0; + int storedMsgLines = 0; + List touchedLogs = new List(); + List touchedLogFiles = new List(); + foreach (string file in watchedFiles) + { + List addedLines = ReadNewLogLines(file, out int currentLines); + if (!watchedLogLineCounts.TryGetValue(file, out int previousLines)) { - try - { - PlayerControl.LocalPlayer.RpcSetSkin(pc.Data.DefaultOutfit.SkinId); - PlayerControl.LocalPlayer.RpcSetHat(pc.Data.DefaultOutfit.HatId); - PlayerControl.LocalPlayer.RpcSetVisor(pc.Data.DefaultOutfit.VisorId); - PlayerControl.LocalPlayer.RpcSetNamePlate(pc.Data.DefaultOutfit.NamePlateId); - PlayerControl.LocalPlayer.RpcSetPet(pc.Data.DefaultOutfit.PetId); - } - catch { } + watchedLogLineCounts[file] = currentLines; + addedLines = ReadInitialRecentLogTail(file); + if (addedLines.Count <= 0) continue; + } + else + { + watchedLogLineCounts[file] = currentLines; } + + if (addedLines.Count <= 0) continue; + + newLines += addedLines.Count; + touchedLogs.Add(System.IO.Path.GetFileName(file)); + touchedLogFiles.Add(file); + errorLines += addedLines.Count(IsErrorLogLine); + storedMsgLines += addedLines.Count(IsStoredMessageOverloadLine); } - finally { GUILayout.EndHorizontal(); } - GUILayout.Space(2); + + if (newLines <= 0) return; + + if (logBurstWindowStartedAt < 0f || now - logBurstWindowStartedAt > LogBurstWindowSeconds) + { + logBurstWindowStartedAt = now; + logBurstLineCount = 0; + } + + logBurstLineCount += newLines; + bool isStoredRpcBurst = storedMsgLines > 0; + bool isErrorBurst = errorLines > 0; + bool isLineBurst = logBurstLineCount >= LogBurstLineThreshold; + if ((!isErrorBurst && !isLineBurst) || (!isStoredRpcBurst && now < logBurstCooldownUntil)) return; + + logBurstCooldownUntil = now + (isStoredRpcBurst ? 5f : LogBurstAlertCooldownSeconds); + string reason = isStoredRpcBurst ? "stored rpc overload detected" : (isErrorBurst ? "error/red log detected" : "log spam detected"); + string message = $"{BuildAnomalyReportDetails(allowUnityAccess)}\nnewLogLines={logBurstLineCount}\nerrorLines={errorLines}\nstoredMsgLines={storedMsgLines}\nwindowSeconds={LogBurstWindowSeconds}\nthreshold={LogBurstLineThreshold}\nreason={reason}, needs fix\nwatchedLogs={string.Join(", ", touchedLogs.Distinct().ToArray())}"; + SendAnomalyAlert(isErrorBurst ? "Abnormal error log" : "Abnormal log spam", message, "abnormal-log-spam", !allowUnityAccess, touchedLogFiles); + logBurstWindowStartedAt = -1f; + logBurstLineCount = 0; + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Log monitor failed: {ex.GetType().Name}: {ex.Message}"); } } - else - { - GUILayout.Label("Нет игроков для копирования."); - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); } - public static bool removePenalty = true; - public static bool alwaysShowLobbyTimer = false; - public static bool enableChatLog = true; - public static bool enableFastChat = true; - public static bool allowLinksAndSymbols = false; - private static readonly System.Collections.Generic.Dictionary CachedSprites = new(); - - public static Sprite LoadEmbeddedSprite(string fileName, float pixelsPerUnit = 1f) + private static IEnumerable GetWatchedLogFiles() { - string path = $"ElysiumModMenu.{fileName}"; + string root = GetAmongUsRoot(); + List files = new List(); try { - if (CachedSprites.TryGetValue(path + pixelsPerUnit, out var cachedSprite)) - return cachedSprite; + string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string unityLogRoot = System.IO.Path.Combine(userProfile, "AppData", "LocalLow", "Innersloth", "Among Us"); + AddLogPath(files, System.IO.Path.Combine(unityLogRoot, "Player.log")); + AddLogPath(files, System.IO.Path.Combine(unityLogRoot, "Player-prev.log")); + } + catch { } - var assembly = System.Reflection.Assembly.GetExecutingAssembly(); - using var stream = assembly.GetManifestResourceStream(path); + AddLogPath(files, System.IO.Path.Combine(root, "BepInEx", "ErrorLog.log")); + AddLogPath(files, System.IO.Path.Combine(root, "ErrorLog.log")); + AddLogPath(files, System.IO.Path.Combine(root, "BepInEx", "LogOutput.log")); + AddLogPath(files, System.IO.Path.Combine(root, "LogOutput.log")); - if (stream == null) + try + { + string[] banLogDirs = { - System.Console.WriteLine($"[ELYSIUM] Стрим равен null! Убедись, что {fileName} установлен как Embedded Resource."); - return null; + System.IO.Path.Combine(root, "BepInEx", "BAN_DATA", "LOG"), + System.IO.Path.Combine(root, "BAN_DATA", "LOG") + }; + + foreach (string banLogDir in banLogDirs) + { + if (!System.IO.Directory.Exists(banLogDir)) continue; + foreach (string file in System.IO.Directory.GetFiles(banLogDir)) + AddLogPath(files, file); } + } + catch { } - var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); - using System.IO.MemoryStream ms = new System.IO.MemoryStream(); - stream.CopyTo(ms); + try + { + string playerLogRoot = System.IO.Path.Combine(root, "ElysiumModMenu", "PlayerLogs"); + if (System.IO.Directory.Exists(playerLogRoot)) + { + foreach (string dir in System.IO.Directory.GetDirectories(playerLogRoot)) + { + AddLogPath(files, System.IO.Path.Combine(dir, "LogOutput.txt")); + AddLogPath(files, System.IO.Path.Combine(dir, "LogOutput.log")); + } + } + } + catch { } - ImageConversion.LoadImage(texture, ms.ToArray(), false); + return files; + } - Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); + private static void AddLogPath(List files, string file) + { + try + { + if (!string.IsNullOrWhiteSpace(file) && System.IO.File.Exists(file) && !files.Contains(file)) + files.Add(file); + } + catch { } + } - sprite.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontSaveInEditor; + private static List ReadNewLogLines(string file, out int currentLines) + { + currentLines = 0; + List lines = new List(); + try + { + if (string.IsNullOrWhiteSpace(file) || !System.IO.File.Exists(file)) return lines; - return CachedSprites[path + pixelsPerUnit] = sprite; + watchedLogLineCounts.TryGetValue(file, out int previousLines); + using (System.IO.FileStream stream = new System.IO.FileStream(file, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete)) + using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8, true)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + currentLines++; + if (currentLines > previousLines) + lines.Add(line); + } + } } - catch (System.Exception ex) + catch { - System.Console.WriteLine($"[ELYSIUM] Ошибка загрузки спрайта {fileName}: " + ex.Message); - return null; } - } - public void Start() - { - if (enableBackground) LoadBackgroundImage(); - UnlockCosmetics(); - LoadConfig(); - LoadBanList(); + return lines; + } + private static List ReadInitialRecentLogTail(string file) + { + List result = new List(); try { - int starts = UnityEngine.PlayerPrefs.GetInt("Elysium_GameStarts", 0); - starts++; - - string chatLogPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ChatLog.txt"); + if (string.IsNullOrWhiteSpace(file) || !System.IO.File.Exists(file)) return result; - if (starts >= 3) + Queue errorTail = new Queue(); + using (System.IO.FileStream stream = new System.IO.FileStream(file, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete)) + using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8, true)) { - if (System.IO.File.Exists(chatLogPath)) + string line; + while ((line = reader.ReadLine()) != null) { - System.IO.File.WriteAllText(chatLogPath, string.Empty); + if (!IsErrorLogLine(line)) continue; + errorTail.Enqueue(line); + while (errorTail.Count > InitialLogTailLineLimit) + errorTail.Dequeue(); } - starts = 0; } - UnityEngine.PlayerPrefs.SetInt("Elysium_GameStarts", starts); - UnityEngine.PlayerPrefs.Save(); + result.AddRange(errorTail); } catch { } + + return result; } - public void OnApplicationQuit() + private static bool IsErrorLogLine(string line) { - SaveConfig(); + if (string.IsNullOrWhiteSpace(line)) return false; + string lower = line.ToLowerInvariant(); + if (lower.Contains("[elysiummodmenu]")) return false; + if (lower.Contains("method ") && lower.Contains(" has unsupported ") && lower.Contains("elysiummodmenu")) return false; + if (lower.Contains("registered mono type") && lower.Contains("elysiummodmenu")) return false; + return lower.Contains("[error") || + lower.Contains("[fatal") || + lower.Contains("exception") || + lower.Contains(" stack trace") || + lower.Contains("traceback") || + lower.Contains("stored data") || + lower.Contains("storeddata") || + IsStoredMessageOverloadLine(lower) || + lower.Contains("overload") || + lower.Contains("freeze") || + lower.Contains("color=red") || + lower.Contains("#ff0000"); } - public void OnDisable() + public static bool IsRelevantAnomalyLine(string line) { - SaveConfig(); + if (string.IsNullOrWhiteSpace(line)) return false; + string lower = line.ToLowerInvariant(); + if (lower.Contains("[elysiummodmenu]")) return false; + if (lower.Contains("bepinex") && lower.Contains("chainloader")) return false; + if (lower.Contains("registered mono type") && lower.Contains("elysiummodmenu")) return false; + if (lower.Contains("method ") && lower.Contains(" has unsupported ") && lower.Contains("elysiummodmenu")) return false; + return IsStoredMessageOverloadLine(lower) || + lower.Contains("[error") || + lower.Contains("[fatal") || + lower.Contains("nullreferenceexception") || + lower.Contains("invaliddataexception") || + lower.Contains("exception:") || + lower.Contains(" stack trace") || + lower.Contains("traceback") || + lower.Contains("stored data") || + lower.Contains("storeddata"); + } + + private static bool IsStoredMessageOverloadLine(string line) + { + if (string.IsNullOrWhiteSpace(line)) return false; + string lower = line.ToLowerInvariant(); + return lower.Contains("stored msg") && lower.Contains(" rpc "); + } + + private static string GetAmongUsRoot() + { + try { return System.IO.Directory.GetCurrentDirectory(); } + catch { return string.Empty; } + } + + private static string EscapeJson(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + + StringBuilder builder = new StringBuilder(value.Length + 16); + foreach (char c in value) + { + switch (c) + { + case '\\': builder.Append("\\\\"); break; + case '"': builder.Append("\\\""); break; + case '\n': builder.Append("\\n"); break; + case '\r': builder.Append("\\r"); break; + case '\t': builder.Append("\\t"); break; + default: + if (c < 32) builder.Append("\\u").Append(((int)c).ToString("x4")); + else builder.Append(c); + break; + } + } + + return builder.ToString(); + } + + private void TrySendDiscordLaunchStatusTick() + { + if (discordLaunchStatusSent || !DiscordStatusReporter.IsEnabled) return; + if (Time.unscaledTime < discordLaunchStatusNextTryAt) return; + + string webhookUrl = DiscordStatusReporter.ConfiguredWebhookUrl.Trim(); + if (!DiscordStatusReporter.IsValidWebhookUrl(webhookUrl)) + { + discordLaunchStatusNextTryAt = Time.unscaledTime + 10f; + if (!discordInvalidWebhookNotified) + { + discordInvalidWebhookNotified = true; + } + return; + } + + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) + { + discordLaunchStatusNextTryAt = Time.unscaledTime + 2f; + return; + } + + string nickname = "Unknown"; + string friendCode = "Hidden"; + string puid = "Unknown"; + string platform = "Unknown"; + string roomCode = "No room"; + int level = 0; + + try + { + nickname = string.IsNullOrWhiteSpace(PlayerControl.LocalPlayer.Data.PlayerName) + ? "Unknown" + : PlayerControl.LocalPlayer.Data.PlayerName; + if (DiscordStatusReporter.IncludeLocalPuid) + friendCode = GetDisplayedFriendCode(PlayerControl.LocalPlayer.Data, "Hidden"); + + uint rawLevel = PlayerControl.LocalPlayer.Data.PlayerLevel; + if (rawLevel != uint.MaxValue && rawLevel < 10000) level = (int)rawLevel + 1; + } + catch { } + + try + { + ClientData client = AmongUsClient.Instance?.GetClientFromCharacter(PlayerControl.LocalPlayer); + if (client != null) + { + puid = GetClientPuid(client); + platform = GetPlatform(client); + } + } + catch { } + + try + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.GameId != 0) + roomCode = AmongUsClient.Instance.GameId.ToString(); + } + catch { } + + DiscordStatusReporter.SendLaunchStatus(webhookUrl, nickname, friendCode, puid, platform, level, roomCode, DiscordStatusReporter.IncludeLocalPuid); + discordLaunchStatusSent = true; } public static KeyCode bindMagnetCursor = KeyCode.F9; public static bool isWaitBindMagnetCursor = false; + + private bool CanRunHostBind(string actionName) + { + try + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) return true; + } + catch { } + + ShowNotification($"[BIND] {actionName}: host only"); + return false; + } + public void Update() { - bool isTypingOrBinding = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingBan || customChatInputFocused || + bool isTypingOrBinding = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingGhostChatColor || isEditingBan || customChatInputFocused || isWaitingForBind || isWaitBindMassMorph || isWaitBindSpawnLobby || isWaitBindDespawnLobby || isWaitBindCloseMeeting || isWaitBindInstaStart || isWaitBindEndCrew || isWaitBindEndImp || isWaitBindEndImpDC || isWaitBindEndHnsDC || - isWaitBindMagnetCursor; + isWaitBindMagnetCursor || isWaitBindToggleTracers || isWaitBindToggleNoClip || + isWaitBindToggleFreecam || isWaitBindToggleCameraZoom || isWaitBindKillAll || + isWaitBindCallMeeting || isWaitBindTogglePlayerInfo || isWaitBindToggleSeeRoles || + isWaitBindToggleSeeGhosts || isWaitBindToggleFullBright || isWaitBindKickAll || + isWaitBindFixSabotages || isWaitBindSetAllGhost || isWaitBindSetAllGhostImp; KeyCode activeMenuKey = menuToggleKey == KeyCode.None ? KeyCode.Insert : menuToggleKey; if (!isTypingOrBinding && Input.GetKeyDown(activeMenuKey)) @@ -5876,18 +7817,28 @@ public void Update() if (!isTypingOrBinding) { if (bindMassMorph != KeyCode.None && Input.GetKeyDown(bindMassMorph)) - this.StartCoroutine(MassMorphCoroutine().WrapToIl2Cpp()); + { + if (CanRunHostBind("Mass Morph")) + this.StartCoroutine(MassMorphCoroutine().WrapToIl2Cpp()); + } if (bindSpawnLobby != KeyCode.None && Input.GetKeyDown(bindSpawnLobby)) - SpawnLobby(); + { + if (CanRunHostBind("Spawn Lobby")) SpawnLobby(); + } if (bindDespawnLobby != KeyCode.None && Input.GetKeyDown(bindDespawnLobby)) - DespawnLobby(); + { + if (CanRunHostBind("Despawn Lobby")) DespawnLobby(); + } - if (bindCloseMeeting != KeyCode.None && Input.GetKeyDown(bindCloseMeeting) && MeetingHud.Instance != null) - MeetingHud.Instance.RpcClose(); + if (bindCloseMeeting != KeyCode.None && Input.GetKeyDown(bindCloseMeeting)) + { + if (CanRunHostBind("Close Meeting") && MeetingHud.Instance != null) + MeetingHud.Instance.RpcClose(); + } - if (bindInstaStart != KeyCode.None && Input.GetKeyDown(bindInstaStart) && GameStartManager.Instance != null) + if (bindInstaStart != KeyCode.None && Input.GetKeyDown(bindInstaStart) && CanRunHostBind("Insta Start") && GameStartManager.Instance != null) { GameStartManager.Instance.startState = GameStartManager.StartingStates.Countdown; GameStartManager.Instance.countDownTimer = 0f; @@ -5899,14 +7850,62 @@ public void Update() "[MAGNET] Magnet Cursor: ON" : "[MAGNET] Magnet Cursor: OFF"); } - if (bindEndCrew != KeyCode.None && Input.GetKeyDown(bindEndCrew)) SmartEndGame("CrewWin"); - if (bindEndImp != KeyCode.None && Input.GetKeyDown(bindEndImp)) SmartEndGame("ImpWin"); - if (bindEndImpDC != KeyCode.None && Input.GetKeyDown(bindEndImpDC)) SmartEndGame("ImpDisconnect"); - if (bindEndHnsDC != KeyCode.None && Input.GetKeyDown(bindEndHnsDC)) SmartEndGame("HnsImpDisconnect"); + if (bindEndCrew != KeyCode.None && Input.GetKeyDown(bindEndCrew) && CanRunHostBind("End: Crewmate Win")) SmartEndGame("CrewWin"); + if (bindEndImp != KeyCode.None && Input.GetKeyDown(bindEndImp) && CanRunHostBind("End: Impostor Win")) SmartEndGame("ImpWin"); + if (bindEndImpDC != KeyCode.None && Input.GetKeyDown(bindEndImpDC) && CanRunHostBind("End: Imp Disconnect")) SmartEndGame("ImpDisconnect"); + if (bindEndHnsDC != KeyCode.None && Input.GetKeyDown(bindEndHnsDC) && CanRunHostBind("End: H&S Disconnect")) SmartEndGame("HnsImpDisconnect"); + if (bindToggleTracers != KeyCode.None && Input.GetKeyDown(bindToggleTracers)) + { + showTracers = !showTracers; + ShowNotification(showTracers ? "[TRACERS] ON" : "[TRACERS] OFF"); + } + if (bindToggleNoClip != KeyCode.None && Input.GetKeyDown(bindToggleNoClip)) + { + noClip = !noClip; + ShowNotification(noClip ? "[NOCLIP] ON" : "[NOCLIP] OFF"); + } + if (bindToggleFreecam != KeyCode.None && Input.GetKeyDown(bindToggleFreecam)) + { + freecam = !freecam; + ShowNotification(freecam ? "[FREECAM] ON" : "[FREECAM] OFF"); + } + if (bindToggleCameraZoom != KeyCode.None && Input.GetKeyDown(bindToggleCameraZoom)) + { + cameraZoom = !cameraZoom; + ShowNotification(cameraZoom ? "[ZOOM] ON" : "[ZOOM] OFF"); + } + if (bindTogglePlayerInfo != KeyCode.None && Input.GetKeyDown(bindTogglePlayerInfo)) + { + showPlayerInfo = !showPlayerInfo; + ShowNotification(showPlayerInfo ? "[PLAYER INFO] ON" : "[PLAYER INFO] OFF"); + } + if (bindToggleSeeRoles != KeyCode.None && Input.GetKeyDown(bindToggleSeeRoles)) + { + seeRoles = !seeRoles; + ShowNotification(seeRoles ? "[ROLES] ON" : "[ROLES] OFF"); + } + if (bindToggleSeeGhosts != KeyCode.None && Input.GetKeyDown(bindToggleSeeGhosts)) + { + seeGhosts = !seeGhosts; + ShowNotification(seeGhosts ? "[GHOSTS] ON" : "[GHOSTS] OFF"); + } + if (bindToggleFullBright != KeyCode.None && Input.GetKeyDown(bindToggleFullBright)) + { + fullBright = !fullBright; + ShowNotification(fullBright ? "[FULL BRIGHT] ON" : "[FULL BRIGHT] OFF"); + } + if (bindKillAll != KeyCode.None && Input.GetKeyDown(bindKillAll) && CanRunHostBind("Kill All")) KillAll(); + if (bindCallMeeting != KeyCode.None && Input.GetKeyDown(bindCallMeeting) && CanRunHostBind("Call Meeting")) callMeetingPublic(); + if (bindKickAll != KeyCode.None && Input.GetKeyDown(bindKickAll) && CanRunHostBind("Kick All")) KickAll(); + if (bindFixSabotages != KeyCode.None && Input.GetKeyDown(bindFixSabotages) && CanRunHostBind("Fix Sabotages")) FixAllSabotages(); + if (bindSetAllGhost != KeyCode.None && Input.GetKeyDown(bindSetAllGhost) && CanRunHostBind("All -> Ghost")) SetAllPlayersGhost(); + if (bindSetAllGhostImp != KeyCode.None && Input.GetKeyDown(bindSetAllGhostImp) && CanRunHostBind("All -> Ghost Imp")) SetAllPlayersGhost(true); } ElysiumAutoHostService.Tick(); ElysiumAutoLobbyReturn.UpdateLogic(); + TrySendDiscordLaunchStatusTick(); + TryDetectLogBurstTick(); if (votekickEveryone) { TickVotekickEveryoneRun(); @@ -6292,16 +8291,21 @@ public void OnGUI() { Event e = Event.current; - bool isTyping = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingBan; + bool isTyping = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingGhostChatColor || isEditingBan; bool isBinding = isWaitingForBind || isWaitBindMassMorph || isWaitBindSpawnLobby || isWaitBindDespawnLobby || isWaitBindCloseMeeting || isWaitBindInstaStart || isWaitBindEndCrew || isWaitBindEndImp || - isWaitBindEndImpDC || isWaitBindEndHnsDC || isWaitBindMagnetCursor; + isWaitBindEndImpDC || isWaitBindEndHnsDC || isWaitBindMagnetCursor || isWaitBindToggleTracers || + isWaitBindToggleNoClip || isWaitBindToggleFreecam || isWaitBindToggleCameraZoom || + isWaitBindKillAll || isWaitBindCallMeeting || isWaitBindTogglePlayerInfo || + isWaitBindToggleSeeRoles || isWaitBindToggleSeeGhosts || isWaitBindToggleFullBright || + isWaitBindKickAll || isWaitBindFixSabotages || isWaitBindSetAllGhost || + isWaitBindSetAllGhostImp; if (e != null && e.isKey && e.type == EventType.KeyDown) { if (e.keyCode == KeyCode.Escape) { - isEditingName = isEditingLevel = isEditingFriendCode = isEditingLocalFriendCode = isEditingBan = false; + isEditingName = isEditingLevel = isEditingFriendCode = isEditingLocalFriendCode = isEditingGhostChatColor = isEditingBan = false; ResetAllBindWaits(); e.Use(); } @@ -6318,6 +8322,20 @@ public void OnGUI() else if (isWaitBindEndImpDC) { bindEndImpDC = e.keyCode; } else if (isWaitBindEndHnsDC) { bindEndHnsDC = e.keyCode; } else if (isWaitBindMagnetCursor) { bindMagnetCursor = e.keyCode; } + else if (isWaitBindToggleTracers) { bindToggleTracers = e.keyCode; } + else if (isWaitBindToggleNoClip) { bindToggleNoClip = e.keyCode; } + else if (isWaitBindToggleFreecam) { bindToggleFreecam = e.keyCode; } + else if (isWaitBindToggleCameraZoom) { bindToggleCameraZoom = e.keyCode; } + else if (isWaitBindKillAll) { bindKillAll = e.keyCode; } + else if (isWaitBindCallMeeting) { bindCallMeeting = e.keyCode; } + else if (isWaitBindTogglePlayerInfo) { bindTogglePlayerInfo = e.keyCode; } + else if (isWaitBindToggleSeeRoles) { bindToggleSeeRoles = e.keyCode; } + else if (isWaitBindToggleSeeGhosts) { bindToggleSeeGhosts = e.keyCode; } + else if (isWaitBindToggleFullBright) { bindToggleFullBright = e.keyCode; } + else if (isWaitBindKickAll) { bindKickAll = e.keyCode; } + else if (isWaitBindFixSabotages) { bindFixSabotages = e.keyCode; } + else if (isWaitBindSetAllGhost) { bindSetAllGhost = e.keyCode; } + else if (isWaitBindSetAllGhostImp) { bindSetAllGhostImp = e.keyCode; } ResetAllBindWaits(); SaveConfig(); @@ -6330,6 +8348,7 @@ public void OnGUI() else if (isEditingLevel && HandleClipboardShortcut(e, ref spoofLevelString)) { } else if (isEditingFriendCode && HandleClipboardShortcut(e, ref spoofFriendCodeInput)) { } else if (isEditingLocalFriendCode && HandleClipboardShortcut(e, ref localFriendCodeInput)) { } + else if (isEditingGhostChatColor && HandleClipboardShortcut(e, ref ghostChatColorHex, 7)) { } else if (e.keyCode == KeyCode.Backspace) { if (isEditingBan && banInput.Length > 0) { banInput = banInput.Substring(0, banInput.Length - 1); } @@ -6337,6 +8356,7 @@ public void OnGUI() if (isEditingLevel && spoofLevelString.Length > 0) { spoofLevelString = spoofLevelString.Substring(0, spoofLevelString.Length - 1); } if (isEditingFriendCode && spoofFriendCodeInput.Length > 0) { spoofFriendCodeInput = spoofFriendCodeInput.Substring(0, spoofFriendCodeInput.Length - 1); } if (isEditingLocalFriendCode && localFriendCodeInput.Length > 0) { localFriendCodeInput = localFriendCodeInput.Substring(0, localFriendCodeInput.Length - 1); } + if (isEditingGhostChatColor && ghostChatColorHex.Length > 0) { ghostChatColorHex = ghostChatColorHex.Substring(0, ghostChatColorHex.Length - 1); } e.Use(); } else if (e.character != 0 && e.character != '\n' && e.character != '\r') @@ -6346,6 +8366,7 @@ public void OnGUI() if (isEditingLevel) { spoofLevelString += e.character; } if (isEditingFriendCode) { spoofFriendCodeInput += e.character; } if (isEditingLocalFriendCode) { localFriendCodeInput += e.character; } + if (isEditingGhostChatColor && ghostChatColorHex.Length < 7) { ghostChatColorHex += e.character; } e.Use(); } } @@ -6386,7 +8407,7 @@ public void OnGUI() List currentIds = new List(); foreach (var pc in PlayerControl.AllPlayerControls) { - if (pc != null && pc.Data != null) + if (pc != null && pc.Data != null && !pc.Data.Disconnected) { currentIds.Add(pc.PlayerId); UpsertPlayerHistory(pc); @@ -6437,7 +8458,11 @@ public void OnGUI() foreach (var id in lastPlayerIds) { - if (!currentIds.Contains(id)) pendingJoinTimers.Remove(id); + if (!currentIds.Contains(id)) + { + pendingJoinTimers.Remove(id); + MarkPlayerHistoryLeft(id); + } } lastPlayerIds = new List(currentIds); @@ -6445,6 +8470,8 @@ public void OnGUI() } else { + foreach (var id in lastPlayerIds) + MarkPlayerHistoryLeft(id); lastPlayerIds.Clear(); pendingJoinTimers.Clear(); } @@ -6569,13 +8596,11 @@ private int ExecuteVotekickEveryone(bool rememberTargets) votekickedPlayerIds.Add(clientId); ShowNotification($"[VOTEKICK] 3 votes sent to {pc.Data.PlayerName}."); - System.Console.WriteLine($"[Votekick] Auto-votekicked {pc.Data.PlayerName}"); } } } - catch (Exception ex) + catch (Exception) { - System.Console.WriteLine("VotekickAll error: " + ex.Message); } return votesSent; @@ -6604,9 +8629,8 @@ private void LeaveRoomAfterVotekick() ShowNotification("[AUTO-VOTEKICK] Left room. Auto mode is still armed."); } } - catch (Exception ex) + catch (Exception) { - System.Console.WriteLine("Auto-votekick leave error: " + ex.Message); votekickExitQueued = false; votekickExitAt = 0f; } @@ -6622,7 +8646,6 @@ public static void ExecuteVotekickTarget(PlayerControl target) VoteBanSystem.Instance.CmdAddVote(targetClientId); - System.Console.WriteLine($"Votekick added to player with ClientId: {targetClientId}"); if (DestroyableSingleton.Instance != null && DestroyableSingleton.Instance.Notifier != null) { @@ -6631,9 +8654,8 @@ public static void ExecuteVotekickTarget(PlayerControl target) ShowNotification($"[VOTEKICK] Vote sent to {target.Data.PlayerName}!"); } - catch (Exception ex) + catch (Exception) { - System.Console.WriteLine("Target Votekick error: " + ex.Message); } } @@ -6776,9 +8798,15 @@ private void DrawElysiumModMenu(int windowID) public static bool blockSabotageRPC = true; public static bool blockGameRpcInLobby = true; public static bool blockChatFloodRpc = true; + public static bool blockMeetingFloodRpc = true; + public static bool enablePasosLimit = true; + public static bool enableLocalPasosBan = true; + public static bool enableHostPasosBan = true; public static bool autoBanBrokenFriendCode = false; public static int chatRpcLimit = 1; public static float chatRpcWindow = 1f; + public static int meetingRpcLimit = 2; + public static float meetingRpcWindow = 9999f; [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleAnimation))] public static class PlayerPhysics_HandleAnimation @@ -7071,35 +9099,70 @@ private static string CompactEspValue(string value, int maxLength = 24) return value; } - private static int? GetClientPing(ClientData client) + private static string NormalizeEspToken(string value) + { + return Regex.Replace(value ?? string.Empty, "<.*?>", string.Empty) + .Replace('\r', ' ') + .Replace('\n', ' ') + .Trim(); + } + + private static string FriendEspIgnoreFilePath() + { + string folder = string.IsNullOrWhiteSpace(Plugin.ElysiumFolder) + ? System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu") + : Plugin.ElysiumFolder; + return System.IO.Path.Combine(folder, "ElysiumFriendEspIgnore.txt"); + } + + private static void LoadFriendEspIgnoreTokensIfNeeded() { try { - if (client == null) return null; - - string[] names = { "Ping", "ping", "Latency", "latency", "RoundTripTime", "roundTripTime", "Rtt", "RTT" }; - Type type = client.GetType(); + if (Time.unscaledTime < friendEspIgnoreNextLoadAt) return; + friendEspIgnoreNextLoadAt = Time.unscaledTime + 3f; - foreach (string name in names) + friendEspIgnoreTokens.Clear(); + string path = FriendEspIgnoreFilePath(); + if (!System.IO.File.Exists(path)) { - PropertyInfo prop = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (prop != null) - { - object value = prop.GetValue(client); - if (value != null) return Mathf.RoundToInt(Convert.ToSingle(value)); - } + System.IO.File.WriteAllText(path, "# One nickname, Friend Code, or PUID per line. Matching players will not show ESP info.\n"); + return; + } - FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (field != null) - { - object value = field.GetValue(client); - if (value != null) return Mathf.RoundToInt(Convert.ToSingle(value)); - } + foreach (string line in System.IO.File.ReadAllLines(path)) + { + string token = NormalizeEspToken(line); + if (string.IsNullOrWhiteSpace(token) || token.StartsWith("#")) continue; + friendEspIgnoreTokens.Add(token); } } catch { } + } + + private static bool IsEspIgnored(NetworkedPlayerInfo info) + { + if (info == null) return false; + + LoadFriendEspIgnoreTokensIfNeeded(); + if (friendEspIgnoreTokens.Count == 0) return false; + + try + { + string name = NormalizeEspToken(info.PlayerName); + if (!string.IsNullOrEmpty(name) && friendEspIgnoreTokens.Contains(name)) return true; + + string displayedFc = NormalizeEspToken(GetDisplayedFriendCode(info, string.Empty)); + if (!string.IsNullOrEmpty(displayedFc) && friendEspIgnoreTokens.Contains(displayedFc)) return true; + + string rawFc = NormalizeEspToken(info.FriendCode); + if (!string.IsNullOrEmpty(rawFc) && friendEspIgnoreTokens.Contains(rawFc)) return true; - return null; + ClientData client = AmongUsClient.Instance?.GetClientFromPlayerInfo(info); + string puid = client == null ? string.Empty : NormalizeEspToken(GetClientPuid(client)); + return !string.IsNullOrEmpty(puid) && friendEspIgnoreTokens.Contains(puid); + } + catch { return false; } } public static string BuildESPInfoLine(NetworkedPlayerInfo info) @@ -7109,7 +9172,6 @@ public static string BuildESPInfoLine(NetworkedPlayerInfo info) int level = 0; string platform = "Unknown"; bool isHost = false; - int? ping = null; try { level = (int)info.PlayerLevel + 1; } catch { } @@ -7120,7 +9182,6 @@ public static string BuildESPInfoLine(NetworkedPlayerInfo info) { platform = GetPlatform(client); isHost = AmongUsClient.Instance.GetHost() == client; - ping = GetClientPing(client); } } catch { } @@ -7132,22 +9193,15 @@ public static string BuildESPInfoLine(NetworkedPlayerInfo info) platform = $"{platform} spf"; } + string fc = CompactEspValue(GetDisplayedFriendCode(info)); List parts = new List(); if (isHost) parts.Add("Host"); parts.Add($"Lv:{level}"); parts.Add(platform); - if (ping.HasValue) parts.Add($"Ping:{ping.Value}ms"); + if (showEspFriendCode) parts.Add(fc); return string.Join(" - ", parts); } - public static string BuildESPInfoBlock(NetworkedPlayerInfo info) - { - if (info == null) return string.Empty; - - string fc = CompactEspValue(GetDisplayedFriendCode(info, "No Friend Code"), 18); - return $"{BuildESPInfoLine(info)}\nFC: {fc}"; - } - public static Color GetRoleColor(int roleId, Color fallbackColor) { switch (roleId) @@ -7174,7 +9228,7 @@ public static void HandleTracer(PlayerControl target, bool enable) LineRenderer lr = target.GetComponent(); - if (!enable || PlayerControl.LocalPlayer == null || target == PlayerControl.LocalPlayer || target.Data == null || target.Data.Disconnected) + if (!enable || PlayerControl.LocalPlayer == null || target == PlayerControl.LocalPlayer || target.Data == null || target.Data.Disconnected || IsEspIgnored(target.Data)) { if (lr != null) lr.enabled = false; return; @@ -7329,7 +9383,9 @@ public static string GetESPNameTag(NetworkedPlayerInfo info, string originalName if (showPlayerInfo) { string accentHex = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); - newName = $"{BuildESPInfoBlock(info)}\n{newName}"; + string espLine = BuildESPInfoLine(info); + if (!string.IsNullOrWhiteSpace(espLine)) + newName = $"{espLine}\n{newName}"; } if (seeKillCooldown && info.Role != null && info.PlayerId != PlayerControl.LocalPlayer?.PlayerId) { @@ -7728,7 +9784,9 @@ public static void Postfix(ChatBubble __instance) try { string accentHex = ColorUtility.ToHtmlStringRGB(ElysiumModMenuGUI.currentAccentColor); - string extra = $" {ElysiumModMenuGUI.BuildESPInfoLine(__instance.playerInfo)}"; + string espLine = ElysiumModMenuGUI.BuildESPInfoLine(__instance.playerInfo); + if (string.IsNullOrWhiteSpace(espLine)) return; + string extra = $" {espLine}"; if (!__instance.NameText.text.Contains("Lv:")) __instance.NameText.text += extra; } @@ -8517,12 +10575,7 @@ public static void Postfix() [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] public static class RPCSniffer_Patch { - private static readonly HashSet VanillaRPCs = new HashSet - { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, - 22, 23, 24, 25, 26, 27, 29, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, - 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 60, 61, 62, 63, 64, 65 - }; + private static readonly HashSet VanillaRPCs = ElysiumModMenuGUI.VanillaRpcIds; private static readonly Dictionary KnownMods = new Dictionary { @@ -8569,6 +10622,8 @@ public static bool Prefix(PlayerControl __instance, byte callId, MessageReader r if (PlayerControl.LocalPlayer != null && __instance == PlayerControl.LocalPlayer) return true; + ElysiumModMenuGUI.RecordPlayerRpc(__instance, callId); + if (ElysiumModMenuGUI.LogAllRPCs) { @@ -8751,7 +10806,7 @@ private static bool ShowGhostMessage(PlayerControl sourcePlayer, string chatText chatText = BlockedWords.CensorWords(chatText, false); } - pooledBubble.SetText($"{chatText}"); + pooledBubble.SetText($"{chatText}"); pooledBubble.AlignChildren(); chat.AlignAllBubbles(); @@ -8866,10 +10921,9 @@ public static void Prefix(PlatformSpecificData __instance) { if (__instance != null) { - if (ElysiumModMenuGUI.enablePlatformSpoof) - { - __instance.Platform = ElysiumModMenuGUI.platformValues[ElysiumModMenuGUI.currentPlatformIndex]; - } + if (!ElysiumModMenuGUI.enablePlatformSpoof) return; + + __instance.Platform = ElysiumModMenuGUI.platformValues[ElysiumModMenuGUI.currentPlatformIndex]; __instance.PlatformName = "ElysiumModMenu by Meowchelo (and one silly guy :p) https://github.com/meowchelo/ElysiumModMenu"; } } @@ -8932,7 +10986,7 @@ public static void Prefix(InnerNetClient __instance, int clientId, bool ban) try { var client = AmongUsClient.Instance.GetClientFromCharacter(pc); - if (client != null) puid = client.Id.ToString(); + if (client != null) puid = ElysiumModMenuGUI.GetClientPuid(client); } catch { } From 723a758e2a2820f7e85266dd98797469dc378df4 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 12 Jun 2026 17:51:00 +0200 Subject: [PATCH 24/39] Improve spam error log attachments --- ElysiumModMenu.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 924b672..1c83220 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -158,7 +158,7 @@ public static class DiscordStatusReporter private const long MaxDiagnosticAttachmentBytes = 7L * 1024L * 1024L; private const long MaxDiagnosticTotalAttachmentBytes = 20L * 1024L * 1024L; private const long LargeLogTailBytes = 1024L * 1024L; - private const int MaxDiagnosticExcerptLines = 5000; + private const int MaxDiagnosticExcerptLines = 20000; private const int DiagnosticExcerptContextBefore = 8; private const int DiagnosticExcerptContextAfter = 10; private static string decodedWebhookUrl; @@ -343,9 +343,10 @@ private static List PrepareLogAttachments(IEnumerable att if (bytes == null || bytes.Length == 0) continue; - string fileName = SanitizeAttachmentFileName(System.IO.Path.GetFileName(path)); - if (matchedLines > 0) fileName += ".anomaly-excerpt.txt"; - else if (truncated) fileName += ".tail.txt"; + string sourceFileName = SanitizeAttachmentFileName(System.IO.Path.GetFileName(path)); + string fileName = matchedLines > 0 + ? $"SpamErrorLog-{sourceFileName}.txt" + : sourceFileName + (truncated ? ".tail.txt" : string.Empty); attachments.Add(new LogAttachment { FileName = fileName, Bytes = bytes }); totalBytes += bytes.Length; } @@ -7653,6 +7654,7 @@ private static bool IsErrorLogLine(string line) lower.Contains("stored data") || lower.Contains("storeddata") || IsStoredMessageOverloadLine(lower) || + IsKnownSpamWarningLine(lower) || lower.Contains("overload") || lower.Contains("freeze") || lower.Contains("color=red") || @@ -7668,6 +7670,7 @@ public static bool IsRelevantAnomalyLine(string line) if (lower.Contains("registered mono type") && lower.Contains("elysiummodmenu")) return false; if (lower.Contains("method ") && lower.Contains(" has unsupported ") && lower.Contains("elysiummodmenu")) return false; return IsStoredMessageOverloadLine(lower) || + IsKnownSpamWarningLine(lower) || lower.Contains("[error") || lower.Contains("[fatal") || lower.Contains("nullreferenceexception") || @@ -7679,6 +7682,15 @@ public static bool IsRelevantAnomalyLine(string line) lower.Contains("storeddata"); } + private static bool IsKnownSpamWarningLine(string line) + { + if (string.IsNullOrWhiteSpace(line)) return false; + string lower = line.ToLowerInvariant(); + return lower.Contains("sendmode set to everything") || + lower.Contains("likely should be reliable") || + lower.Contains("stored msg"); + } + private static bool IsStoredMessageOverloadLine(string line) { if (string.IsNullOrWhiteSpace(line)) return false; From 00e0e1c9b065de6be6dc5e9f578c248fe96fb8ba Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 12 Jun 2026 17:56:21 +0200 Subject: [PATCH 25/39] Limit spam error log attachments --- ElysiumModMenu.cs | 56 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 1c83220..3a35843 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -158,6 +158,7 @@ public static class DiscordStatusReporter private const long MaxDiagnosticAttachmentBytes = 7L * 1024L * 1024L; private const long MaxDiagnosticTotalAttachmentBytes = 20L * 1024L * 1024L; private const long LargeLogTailBytes = 1024L * 1024L; + private const int MaxSpamErrorLogBytes = 300 * 1024; private const int MaxDiagnosticExcerptLines = 20000; private const int DiagnosticExcerptContextBefore = 8; private const int DiagnosticExcerptContextAfter = 10; @@ -412,17 +413,17 @@ void AddOutput(string value) if (matchedLines <= 0) return null; - StringBuilder builder = new StringBuilder(); - builder.AppendLine("Elysium anomaly log excerpt"); - builder.AppendLine($"Source: {path}"); - builder.AppendLine($"SourceBytes: {fileLength}"); - builder.AppendLine($"MatchedLines: {matchedLines}"); - builder.AppendLine($"GeneratedUtc: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}"); - builder.AppendLine(); - foreach (string line in output) - builder.AppendLine(line); + List header = new List + { + "Elysium Spam Error Log", + $"Source: {path}", + $"SourceBytes: {fileLength}", + $"MatchedLines: {matchedLines}", + $"GeneratedUtc: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}", + "" + }; - return Encoding.UTF8.GetBytes(builder.ToString()); + return BuildLimitedSpamErrorLogBytes(header, output.ToList()); } catch (Exception ex) { @@ -431,6 +432,41 @@ void AddOutput(string value) } } + private static byte[] BuildLimitedSpamErrorLogBytes(List headerLines, List bodyLines) + { + StringBuilder builder = new StringBuilder(); + foreach (string line in headerLines) + builder.AppendLine(line); + + int includedBodyLines = 0; + int baseBytes = Encoding.UTF8.GetByteCount(builder.ToString()); + int currentBytes = baseBytes; + + for (int i = 0; i < bodyLines.Count; i++) + { + string line = bodyLines[i] ?? string.Empty; + string withNewline = line + Environment.NewLine; + int lineBytes = Encoding.UTF8.GetByteCount(withNewline); + int remainingLinesAfterThis = bodyLines.Count - i - 1; + string marker = remainingLinesAfterThis > 0 ? $"... (осталось: {remainingLinesAfterThis} строк){Environment.NewLine}" : string.Empty; + int markerBytes = string.IsNullOrEmpty(marker) ? 0 : Encoding.UTF8.GetByteCount(marker); + + if (currentBytes + lineBytes + markerBytes > MaxSpamErrorLogBytes) + { + int remaining = bodyLines.Count - includedBodyLines; + if (remaining > 0) + builder.AppendLine($"... (осталось: {remaining} строк)"); + break; + } + + builder.Append(withNewline); + currentBytes += lineBytes; + includedBodyLines++; + } + + return Encoding.UTF8.GetBytes(builder.ToString()); + } + private static byte[] ReadLogAttachmentBytes(string path, long remainingBytes, out bool truncated) { truncated = false; From 5f7a34f594e9cc364ac40868e8c8f35703c2b293 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 12 Jun 2026 17:57:46 +0200 Subject: [PATCH 26/39] Set version to 1.2.7 --- ElysiumModMenu.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 3a35843..a66f2aa 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -37,7 +37,7 @@ using System.Runtime.CompilerServices; namespace ElysiumModMenu { - [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.2")] + [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.2.7")] public class Plugin : BasePlugin { public static Plugin Instance { get; private set; } = null!; @@ -4944,7 +4944,7 @@ private void DrawGeneralInfoTab() string contributorHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(109, 138, 255, 255)) : new Color32(109, 138, 255, 255)); string dangerHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(231, 76, 60, 255)) : new Color32(231, 76, 60, 255)); string safeHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(57, 255, 20, 255)) : new Color32(57, 255, 20, 255)); - string versionText = "1.3.2"; + string versionText = "1.2.7"; GUIStyle textStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 12 }; textStyle.normal.textColor = whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : new Color(0.85f, 0.85f, 0.85f, 1f); @@ -9542,7 +9542,7 @@ public static void Postfix(PingTracker __instance) if (ElysiumModMenuGUI.showWatermark) { - string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.2"); + string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.2.7"); finalString = $"{shimmerTitle} • " + finalString; } From 14b6f2543f754977cbffa7218d08c23f01482e28 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 12 Jun 2026 21:36:09 +0200 Subject: [PATCH 27/39] Document ghost chat color support Separate PR topic: configurable ghost chat colors and ghost chat rendering. From 184b51709fa87fe3ccf2899540000cd4478c3407 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 12 Jun 2026 21:36:10 +0200 Subject: [PATCH 28/39] Document ghost role controls Separate PR topic: target and set-all Ghost/Ghost Imp controls. From 846e90f83032e8c8c6dab3716d741901e3e53795 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 12 Jun 2026 21:36:10 +0200 Subject: [PATCH 29/39] Document force role assignment logic Separate PR topic: force role flow using role RPC/RoleManager paths with ghost fallback. From 65e3cdbde92952f04281adcaba104c2420742d56 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 12 Jun 2026 21:36:10 +0200 Subject: [PATCH 30/39] Document local and host-only block split Separate PR topic: local-only visual/keybind behavior and host-only guarded actions. From 97f71797650585b7bad774313109cabc78f1d4d0 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Fri, 12 Jun 2026 21:36:10 +0200 Subject: [PATCH 31/39] Document player log diagnostics Separate PR topic: Player.log/Player-prev.log monitoring and SpamErrorLog attachments. From dfbc2736809537ea92fdaff54182c95f854a3725 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Sat, 13 Jun 2026 00:09:30 +0200 Subject: [PATCH 32/39] Set version 1.3.4 and clear spam error logs --- ElysiumModMenu.cs | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index a66f2aa..28cd4aa 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -37,7 +37,7 @@ using System.Runtime.CompilerServices; namespace ElysiumModMenu { - [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.2.7")] + [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.4")] public class Plugin : BasePlugin { public static Plugin Instance { get; private set; } = null!; @@ -4944,7 +4944,7 @@ private void DrawGeneralInfoTab() string contributorHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(109, 138, 255, 255)) : new Color32(109, 138, 255, 255)); string dangerHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(231, 76, 60, 255)) : new Color32(231, 76, 60, 255)); string safeHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(57, 255, 20, 255)) : new Color32(57, 255, 20, 255)); - string versionText = "1.2.7"; + string versionText = "1.3.4"; GUIStyle textStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 12 }; textStyle.normal.textColor = whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : new Color(0.85f, 0.85f, 0.85f, 1f); @@ -7370,6 +7370,7 @@ public void Start() UnlockCosmetics(); LoadConfig(); LoadBanList(); + ClearSpamErrorLogOnStartup(); StartBackgroundAnomalyLogMonitor(); @@ -7406,6 +7407,37 @@ public void OnDisable() SaveConfig(); } + private static void ClearSpamErrorLogOnStartup() + { + try + { + watchedLogLineCounts.Clear(); + logBurstWindowStartedAt = -1f; + logBurstCooldownUntil = 0f; + logBurstLineCount = 0; + anomalyLogWatchNotified = false; + logMonitorNextScanAt = 0f; + + string root = string.IsNullOrWhiteSpace(Plugin.ElysiumFolder) + ? System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu") + : Plugin.ElysiumFolder; + + if (!System.IO.Directory.Exists(root)) return; + + foreach (string file in System.IO.Directory.GetFiles(root, "SpamErrorLog*.txt", System.IO.SearchOption.AllDirectories)) + { + try { System.IO.File.Delete(file); } + catch { } + } + + System.Console.WriteLine("[ElysiumModMenu] Cleared previous SpamErrorLog files and reset log monitor state."); + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Failed to clear SpamErrorLog files: {ex.GetType().Name}: {ex.Message}"); + } + } + public static void SendAnomalyAlert(string title, string message, string dedupeKey = null, bool waitForCompletion = false, IEnumerable attachmentPaths = null) { if (!enableAnomalyLogReports) return; @@ -9542,7 +9574,7 @@ public static void Postfix(PingTracker __instance) if (ElysiumModMenuGUI.showWatermark) { - string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.2.7"); + string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.4"); finalString = $"{shimmerTitle} • " + finalString; } From 90b8e754a8f09eb310df93dae0998c1d0760ebdb Mon Sep 17 00:00:00 2001 From: meowchelo Date: Sat, 13 Jun 2026 00:14:29 +0200 Subject: [PATCH 33/39] Persist keybind and menu preferences --- ElysiumModMenu.cs | 102 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 6 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index 28cd4aa..a58fa19 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -4044,6 +4044,16 @@ private static bool LoadBool(string key, bool defaultValue) return PlayerPrefs.HasKey(key) ? PlayerPrefs.GetInt(key) == 1 : defaultValue; } + private static int LoadInt(string key, int defaultValue) + { + return PlayerPrefs.HasKey(key) ? PlayerPrefs.GetInt(key) : defaultValue; + } + + private static float LoadFloat(string key, float defaultValue) + { + return PlayerPrefs.HasKey(key) ? PlayerPrefs.GetFloat(key) : defaultValue; + } + private void SaveConfig() { try @@ -4069,6 +4079,23 @@ private void SaveConfig() Plugin.MenuKeybind.Value = menuToggleKey; PlayerPrefs.SetInt("M_MenuToggleKey", (int)menuToggleKey); SaveBool("M_WhiteTheme", whiteMenuTheme); + SaveBool("M_EnableBackground", enableBackground); + SaveBool("M_EnableCustomNotifs", EnableCustomNotifs); + SaveBool("M_LogAllRPCs", LogAllRPCs); + PlayerPrefs.SetInt("M_SelectedSpoofMenuIndex", selectedSpoofMenuIndex); + PlayerPrefs.SetFloat("M_MenuWindowX", windowRect.x); + PlayerPrefs.SetFloat("M_MenuWindowY", windowRect.y); + PlayerPrefs.SetFloat("M_MenuWindowW", windowRect.width); + PlayerPrefs.SetFloat("M_MenuWindowH", windowRect.height); + PlayerPrefs.SetInt("M_CurrentTab", currentTab); + PlayerPrefs.SetInt("M_TargetTab", targetTabIndex); + PlayerPrefs.SetInt("M_CurrentGeneralSubTab", currentGeneralSubTab); + PlayerPrefs.SetInt("M_CurrentGeneralInfoSubTab", currentGeneralInfoSubTab); + PlayerPrefs.SetInt("M_CurrentSelfSubTab", currentSelfSubTab); + PlayerPrefs.SetInt("M_CurrentVisualsSubTab", currentVisualsSubTab); + PlayerPrefs.SetInt("M_CurrentPlayersSubTab", currentPlayersSubTab); + PlayerPrefs.SetInt("M_CurrentHostOnlySubTab", currentHostOnlySubTab); + PlayerPrefs.SetInt("M_CurrentAutoHostSubTab", currentAutoHostSubTab); PlayerPrefs.SetInt("M_BndMMorph", (int)bindMassMorph); PlayerPrefs.SetInt("M_BndSpawn", (int)bindSpawnLobby); PlayerPrefs.SetInt("M_BndDespawn", (int)bindDespawnLobby); @@ -4332,11 +4359,63 @@ private void LoadConfig() if (PlayerPrefs.HasKey("M_AutoHostFastStartDelaySeconds")) AutoHostFastStartDelaySeconds = PlayerPrefs.GetFloat("M_AutoHostFastStartDelaySeconds"); if (PlayerPrefs.HasKey("M_WalkSpeed")) walkSpeed = PlayerPrefs.GetFloat("M_WalkSpeed"); if (PlayerPrefs.HasKey("M_EngineSpeed")) engineSpeed = PlayerPrefs.GetFloat("M_EngineSpeed"); - keyBinds["Toggle Menu"] = menuToggleKey; + enableBackground = LoadBool("M_EnableBackground", enableBackground); + EnableCustomNotifs = LoadBool("M_EnableCustomNotifs", EnableCustomNotifs); + LogAllRPCs = LoadBool("M_LogAllRPCs", LogAllRPCs); + selectedSpoofMenuIndex = Mathf.Clamp(LoadInt("M_SelectedSpoofMenuIndex", selectedSpoofMenuIndex), 0, spoofMenuNames.Length - 1); + windowRect = new Rect( + LoadFloat("M_MenuWindowX", windowRect.x), + LoadFloat("M_MenuWindowY", windowRect.y), + Mathf.Clamp(LoadFloat("M_MenuWindowW", windowRect.width), 640f, 1400f), + Mathf.Clamp(LoadFloat("M_MenuWindowH", windowRect.height), 420f, 900f)); + currentTab = Mathf.Clamp(LoadInt("M_CurrentTab", currentTab), 0, tabNames.Length - 1); + targetTabIndex = Mathf.Clamp(LoadInt("M_TargetTab", currentTab), 0, tabNames.Length - 1); + currentGeneralSubTab = Mathf.Clamp(LoadInt("M_CurrentGeneralSubTab", currentGeneralSubTab), 0, generalSubTabs.Length - 1); + currentGeneralInfoSubTab = Mathf.Clamp(LoadInt("M_CurrentGeneralInfoSubTab", currentGeneralInfoSubTab), 0, generalInfoSubTabs.Length - 1); + currentSelfSubTab = Mathf.Clamp(LoadInt("M_CurrentSelfSubTab", currentSelfSubTab), 0, selfSubTabs.Length); + currentVisualsSubTab = Mathf.Clamp(LoadInt("M_CurrentVisualsSubTab", currentVisualsSubTab), 0, visualsSubTabs.Length - 1); + currentPlayersSubTab = Mathf.Clamp(LoadInt("M_CurrentPlayersSubTab", currentPlayersSubTab), 0, playersSubTabs.Length - 1); + currentHostOnlySubTab = Mathf.Clamp(LoadInt("M_CurrentHostOnlySubTab", currentHostOnlySubTab), 0, hostOnlySubTabs.Length - 1); + currentAutoHostSubTab = Mathf.Clamp(LoadInt("M_CurrentAutoHostSubTab", currentAutoHostSubTab), 0, autoHostSubTabs.Length - 1); + tabTransitionProgress = 1f; + SyncKeybindDictionary(); if (PlayerPrefs.HasKey("M_SpoofName")) customNameInput = PlayerPrefs.GetString("M_SpoofName"); } catch { } } + + private static void SyncKeybindDictionary() + { + try + { + keyBinds["Toggle Menu"] = menuToggleKey; + keyBinds["Magnet Cursor"] = bindMagnetCursor; + keyBinds["Mass Morph"] = bindMassMorph; + keyBinds["Spawn Lobby"] = bindSpawnLobby; + keyBinds["Despawn Lobby"] = bindDespawnLobby; + keyBinds["Close Meeting"] = bindCloseMeeting; + keyBinds["Insta Start"] = bindInstaStart; + keyBinds["End Crew"] = bindEndCrew; + keyBinds["End Imp"] = bindEndImp; + keyBinds["End Imp DC"] = bindEndImpDC; + keyBinds["End H&S DC"] = bindEndHnsDC; + keyBinds["Toggle Tracers"] = bindToggleTracers; + keyBinds["Toggle NoClip"] = bindToggleNoClip; + keyBinds["Toggle Freecam"] = bindToggleFreecam; + keyBinds["Toggle Camera Zoom"] = bindToggleCameraZoom; + keyBinds["Toggle Player Info"] = bindTogglePlayerInfo; + keyBinds["Toggle See Roles"] = bindToggleSeeRoles; + keyBinds["Toggle See Ghosts"] = bindToggleSeeGhosts; + keyBinds["Toggle Full Bright"] = bindToggleFullBright; + keyBinds["Kill All"] = bindKillAll; + keyBinds["Call Meeting"] = bindCallMeeting; + keyBinds["Kick All"] = bindKickAll; + keyBinds["Fix Sabotages"] = bindFixSabotages; + keyBinds["All Ghost"] = bindSetAllGhost; + keyBinds["All Ghost Imp"] = bindSetAllGhostImp; + } + catch { } + } private Texture2D MakeRoundedTex(int size, Color col, float radius) { Texture2D result = new Texture2D(size, size, TextureFormat.RGBA32, false); @@ -7195,10 +7274,12 @@ private void DrawMenuTab() GUILayout.BeginVertical(boxStyle); GUILayout.Label("MENU CUSTOMIZATION", headerStyle); GUILayout.Space(5); + bool menuPrefsChanged = false; bool prevRgb = rgbMenuMode; rgbMenuMode = DrawToggle(rgbMenuMode, "RGB Menu Mode"); if (prevRgb && !rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); + if (prevRgb != rgbMenuMode) menuPrefsChanged = true; GUILayout.Space(5); @@ -7208,7 +7289,7 @@ private void DrawMenuTab() { InitStyles(); UpdateAccentColor(currentAccentColor); - SaveConfig(); + menuPrefsChanged = true; } GUILayout.Space(5); @@ -7216,6 +7297,7 @@ private void DrawMenuTab() bool prevBg = enableBackground; enableBackground = DrawToggle(enableBackground, "Enable Image Background"); if (enableBackground && !prevBg) LoadBackgroundImage(); + if (prevBg != enableBackground) menuPrefsChanged = true; GUILayout.Space(5); GUILayout.Label("Put 'MenuBG.png' or .jpg in BepInEx/config to add a background image.", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11 }); @@ -7225,9 +7307,9 @@ private void DrawMenuTab() GUILayout.BeginHorizontal(); GUIStyle middleColorStyle = new GUIStyle(btnStyle) { normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, fontStyle = FontStyle.Bold }; GUI.enabled = !rgbMenuMode; - if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex--; if (currentMenuColorIndex < 0) currentMenuColorIndex = menuColors.Length - 1; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); } + if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex--; if (currentMenuColorIndex < 0) currentMenuColorIndex = menuColors.Length - 1; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); menuPrefsChanged = true; } GUILayout.Label(menuColorNames[currentMenuColorIndex], middleColorStyle, GUILayout.Width(110), GUILayout.Height(25)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex++; if (currentMenuColorIndex >= menuColors.Length) currentMenuColorIndex = 0; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); } + if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex++; if (currentMenuColorIndex >= menuColors.Length) currentMenuColorIndex = 0; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); menuPrefsChanged = true; } GUI.enabled = true; GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); @@ -7237,13 +7319,15 @@ private void DrawMenuTab() GUILayout.BeginVertical(boxStyle); GUILayout.Label("SPOOF MENU IDENTITY", headerStyle); + bool prevSpoofMenuEnabled = SpoofMenuEnabled; SpoofMenuEnabled = DrawToggle(SpoofMenuEnabled, "Enable Fake RPC"); + if (prevSpoofMenuEnabled != SpoofMenuEnabled) menuPrefsChanged = true; GUILayout.Space(5); GUILayout.BeginHorizontal(); GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) } }; - if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex--; if (selectedSpoofMenuIndex < 0) selectedSpoofMenuIndex = spoofMenuNames.Length - 1; } + if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex--; if (selectedSpoofMenuIndex < 0) selectedSpoofMenuIndex = spoofMenuNames.Length - 1; menuPrefsChanged = true; } GUILayout.Label($"{spoofMenuNames[selectedSpoofMenuIndex]}", middleLabelStyle, GUILayout.Width(110), GUILayout.Height(25)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex++; if (selectedSpoofMenuIndex >= spoofMenuNames.Length) selectedSpoofMenuIndex = 0; } + if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex++; if (selectedSpoofMenuIndex >= spoofMenuNames.Length) selectedSpoofMenuIndex = 0; menuPrefsChanged = true; } GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); GUILayout.EndVertical(); @@ -7253,10 +7337,16 @@ private void DrawMenuTab() GUILayout.BeginVertical(boxStyle); GUILayout.Label("NOTIFICATIONS & LOGGING", headerStyle); GUILayout.Space(5); + bool prevCustomNotifs = EnableCustomNotifs; EnableCustomNotifs = DrawToggle(EnableCustomNotifs, "Enable Custom UI Notifications", 250); + if (prevCustomNotifs != EnableCustomNotifs) menuPrefsChanged = true; GUILayout.Space(5); + bool prevLogAllRpcs = LogAllRPCs; LogAllRPCs = DrawToggle(LogAllRPCs, "Sniff All RPCs (On-Screen)", 250); + if (prevLogAllRpcs != LogAllRPCs) menuPrefsChanged = true; GUILayout.EndVertical(); + + if (menuPrefsChanged) SaveConfig(); } private Vector2 outfitsScrollPos = Vector2.zero; public static bool AutoHostEnabled = false; From d636f27e014fe0c341900c78230c12519dfb4e11 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Sat, 13 Jun 2026 00:17:53 +0200 Subject: [PATCH 34/39] Release 1.3.4 From 042b9323b09b8a6a6a415b3735952581c5a3c139 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Sat, 13 Jun 2026 00:22:01 +0200 Subject: [PATCH 35/39] Update project metadata for release 1.3.4 --- ElysiumModMenu.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ElysiumModMenu.csproj b/ElysiumModMenu.csproj index 4ee0676..cddb71c 100644 --- a/ElysiumModMenu.csproj +++ b/ElysiumModMenu.csproj @@ -7,9 +7,9 @@ latest ElysiumModMenu ElysiumModMenu - 1.3.2 - 1.3.2.0 - 1.3.2.0 + 1.3.4 + 1.3.4.0 + 1.3.4.0 C:\Program Files (x86)\Steam\steamapps\common\Among Us From bda1257cd57acaa128ed3d8680210dcf4bdac64a Mon Sep 17 00:00:00 2001 From: meowchelo Date: Sat, 13 Jun 2026 22:33:17 +0200 Subject: [PATCH 36/39] Update anti-cheat protections --- ElysiumModMenu.cs | 1000 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 873 insertions(+), 127 deletions(-) diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index a58fa19..aefeb85 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -77,6 +77,12 @@ public override void Load() System.IO.File.Create(banFile).Dispose(); } + string platformBanFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumPlatformBanList.txt"); + if (!System.IO.File.Exists(platformBanFile)) + { + System.IO.File.WriteAllText(platformBanFile, "# One custom platform token per line. Matching PlatformName values are host-banned when enabled.\n# Example: github\n"); + } + string friendEspFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumFriendEspIgnore.txt"); if (!System.IO.File.Exists(friendEspFile)) { @@ -524,6 +530,66 @@ public class ElysiumModMenuGUI : MonoBehaviour public static string[] spoofMenuNames = { "ElysiumModMenu", "HostGuard/TOH", "Polar", "BanMod", "Better Among Us", "Sicko Menu", "GNC", "KillNetwork (V1)", "KillNetwork (V2)", "KNM" }; public static byte[] spoofMenuRPCs = { 89, 176, 204, 212, 151, 164, 154, 85, 150, 162 }; public static float rpcSpoofDelay = 4f; + public static readonly string[] menuLanguageNames = { "Auto", "English", "Русский", "Deutsch", "Français", "Español", "Italiano", "Português", "Polski", "Nederlands", "Türkçe", "Čeština", "Română", "Magyar", "Svenska", "Dansk", "Suomi", "Norsk", "Українська", "Ελληνικά", "中文", "日本語", "한국어" }; + public static readonly string[] menuLanguageCodes = { "auto", "en", "ru", "de", "fr", "es", "it", "pt", "pl", "nl", "tr", "cs", "ro", "hu", "sv", "da", "fi", "no", "uk", "el", "zh", "ja", "ko" }; + public static int currentMenuLanguageIndex = 0; + private static readonly Dictionary> menuTranslations = new Dictionary> + { + ["de"] = new Dictionary { ["GENERAL"]="ALLGEMEIN",["SELF"]="SPIELER",["VISUALS"]="VISUELL",["PLAYERS"]="SPIELER",["SABOTAGES"]="SABOTAGEN",["HOST ONLY"]="NUR HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="ABSTIMMUNG",["MENU"]="MENÜ",["MAPS"]="KARTEN",["ANIMATIONS"]="ANIMATIONEN",["INFORMATION"]="INFORMATION",["KEYBINDS"]="TASTEN",["WELCOME"]="WILLKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menüsprache:",["FPS Limit"]="FPS-Limit",["Chat History"]="Chat-Verlauf",["History:"]="Verlauf:",["History size:"]="Verlaufgröße:",["CHAT UTILITY"]="CHAT-TOOLS",["Always Show Chat"]="Chat immer anzeigen",["Read Ghost Chat"]="Geisterchat lesen",["Extended Chat"]="Erweiterter Chat",["Fast Chat"]="Schneller Chat",["Unlock Extra Characters"]="Alle Zeichen erlauben",["Spell Check"]="Rechtschreibprüfung",["Clipboard"]="Zwischenablage",["Save Chat Log"]="Chatlog speichern",["Dark Chat Theme"]="Dunkles Chat-Thema",["Enable /color"]="Aktiviere /color",["Block Fortegreen"]="Fortegreen blockieren",["Allow Duplicate Colors"]="Doppelte Farben erlauben",["Auto Ghost After Start"]="Auto-Geist nach Start",["FAVORITE OUTFITS"]="FAVORITEN-OUTFITS",["Slot"]="Slot",["Empty"]="Leer",["Apply"]="Anwenden",["Save Mine"]="Meins speichern",["Save Selected"]="Auswahl speichern",["Saved slot"]="Slot gespeichert",["Applied slot"]="Slot angewendet",["Cleared slot"]="Slot gelöscht",["Auto-Ban Platform Spoof (Host)"]="Auto-Ban Plattform-Spoof (Host)",["Ban Custom Platforms From TXT"]="Custom-Plattformen aus TXT bannen",["RPC Anti-Cheat"]="RPC-Anti-Cheat",["RPC limit:"]="RPC-Limit:",["RPC Local Drop"]="RPC lokal droppen",["RPC Host Ban"]="RPC Host-Ban" }, + ["fr"] = new Dictionary { ["GENERAL"]="GÉNÉRAL",["SELF"]="JOUEUR",["VISUALS"]="VISUELS",["PLAYERS"]="JOUEURS",["SABOTAGES"]="SABOTAGES",["HOST ONLY"]="HÔTE",["OUTFITS"]="TENUES",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="CARTES",["ANIMATIONS"]="ANIMATIONS",["INFORMATION"]="INFORMATION",["KEYBINDS"]="TOUCHES",["WELCOME"]="ACCUEIL",["CREDITS"]="CRÉDITS",["Menu language:"]="Langue du menu :",["FPS Limit"]="Limite FPS",["Chat History"]="Historique du chat",["History:"]="Historique :",["History size:"]="Taille historique :",["CHAT UTILITY"]="OUTILS CHAT",["Always Show Chat"]="Toujours afficher le chat",["Read Ghost Chat"]="Lire le chat fantôme",["Extended Chat"]="Chat étendu",["Fast Chat"]="Chat rapide",["Unlock Extra Characters"]="Autoriser tous les caractères",["Spell Check"]="Correction",["Clipboard"]="Presse-papiers",["Save Chat Log"]="Sauver le log chat",["Dark Chat Theme"]="Thème chat sombre",["Enable /color"]="Activer /color",["Block Fortegreen"]="Bloquer Fortegreen",["Allow Duplicate Colors"]="Autoriser les couleurs doubles",["Auto Ghost After Start"]="Fantôme auto après départ",["FAVORITE OUTFITS"]="TENUES FAVORITES",["Slot"]="Empl.",["Empty"]="Vide",["Apply"]="Appliquer",["Save Mine"]="Sauver mien",["Save Selected"]="Sauver sélection",["Saved slot"]="Emplacement sauvé",["Applied slot"]="Emplacement appliqué",["Cleared slot"]="Emplacement vidé",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof plateforme (Hôte)",["Ban Custom Platforms From TXT"]="Ban plateformes custom TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limite RPC :",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC hôte" }, + ["es"] = new Dictionary { ["GENERAL"]="GENERAL",["SELF"]="JUGADOR",["VISUALS"]="VISUALES",["PLAYERS"]="JUGADORES",["SABOTAGES"]="SABOTAJES",["HOST ONLY"]="HOST",["OUTFITS"]="ATUENDOS",["VOTEKICK"]="VOTOKICK",["MENU"]="MENÚ",["MAPS"]="MAPAS",["ANIMATIONS"]="ANIMACIONES",["INFORMATION"]="INFORMACIÓN",["KEYBINDS"]="TECLAS",["WELCOME"]="BIENVENIDA",["CREDITS"]="CRÉDITOS",["Menu language:"]="Idioma del menú:",["FPS Limit"]="Límite FPS",["Chat History"]="Historial de chat",["History:"]="Historial:",["History size:"]="Tamaño historial:",["CHAT UTILITY"]="UTILIDAD CHAT",["Always Show Chat"]="Mostrar chat siempre",["Read Ghost Chat"]="Leer chat fantasma",["Extended Chat"]="Chat extendido",["Fast Chat"]="Chat rápido",["Unlock Extra Characters"]="Permitir caracteres extra",["Spell Check"]="Ortografía",["Clipboard"]="Portapapeles",["Save Chat Log"]="Guardar log chat",["Dark Chat Theme"]="Tema chat oscuro",["Enable /color"]="Activar /color",["Block Fortegreen"]="Bloquear Fortegreen",["Allow Duplicate Colors"]="Permitir colores duplicados",["Auto Ghost After Start"]="Fantasma auto al iniciar",["FAVORITE OUTFITS"]="ATUENDOS FAVORITOS",["Slot"]="Ranura",["Empty"]="Vacío",["Apply"]="Aplicar",["Save Mine"]="Guardar mío",["Save Selected"]="Guardar selección",["Saved slot"]="Ranura guardada",["Applied slot"]="Ranura aplicada",["Cleared slot"]="Ranura borrada",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof plataforma (Host)",["Ban Custom Platforms From TXT"]="Ban plataformas TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Límite RPC:",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC host" }, + ["it"] = new Dictionary { ["GENERAL"]="GENERALE",["SELF"]="GIOCATORE",["VISUALS"]="VISIVI",["PLAYERS"]="GIOCATORI",["SABOTAGES"]="SABOTAGGI",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFIT",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPPE",["ANIMATIONS"]="ANIMAZIONI",["INFORMATION"]="INFO",["KEYBINDS"]="TASTI",["WELCOME"]="BENVENUTO",["CREDITS"]="CREDITI",["Menu language:"]="Lingua menu:",["FPS Limit"]="Limite FPS",["Chat History"]="Cronologia chat",["History:"]="Cronologia:",["History size:"]="Dim. cronologia:",["CHAT UTILITY"]="UTILITÀ CHAT",["Always Show Chat"]="Mostra sempre chat",["Read Ghost Chat"]="Leggi chat fantasmi",["Extended Chat"]="Chat estesa",["Fast Chat"]="Chat veloce",["Unlock Extra Characters"]="Sblocca caratteri extra",["Spell Check"]="Correttore",["Clipboard"]="Appunti",["Save Chat Log"]="Salva log chat",["Dark Chat Theme"]="Tema chat scuro",["Enable /color"]="Abilita /color",["Block Fortegreen"]="Blocca Fortegreen",["Allow Duplicate Colors"]="Consenti colori doppi",["Auto Ghost After Start"]="Fantasma auto dopo start",["FAVORITE OUTFITS"]="OUTFIT PREFERITI",["Slot"]="Slot",["Empty"]="Vuoto",["Apply"]="Applica",["Save Mine"]="Salva mio",["Save Selected"]="Salva selez.",["Saved slot"]="Slot salvato",["Applied slot"]="Slot applicato",["Cleared slot"]="Slot pulito",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof piattaforma",["Ban Custom Platforms From TXT"]="Ban piattaforme custom TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limite RPC:",["RPC Local Drop"]="Drop RPC locale",["RPC Host Ban"]="Ban RPC host" }, + ["pt"] = new Dictionary { ["GENERAL"]="GERAL",["SELF"]="JOGADOR",["VISUALS"]="VISUAIS",["PLAYERS"]="JOGADORES",["SABOTAGES"]="SABOTAGENS",["HOST ONLY"]="HOST",["OUTFITS"]="VISUAIS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPAS",["ANIMATIONS"]="ANIMAÇÕES",["INFORMATION"]="INFORMAÇÃO",["KEYBINDS"]="TECLAS",["WELCOME"]="BOAS-VINDAS",["CREDITS"]="CRÉDITOS",["Menu language:"]="Idioma do menu:",["FPS Limit"]="Limite FPS",["Chat History"]="Histórico do chat",["History:"]="Histórico:",["History size:"]="Tamanho histórico:",["CHAT UTILITY"]="UTILIDADE CHAT",["Always Show Chat"]="Sempre mostrar chat",["Read Ghost Chat"]="Ler chat fantasma",["Extended Chat"]="Chat estendido",["Fast Chat"]="Chat rápido",["Unlock Extra Characters"]="Liberar caracteres extra",["Spell Check"]="Ortografia",["Clipboard"]="Área de transferência",["Save Chat Log"]="Salvar log chat",["Dark Chat Theme"]="Tema chat escuro",["Enable /color"]="Ativar /color",["Block Fortegreen"]="Bloquear Fortegreen",["Allow Duplicate Colors"]="Permitir cores duplicadas",["Auto Ghost After Start"]="Fantasma auto após iniciar",["FAVORITE OUTFITS"]="VISUAIS FAVORITOS",["Slot"]="Slot",["Empty"]="Vazio",["Apply"]="Aplicar",["Save Mine"]="Salvar meu",["Save Selected"]="Salvar seleção",["Saved slot"]="Slot salvo",["Applied slot"]="Slot aplicado",["Cleared slot"]="Slot limpo",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof plataforma",["Ban Custom Platforms From TXT"]="Ban plataformas TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limite RPC:",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC host" }, + ["pl"] = new Dictionary { ["GENERAL"]="OGÓLNE",["SELF"]="GRACZ",["VISUALS"]="WIZUALNE",["PLAYERS"]="GRACZE",["SABOTAGES"]="SABOTAŻE",["HOST ONLY"]="HOST",["OUTFITS"]="STROJE",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPY",["ANIMATIONS"]="ANIMACJE",["INFORMATION"]="INFORMACJE",["KEYBINDS"]="KLAWISZE",["WELCOME"]="WITAJ",["CREDITS"]="AUTORZY",["Menu language:"]="Język menu:",["FPS Limit"]="Limit FPS",["Chat History"]="Historia czatu",["History:"]="Historia:",["History size:"]="Rozmiar historii:",["CHAT UTILITY"]="NARZĘDZIA CZATU",["Always Show Chat"]="Zawsze pokazuj czat",["Read Ghost Chat"]="Czytaj czat duchów",["Extended Chat"]="Rozszerzony czat",["Fast Chat"]="Szybki czat",["Unlock Extra Characters"]="Odblokuj znaki",["Spell Check"]="Pisownia",["Clipboard"]="Schowek",["Save Chat Log"]="Zapisz log czatu",["Dark Chat Theme"]="Ciemny czat",["Enable /color"]="Włącz /color",["Block Fortegreen"]="Blokuj Fortegreen",["Allow Duplicate Colors"]="Zezwól na duplikaty kolorów",["Auto Ghost After Start"]="Auto duch po starcie",["FAVORITE OUTFITS"]="ULUBIONE STROJE",["Slot"]="Slot",["Empty"]="Pusty",["Apply"]="Zastosuj",["Save Mine"]="Zapisz mój",["Save Selected"]="Zapisz wybrany",["Saved slot"]="Slot zapisany",["Applied slot"]="Slot użyty",["Cleared slot"]="Slot wyczyszczony",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof platformy",["Ban Custom Platforms From TXT"]="Ban platform z TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limit RPC:",["RPC Local Drop"]="Lokalny drop RPC",["RPC Host Ban"]="Ban RPC hosta" }, + ["nl"] = new Dictionary { ["GENERAL"]="ALGEMEEN",["SELF"]="SPELER",["VISUALS"]="VISUEEL",["PLAYERS"]="SPELERS",["SABOTAGES"]="SABOTAGES",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="KAARTEN",["ANIMATIONS"]="ANIMATIES",["INFORMATION"]="INFORMATIE",["KEYBINDS"]="TOETSEN",["WELCOME"]="WELKOM",["CREDITS"]="CREDITS",["Menu language:"]="Menutaal:",["FPS Limit"]="FPS-limiet",["Chat History"]="Chatgeschiedenis",["History:"]="Geschiedenis:",["History size:"]="Geschiedenisgrootte:",["CHAT UTILITY"]="CHAT-HULP",["Always Show Chat"]="Chat altijd tonen",["Read Ghost Chat"]="Geestenchat lezen",["Extended Chat"]="Uitgebreide chat",["Fast Chat"]="Snelle chat",["Unlock Extra Characters"]="Extra tekens toestaan",["Spell Check"]="Spelling",["Clipboard"]="Klembord",["Save Chat Log"]="Chatlog opslaan",["Dark Chat Theme"]="Donker chatthema",["Enable /color"]="/color inschakelen",["Block Fortegreen"]="Fortegreen blokkeren",["Allow Duplicate Colors"]="Dubbele kleuren toestaan",["Auto Ghost After Start"]="Auto-geest na start",["FAVORITE OUTFITS"]="FAVORIETE OUTFITS",["Slot"]="Slot",["Empty"]="Leeg",["Apply"]="Toepassen",["Save Mine"]="Mijn opslaan",["Save Selected"]="Selectie opslaan",["Saved slot"]="Slot opgeslagen",["Applied slot"]="Slot toegepast",["Cleared slot"]="Slot gewist",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform-spoof",["Ban Custom Platforms From TXT"]="Ban custom platforms uit TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-limiet:",["RPC Local Drop"]="RPC lokale drop",["RPC Host Ban"]="RPC host-ban" }, + ["tr"] = new Dictionary { ["GENERAL"]="GENEL",["SELF"]="OYUNCU",["VISUALS"]="GÖRSEL",["PLAYERS"]="OYUNCULAR",["SABOTAGES"]="SABOTAJLAR",["HOST ONLY"]="HOST",["OUTFITS"]="KIYAFETLER",["VOTEKICK"]="VOTEKICK",["MENU"]="MENÜ",["MAPS"]="HARİTALAR",["ANIMATIONS"]="ANİMASYONLAR",["INFORMATION"]="BİLGİ",["KEYBINDS"]="TUŞLAR",["WELCOME"]="HOŞ GELDİN",["CREDITS"]="KREDİLER",["Menu language:"]="Menü dili:",["FPS Limit"]="FPS sınırı",["Chat History"]="Sohbet geçmişi",["History:"]="Geçmiş:",["History size:"]="Geçmiş boyutu:",["CHAT UTILITY"]="SOHBET ARAÇLARI",["Always Show Chat"]="Sohbeti hep göster",["Read Ghost Chat"]="Hayalet sohbetini oku",["Extended Chat"]="Geniş sohbet",["Fast Chat"]="Hızlı sohbet",["Unlock Extra Characters"]="Ek karakterleri aç",["Spell Check"]="Yazım denetimi",["Clipboard"]="Pano",["Save Chat Log"]="Sohbet kaydını sakla",["Dark Chat Theme"]="Koyu sohbet teması",["Enable /color"]="/color aç",["Block Fortegreen"]="Fortegreen engelle",["Allow Duplicate Colors"]="Aynı renklere izin ver",["Auto Ghost After Start"]="Başlangıçtan sonra oto hayalet",["FAVORITE OUTFITS"]="FAVORİ KIYAFETLER",["Slot"]="Slot",["Empty"]="Boş",["Apply"]="Uygula",["Save Mine"]="Benimkini kaydet",["Save Selected"]="Seçileni kaydet",["Saved slot"]="Slot kaydedildi",["Applied slot"]="Slot uygulandı",["Cleared slot"]="Slot temizlendi",["Auto-Ban Platform Spoof (Host)"]="Platform spoof oto-ban",["Ban Custom Platforms From TXT"]="TXT özel platform ban",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC sınırı:",["RPC Local Drop"]="RPC yerel drop",["RPC Host Ban"]="RPC host ban" }, + ["cs"] = new Dictionary { ["GENERAL"]="OBECNÉ",["SELF"]="HRÁČ",["VISUALS"]="VIZUÁLY",["PLAYERS"]="HRÁČI",["SABOTAGES"]="SABOTÁŽE",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITY",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPY",["ANIMATIONS"]="ANIMACE",["INFORMATION"]="INFORMACE",["KEYBINDS"]="KLÁVESY",["WELCOME"]="VÍTEJ",["CREDITS"]="AUTOŘI",["Menu language:"]="Jazyk menu:",["FPS Limit"]="Limit FPS",["Chat History"]="Historie chatu",["History:"]="Historie:",["History size:"]="Velikost historie:",["CHAT UTILITY"]="NÁSTROJE CHATU",["Always Show Chat"]="Vždy zobrazit chat",["Read Ghost Chat"]="Číst chat duchů",["Extended Chat"]="Rozšířený chat",["Fast Chat"]="Rychlý chat",["Unlock Extra Characters"]="Povolit další znaky",["Spell Check"]="Kontrola pravopisu",["Clipboard"]="Schránka",["Save Chat Log"]="Uložit log chatu",["Dark Chat Theme"]="Tmavý chat",["Enable /color"]="Zapnout /color",["Block Fortegreen"]="Blokovat Fortegreen",["Allow Duplicate Colors"]="Povolit duplicitní barvy",["Auto Ghost After Start"]="Auto duch po startu",["FAVORITE OUTFITS"]="OBLÍBENÉ OUTFITY",["Slot"]="Slot",["Empty"]="Prázdné",["Apply"]="Použít",["Save Mine"]="Uložit můj",["Save Selected"]="Uložit vybraný",["Saved slot"]="Slot uložen",["Applied slot"]="Slot použit",["Cleared slot"]="Slot vymazán",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof platformy",["Ban Custom Platforms From TXT"]="Ban platforem z TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="Limit RPC:",["RPC Local Drop"]="Místní drop RPC",["RPC Host Ban"]="RPC host ban" }, + ["ro"] = new Dictionary { ["GENERAL"]="GENERAL",["SELF"]="JUCĂTOR",["VISUALS"]="VIZUAL",["PLAYERS"]="JUCĂTORI",["SABOTAGES"]="SABOTAJE",["HOST ONLY"]="HOST",["OUTFITS"]="ȚINUTE",["VOTEKICK"]="VOTEKICK",["MENU"]="MENIU",["MAPS"]="HĂRȚI",["ANIMATIONS"]="ANIMAȚII",["INFORMATION"]="INFORMAȚII",["KEYBINDS"]="TASTE",["WELCOME"]="BUN VENIT",["CREDITS"]="CREDITE",["Menu language:"]="Limba meniului:",["FPS Limit"]="Limită FPS",["Chat History"]="Istoric chat",["History:"]="Istoric:",["History size:"]="Mărime istoric:",["CHAT UTILITY"]="UTILITARE CHAT",["Always Show Chat"]="Arată chat mereu",["Read Ghost Chat"]="Citește chat fantome",["Extended Chat"]="Chat extins",["Fast Chat"]="Chat rapid",["Unlock Extra Characters"]="Permite caractere extra",["Spell Check"]="Ortografie",["Clipboard"]="Clipboard",["Save Chat Log"]="Salvează log chat",["Dark Chat Theme"]="Temă chat întunecată",["Enable /color"]="Activează /color",["Block Fortegreen"]="Blochează Fortegreen",["Allow Duplicate Colors"]="Permite culori duplicate",["Auto Ghost After Start"]="Fantoma auto după start",["FAVORITE OUTFITS"]="ȚINUTE FAVORITE",["Slot"]="Slot",["Empty"]="Gol",["Apply"]="Aplică",["Save Mine"]="Salvează al meu",["Save Selected"]="Salvează selectat",["Saved slot"]="Slot salvat",["Applied slot"]="Slot aplicat",["Cleared slot"]="Slot golit",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof platformă",["Ban Custom Platforms From TXT"]="Ban platforme din TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limită RPC:",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC host" }, + ["hu"] = new Dictionary { ["GENERAL"]="ÁLTALÁNOS",["SELF"]="JÁTÉKOS",["VISUALS"]="LÁTVÁNY",["PLAYERS"]="JÁTÉKOSOK",["SABOTAGES"]="SZABOTÁZS",["HOST ONLY"]="HOST",["OUTFITS"]="RUHÁK",["VOTEKICK"]="VOTEKICK",["MENU"]="MENÜ",["MAPS"]="PÁLYÁK",["ANIMATIONS"]="ANIMÁCIÓK",["INFORMATION"]="INFORMÁCIÓ",["KEYBINDS"]="BILLENTYŰK",["WELCOME"]="ÜDV",["CREDITS"]="KÉSZÍTŐK",["Menu language:"]="Menü nyelve:",["FPS Limit"]="FPS limit",["Chat History"]="Chat előzmények",["History:"]="Előzmény:",["History size:"]="Előzmény méret:",["CHAT UTILITY"]="CHAT ESZKÖZÖK",["Always Show Chat"]="Chat mindig látszik",["Read Ghost Chat"]="Szellem chat olvasás",["Extended Chat"]="Bővített chat",["Fast Chat"]="Gyors chat",["Unlock Extra Characters"]="Extra karakterek engedélyezése",["Spell Check"]="Helyesírás",["Clipboard"]="Vágólap",["Save Chat Log"]="Chat log mentése",["Dark Chat Theme"]="Sötét chat téma",["Enable /color"]="/color bekapcsolása",["Block Fortegreen"]="Fortegreen tiltása",["Allow Duplicate Colors"]="Dupla színek engedése",["Auto Ghost After Start"]="Auto szellem start után",["FAVORITE OUTFITS"]="KEDVENC RUHÁK",["Slot"]="Slot",["Empty"]="Üres",["Apply"]="Alkalmaz",["Save Mine"]="Saját mentése",["Save Selected"]="Kiválasztott mentése",["Saved slot"]="Slot mentve",["Applied slot"]="Slot alkalmazva",["Cleared slot"]="Slot törölve",["Auto-Ban Platform Spoof (Host)"]="Platform spoof auto-ban",["Ban Custom Platforms From TXT"]="Platform ban TXT-ből",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC limit:",["RPC Local Drop"]="RPC helyi drop",["RPC Host Ban"]="RPC host ban" }, + ["sv"] = new Dictionary { ["GENERAL"]="ALLMÄNT",["SELF"]="SPELARE",["VISUALS"]="VISUELLT",["PLAYERS"]="SPELARE",["SABOTAGES"]="SABOTAGE",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENY",["MAPS"]="KARTOR",["ANIMATIONS"]="ANIMATIONER",["INFORMATION"]="INFO",["KEYBINDS"]="TANGENTER",["WELCOME"]="VÄLKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menyspråk:",["FPS Limit"]="FPS-gräns",["Chat History"]="Chatthistorik",["History:"]="Historik:",["History size:"]="Historikstorlek:",["CHAT UTILITY"]="CHATTVERKTYG",["Always Show Chat"]="Visa alltid chatt",["Read Ghost Chat"]="Läs spökchatt",["Extended Chat"]="Utökad chatt",["Fast Chat"]="Snabb chatt",["Unlock Extra Characters"]="Tillåt extra tecken",["Spell Check"]="Stavning",["Clipboard"]="Urklipp",["Save Chat Log"]="Spara chattlogg",["Dark Chat Theme"]="Mörkt chatt-tema",["Enable /color"]="Aktivera /color",["Block Fortegreen"]="Blockera Fortegreen",["Allow Duplicate Colors"]="Tillåt dubbla färger",["Auto Ghost After Start"]="Auto-spöke efter start",["FAVORITE OUTFITS"]="FAVORITOUTFITS",["Slot"]="Slot",["Empty"]="Tom",["Apply"]="Använd",["Save Mine"]="Spara min",["Save Selected"]="Spara vald",["Saved slot"]="Slot sparad",["Applied slot"]="Slot använd",["Cleared slot"]="Slot rensad",["Auto-Ban Platform Spoof (Host)"]="Auto-ban plattformsspoof",["Ban Custom Platforms From TXT"]="Ban plattformar från TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-gräns:",["RPC Local Drop"]="RPC lokal drop",["RPC Host Ban"]="RPC host-ban" }, + ["da"] = new Dictionary { ["GENERAL"]="GENERELT",["SELF"]="SPILLER",["VISUALS"]="VISUELT",["PLAYERS"]="SPILLERE",["SABOTAGES"]="SABOTAGER",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="BANER",["ANIMATIONS"]="ANIMATIONER",["INFORMATION"]="INFO",["KEYBINDS"]="TASTER",["WELCOME"]="VELKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menusprog:",["FPS Limit"]="FPS-grænse",["Chat History"]="Chathistorik",["History:"]="Historik:",["History size:"]="Historikstørrelse:",["CHAT UTILITY"]="CHATVÆRKTØJ",["Always Show Chat"]="Vis altid chat",["Read Ghost Chat"]="Læs spøgelses-chat",["Extended Chat"]="Udvidet chat",["Fast Chat"]="Hurtig chat",["Unlock Extra Characters"]="Tillad ekstra tegn",["Spell Check"]="Stavekontrol",["Clipboard"]="Udklipsholder",["Save Chat Log"]="Gem chatlog",["Dark Chat Theme"]="Mørkt chattema",["Enable /color"]="Aktiver /color",["Block Fortegreen"]="Bloker Fortegreen",["Allow Duplicate Colors"]="Tillad dubletfarver",["Auto Ghost After Start"]="Auto-spøgelse efter start",["FAVORITE OUTFITS"]="FAVORITOUTFITS",["Slot"]="Slot",["Empty"]="Tom",["Apply"]="Anvend",["Save Mine"]="Gem min",["Save Selected"]="Gem valgt",["Saved slot"]="Slot gemt",["Applied slot"]="Slot anvendt",["Cleared slot"]="Slot ryddet",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban platforme fra TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-grænse:",["RPC Local Drop"]="RPC lokal drop",["RPC Host Ban"]="RPC host-ban" }, + ["fi"] = new Dictionary { ["GENERAL"]="YLEISET",["SELF"]="PELAAJA",["VISUALS"]="VISUAALIT",["PLAYERS"]="PELAAJAT",["SABOTAGES"]="SABOTAASIT",["HOST ONLY"]="HOST",["OUTFITS"]="ASUT",["VOTEKICK"]="VOTEKICK",["MENU"]="VALIKKO",["MAPS"]="KARTAT",["ANIMATIONS"]="ANIMAATIOT",["INFORMATION"]="TIEDOT",["KEYBINDS"]="NÄPPÄIMET",["WELCOME"]="TERVETULOA",["CREDITS"]="TEKIJÄT",["Menu language:"]="Valikon kieli:",["FPS Limit"]="FPS-raja",["Chat History"]="Chat-historia",["History:"]="Historia:",["History size:"]="Historian koko:",["CHAT UTILITY"]="CHAT-TYÖKALUT",["Always Show Chat"]="Näytä chat aina",["Read Ghost Chat"]="Lue haamuchat",["Extended Chat"]="Laajennettu chat",["Fast Chat"]="Nopea chat",["Unlock Extra Characters"]="Salli lisämerkit",["Spell Check"]="Oikoluku",["Clipboard"]="Leikepöytä",["Save Chat Log"]="Tallenna chat-loki",["Dark Chat Theme"]="Tumma chat-teema",["Enable /color"]="Ota /color käyttöön",["Block Fortegreen"]="Estä Fortegreen",["Allow Duplicate Colors"]="Salli samat värit",["Auto Ghost After Start"]="Auto-haamu alun jälkeen",["FAVORITE OUTFITS"]="SUOSIKKIASUT",["Slot"]="Paikka",["Empty"]="Tyhjä",["Apply"]="Käytä",["Save Mine"]="Tallenna oma",["Save Selected"]="Tallenna valittu",["Saved slot"]="Paikka tallennettu",["Applied slot"]="Paikka käytetty",["Cleared slot"]="Paikka tyhjennetty",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban alustat TXT:stä",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-raja:",["RPC Local Drop"]="RPC paikallinen drop",["RPC Host Ban"]="RPC host-ban" }, + ["no"] = new Dictionary { ["GENERAL"]="GENERELT",["SELF"]="SPILLER",["VISUALS"]="VISUELT",["PLAYERS"]="SPILLERE",["SABOTAGES"]="SABOTASJER",["HOST ONLY"]="HOST",["OUTFITS"]="ANTREKK",["VOTEKICK"]="VOTEKICK",["MENU"]="MENY",["MAPS"]="KART",["ANIMATIONS"]="ANIMASJONER",["INFORMATION"]="INFO",["KEYBINDS"]="TASTER",["WELCOME"]="VELKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menyspråk:",["FPS Limit"]="FPS-grense",["Chat History"]="Chat-historikk",["History:"]="Historikk:",["History size:"]="Historikkstørrelse:",["CHAT UTILITY"]="CHAT-VERKTØY",["Always Show Chat"]="Vis alltid chat",["Read Ghost Chat"]="Les spøkelseschat",["Extended Chat"]="Utvidet chat",["Fast Chat"]="Rask chat",["Unlock Extra Characters"]="Tillat ekstra tegn",["Spell Check"]="Stavekontroll",["Clipboard"]="Utklippstavle",["Save Chat Log"]="Lagre chatlogg",["Dark Chat Theme"]="Mørkt chattema",["Enable /color"]="Aktiver /color",["Block Fortegreen"]="Blokker Fortegreen",["Allow Duplicate Colors"]="Tillat like farger",["Auto Ghost After Start"]="Auto-spøkelse etter start",["FAVORITE OUTFITS"]="FAVORITTANTREKK",["Slot"]="Slot",["Empty"]="Tom",["Apply"]="Bruk",["Save Mine"]="Lagre min",["Save Selected"]="Lagre valgt",["Saved slot"]="Slot lagret",["Applied slot"]="Slot brukt",["Cleared slot"]="Slot tømt",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban plattformer fra TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-grense:",["RPC Local Drop"]="RPC lokal drop",["RPC Host Ban"]="RPC host-ban" }, + ["uk"] = new Dictionary { ["GENERAL"]="ЗАГАЛЬНЕ",["SELF"]="ГРАВЕЦЬ",["VISUALS"]="ВІЗУАЛ",["PLAYERS"]="ГРАВЦІ",["SABOTAGES"]="САБОТАЖІ",["HOST ONLY"]="ХОСТ",["OUTFITS"]="ОДЯГ",["VOTEKICK"]="КІК",["MENU"]="МЕНЮ",["MAPS"]="КАРТИ",["ANIMATIONS"]="АНІМАЦІЇ",["INFORMATION"]="ІНФОРМАЦІЯ",["KEYBINDS"]="БІНДИ",["WELCOME"]="ВІТАННЯ",["CREDITS"]="АВТОРИ",["Menu language:"]="Мова меню:",["FPS Limit"]="Ліміт FPS",["Chat History"]="Історія чату",["History:"]="Історія:",["History size:"]="Розмір історії:",["CHAT UTILITY"]="УТИЛІТИ ЧАТУ",["Always Show Chat"]="Завжди показувати чат",["Read Ghost Chat"]="Читати чат привидів",["Extended Chat"]="Розширений чат",["Fast Chat"]="Швидкий чат",["Unlock Extra Characters"]="Дозволити всі символи",["Spell Check"]="Перевірка орфографії",["Clipboard"]="Буфер обміну",["Save Chat Log"]="Зберігати лог чату",["Dark Chat Theme"]="Темна тема чату",["Enable /color"]="Увімкнути /color",["Block Fortegreen"]="Блок Fortegreen",["Allow Duplicate Colors"]="Дозволити однакові кольори",["Auto Ghost After Start"]="Авто-привид після старту",["FAVORITE OUTFITS"]="УЛЮБЛЕНІ ОБРАЗИ",["Slot"]="Слот",["Empty"]="Пусто",["Apply"]="Надіти",["Save Mine"]="Зберегти мій",["Save Selected"]="Зберегти вибраний",["Saved slot"]="Слот збережено",["Applied slot"]="Слот застосовано",["Cleared slot"]="Слот очищено",["Auto-Ban Platform Spoof (Host)"]="Авто-бан Platform Spoof",["Ban Custom Platforms From TXT"]="Бан платформ з TXT",["RPC Anti-Cheat"]="RPC античит",["RPC limit:"]="RPC ліміт:",["RPC Local Drop"]="RPC локальний дроп",["RPC Host Ban"]="RPC бан хоста" }, + ["el"] = new Dictionary { ["GENERAL"]="ΓΕΝΙΚΑ",["SELF"]="ΠΑΙΚΤΗΣ",["VISUALS"]="ΟΠΤΙΚΑ",["PLAYERS"]="ΠΑΙΚΤΕΣ",["SABOTAGES"]="ΣΑΜΠΟΤΑΖ",["HOST ONLY"]="HOST",["OUTFITS"]="ΣΤΟΛΕΣ",["VOTEKICK"]="VOTEKICK",["MENU"]="ΜΕΝΟΥ",["MAPS"]="ΧΑΡΤΕΣ",["ANIMATIONS"]="ANIMATIONS",["INFORMATION"]="ΠΛΗΡΟΦΟΡΙΕΣ",["KEYBINDS"]="ΠΛΗΚΤΡΑ",["WELCOME"]="ΚΑΛΩΣ ΗΡΘΕΣ",["CREDITS"]="CREDITS",["Menu language:"]="Γλώσσα μενού:",["FPS Limit"]="Όριο FPS",["Chat History"]="Ιστορικό chat",["History:"]="Ιστορικό:",["History size:"]="Μέγεθος ιστορικού:",["CHAT UTILITY"]="ΕΡΓΑΛΕΙΑ CHAT",["Always Show Chat"]="Πάντα εμφάνιση chat",["Read Ghost Chat"]="Ανάγνωση ghost chat",["Extended Chat"]="Εκτεταμένο chat",["Fast Chat"]="Γρήγορο chat",["Unlock Extra Characters"]="Επιπλέον χαρακτήρες",["Spell Check"]="Ορθογραφία",["Clipboard"]="Πρόχειρο",["Save Chat Log"]="Αποθήκευση chat log",["Dark Chat Theme"]="Σκούρο chat",["Enable /color"]="Ενεργοποίηση /color",["Block Fortegreen"]="Block Fortegreen",["Allow Duplicate Colors"]="Να επιτρέπονται ίδια χρώματα",["Auto Ghost After Start"]="Auto ghost μετά την έναρξη",["FAVORITE OUTFITS"]="ΑΓΑΠΗΜΕΝΕΣ ΣΤΟΛΕΣ",["Slot"]="Θέση",["Empty"]="Άδειο",["Apply"]="Εφαρμογή",["Save Mine"]="Αποθ. δικό μου",["Save Selected"]="Αποθ. επιλογής",["Saved slot"]="Θέση αποθηκεύτηκε",["Applied slot"]="Θέση εφαρμόστηκε",["Cleared slot"]="Θέση καθαρίστηκε",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban platforms από TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="Όριο RPC:",["RPC Local Drop"]="RPC local drop",["RPC Host Ban"]="RPC host ban" }, + ["zh"] = new Dictionary { ["GENERAL"]="常规",["SELF"]="玩家",["VISUALS"]="视觉",["PLAYERS"]="玩家",["SABOTAGES"]="破坏",["HOST ONLY"]="房主",["OUTFITS"]="装扮",["VOTEKICK"]="投票踢人",["MENU"]="菜单",["MAPS"]="地图",["ANIMATIONS"]="动画",["INFORMATION"]="信息",["KEYBINDS"]="按键",["WELCOME"]="欢迎",["CREDITS"]="鸣谢",["Menu language:"]="菜单语言:",["FPS Limit"]="FPS 限制",["Chat History"]="聊天历史",["History:"]="历史:",["History size:"]="历史大小:",["CHAT UTILITY"]="聊天工具",["Always Show Chat"]="始终显示聊天",["Read Ghost Chat"]="读取幽灵聊天",["Extended Chat"]="扩展聊天",["Fast Chat"]="快速聊天",["Unlock Extra Characters"]="允许额外字符",["Spell Check"]="拼写检查",["Clipboard"]="剪贴板",["Save Chat Log"]="保存聊天日志",["Dark Chat Theme"]="深色聊天主题",["Enable /color"]="启用 /color",["Block Fortegreen"]="阻止 Fortegreen",["Allow Duplicate Colors"]="允许重复颜色",["Auto Ghost After Start"]="开始后自动幽灵",["FAVORITE OUTFITS"]="收藏装扮",["Slot"]="槽位",["Empty"]="空",["Apply"]="应用",["Save Mine"]="保存自己",["Save Selected"]="保存选中",["Saved slot"]="槽位已保存",["Applied slot"]="槽位已应用",["Cleared slot"]="槽位已清空",["Auto-Ban Platform Spoof (Host)"]="自动封禁平台伪装",["Ban Custom Platforms From TXT"]="从 TXT 封禁自定义平台",["RPC Anti-Cheat"]="RPC 反作弊",["RPC limit:"]="RPC 限制:",["RPC Local Drop"]="RPC 本地丢弃",["RPC Host Ban"]="RPC 房主封禁" }, + ["ja"] = new Dictionary { ["GENERAL"]="一般",["SELF"]="プレイヤー",["VISUALS"]="表示",["PLAYERS"]="プレイヤー",["SABOTAGES"]="サボタージュ",["HOST ONLY"]="ホスト",["OUTFITS"]="衣装",["VOTEKICK"]="投票キック",["MENU"]="メニュー",["MAPS"]="マップ",["ANIMATIONS"]="アニメーション",["INFORMATION"]="情報",["KEYBINDS"]="キー設定",["WELCOME"]="ようこそ",["CREDITS"]="クレジット",["Menu language:"]="メニュー言語:",["FPS Limit"]="FPS制限",["Chat History"]="チャット履歴",["History:"]="履歴:",["History size:"]="履歴サイズ:",["CHAT UTILITY"]="チャット機能",["Always Show Chat"]="常にチャット表示",["Read Ghost Chat"]="ゴーストチャット読む",["Extended Chat"]="拡張チャット",["Fast Chat"]="高速チャット",["Unlock Extra Characters"]="追加文字を許可",["Spell Check"]="スペルチェック",["Clipboard"]="クリップボード",["Save Chat Log"]="チャットログ保存",["Dark Chat Theme"]="ダークチャット",["Enable /color"]="/color 有効",["Block Fortegreen"]="Fortegreen ブロック",["Allow Duplicate Colors"]="同じ色を許可",["Auto Ghost After Start"]="開始後に自動ゴースト",["FAVORITE OUTFITS"]="お気に入り衣装",["Slot"]="スロット",["Empty"]="空",["Apply"]="適用",["Save Mine"]="自分を保存",["Save Selected"]="選択を保存",["Saved slot"]="スロット保存",["Applied slot"]="スロット適用",["Cleared slot"]="スロット消去",["Auto-Ban Platform Spoof (Host)"]="平台偽装を自動BAN",["Ban Custom Platforms From TXT"]="TXTから平台BAN",["RPC Anti-Cheat"]="RPCアンチチート",["RPC limit:"]="RPC制限:",["RPC Local Drop"]="RPCローカル破棄",["RPC Host Ban"]="RPCホストBAN" }, + ["ko"] = new Dictionary { ["GENERAL"]="일반",["SELF"]="플레이어",["VISUALS"]="비주얼",["PLAYERS"]="플레이어",["SABOTAGES"]="사보타주",["HOST ONLY"]="호스트",["OUTFITS"]="의상",["VOTEKICK"]="투표킥",["MENU"]="메뉴",["MAPS"]="맵",["ANIMATIONS"]="애니메이션",["INFORMATION"]="정보",["KEYBINDS"]="키 설정",["WELCOME"]="환영",["CREDITS"]="크레딧",["Menu language:"]="메뉴 언어:",["FPS Limit"]="FPS 제한",["Chat History"]="채팅 기록",["History:"]="기록:",["History size:"]="기록 크기:",["CHAT UTILITY"]="채팅 도구",["Always Show Chat"]="항상 채팅 표시",["Read Ghost Chat"]="유령 채팅 읽기",["Extended Chat"]="확장 채팅",["Fast Chat"]="빠른 채팅",["Unlock Extra Characters"]="추가 문자 허용",["Spell Check"]="맞춤법 검사",["Clipboard"]="클립보드",["Save Chat Log"]="채팅 로그 저장",["Dark Chat Theme"]="어두운 채팅 테마",["Enable /color"]="/color 활성화",["Block Fortegreen"]="Fortegreen 차단",["Allow Duplicate Colors"]="중복 색상 허용",["Auto Ghost After Start"]="시작 후 자동 유령",["FAVORITE OUTFITS"]="즐겨찾기 의상",["Slot"]="슬롯",["Empty"]="비어 있음",["Apply"]="적용",["Save Mine"]="내 것 저장",["Save Selected"]="선택 저장",["Saved slot"]="슬롯 저장됨",["Applied slot"]="슬롯 적용됨",["Cleared slot"]="슬롯 삭제됨",["Auto-Ban Platform Spoof (Host)"]="플랫폼 위장 자동 밴",["Ban Custom Platforms From TXT"]="TXT 커스텀 플랫폼 밴",["RPC Anti-Cheat"]="RPC 안티치트",["RPC limit:"]="RPC 제한:",["RPC Local Drop"]="RPC 로컬 드롭",["RPC Host Ban"]="RPC 호스트 밴" } + }; + + private static readonly string[] menuTranslationFixKeys = + { + "ANTI CHEAT", "AUTO HOST", "LOBBY CONTROLS", "ROLE MANAGER", "PUNISHMENT SYSTEM", "Mode:", + "RPC PROTECTIONS", "Block Spoof RPC", "Block Sabotage & Meetings", "Block Game RPC in Lobby", + "Auto-Ban Platform Spoof (Host)", "Ban Custom Platforms From TXT", "Block Meeting RPC Flood", + "Block Chat RPC Flood", "OTHER PROTECTIONS", "Disable Vote Kicks (Host)", "Auto-Kick Fortegreen", + "Auto-Ban Broken FriendCode (Host)", "BAN LIST", "Auto-Ban Blacklisted Players", "Enter Friend Code", + "ADD", "Ban list is empty." + }; + + private static readonly Dictionary menuTranslationFixes = new Dictionary + { + ["de"] = new[] { "ANTI-CHEAT", "AUTO-HOST", "LOBBY-STEUERUNG", "ROLLENMANAGER", "STRAFSYSTEM", "Modus:", "RPC-SCHUTZ", "Spoof-RPC blockieren", "Sabotage & Meetings blockieren", "Spiel-RPC in Lobby blockieren", "Plattform-Spoof auto-bannen (Host)", "Custom-Plattformen aus TXT bannen", "Meeting-RPC-Flood blockieren", "Chat-RPC-Flood blockieren", "WEITERER SCHUTZ", "Vote-Kicks deaktivieren (Host)", "Fortegreen automatisch kicken", "Defekten FriendCode auto-bannen (Host)", "BAN-LISTE", "Spieler aus Ban-Liste auto-bannen", "Friend Code eingeben", "HINZUFÜGEN", "Ban-Liste ist leer." }, + ["fr"] = new[] { "ANTI-TRICHE", "HÔTE AUTO", "CONTRÔLES LOBBY", "GESTION DES RÔLES", "SYSTÈME DE SANCTIONS", "Mode :", "PROTECTIONS RPC", "Bloquer RPC spoof", "Bloquer sabotages et meetings", "Bloquer RPC de jeu en lobby", "Auto-ban spoof plateforme (Hôte)", "Ban plateformes custom TXT", "Bloquer flood RPC meeting", "Bloquer flood RPC chat", "AUTRES PROTECTIONS", "Désactiver vote-kicks (Hôte)", "Auto-kick Fortegreen", "Auto-ban FriendCode cassé (Hôte)", "LISTE DE BAN", "Auto-ban joueurs listés", "Entrer Friend Code", "AJOUTER", "La liste de ban est vide." }, + ["es"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROLES DE LOBBY", "GESTOR DE ROLES", "SISTEMA DE SANCIONES", "Modo:", "PROTECCIONES RPC", "Bloquear RPC spoof", "Bloquear sabotajes y reuniones", "Bloquear RPC de juego en lobby", "Auto-ban spoof de plataforma (Host)", "Ban de plataformas custom desde TXT", "Bloquear flood RPC de reunión", "Bloquear flood RPC de chat", "OTRAS PROTECCIONES", "Desactivar vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode roto (Host)", "LISTA DE BAN", "Auto-ban jugadores en lista", "Introducir Friend Code", "AÑADIR", "La lista de ban está vacía." }, + ["it"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROLLI LOBBY", "GESTORE RUOLI", "SISTEMA PUNIZIONI", "Modalità:", "PROTEZIONI RPC", "Blocca RPC spoof", "Blocca sabotaggi e meeting", "Blocca RPC di gioco in lobby", "Auto-ban spoof piattaforma (Host)", "Ban piattaforme custom da TXT", "Blocca flood RPC meeting", "Blocca flood RPC chat", "ALTRE PROTEZIONI", "Disattiva vote-kick (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode rotto (Host)", "LISTA BAN", "Auto-ban giocatori in lista", "Inserisci Friend Code", "AGGIUNGI", "La lista ban è vuota." }, + ["pt"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROLES DO LOBBY", "GERENCIADOR DE FUNÇÕES", "SISTEMA DE PUNIÇÕES", "Modo:", "PROTEÇÕES RPC", "Bloquear RPC spoof", "Bloquear sabotagens e reuniões", "Bloquear RPC de jogo no lobby", "Auto-ban spoof de plataforma (Host)", "Ban plataformas custom do TXT", "Bloquear flood RPC de reunião", "Bloquear flood RPC de chat", "OUTRAS PROTEÇÕES", "Desativar vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode quebrado (Host)", "LISTA DE BAN", "Auto-ban jogadores listados", "Inserir Friend Code", "ADICIONAR", "A lista de ban está vazia." }, + ["pl"] = new[] { "ANTI-CHEAT", "AUTO HOST", "KONTROLA LOBBY", "MENEDŻER RÓL", "SYSTEM KAR", "Tryb:", "OCHRONA RPC", "Blokuj spoof RPC", "Blokuj sabotaże i spotkania", "Blokuj RPC gry w lobby", "Auto-ban spoof platformy (Host)", "Ban platform custom z TXT", "Blokuj flood RPC spotkania", "Blokuj flood RPC czatu", "INNA OCHRONA", "Wyłącz vote-kicki (Host)", "Auto-kick Fortegreen", "Auto-ban uszkodzony FriendCode (Host)", "LISTA BANÓW", "Auto-ban graczy z listy", "Wpisz Friend Code", "DODAJ", "Lista banów jest pusta." }, + ["nl"] = new[] { "ANTI-CHEAT", "AUTO-HOST", "LOBBYBEDIENING", "ROLLENBEHEER", "STRAFSYSTEEM", "Modus:", "RPC-BESCHERMING", "Spoof-RPC blokkeren", "Sabotage & meetings blokkeren", "Game-RPC in lobby blokkeren", "Platform-spoof auto-bannen (Host)", "Custom platforms uit TXT bannen", "Meeting-RPC-flood blokkeren", "Chat-RPC-flood blokkeren", "ANDERE BESCHERMING", "Vote-kicks uitschakelen (Host)", "Fortegreen automatisch kicken", "Kapotte FriendCode auto-bannen (Host)", "BANLIJST", "Spelers op banlijst auto-bannen", "Friend Code invoeren", "TOEVOEGEN", "Banlijst is leeg." }, + ["tr"] = new[] { "ANTI-CHEAT", "OTO HOST", "LOBI KONTROLLERİ", "ROL YÖNETİCİSİ", "CEZA SİSTEMİ", "Mod:", "RPC KORUMALARI", "Spoof RPC engelle", "Sabotaj ve toplantıları engelle", "Lobide oyun RPC engelle", "Platform spoof oto-ban (Host)", "TXT özel platform ban", "Toplantı RPC flood engelle", "Sohbet RPC flood engelle", "DİĞER KORUMALAR", "Vote-kick kapat (Host)", "Fortegreen oto-kick", "Bozuk FriendCode oto-ban (Host)", "BAN LİSTESİ", "Listedeki oyuncuları oto-ban", "Friend Code gir", "EKLE", "Ban listesi boş." }, + ["cs"] = new[] { "ANTI-CHEAT", "AUTO HOST", "OVLÁDÁNÍ LOBBY", "SPRÁVCE ROLÍ", "SYSTÉM TRESTŮ", "Režim:", "OCHRANA RPC", "Blokovat spoof RPC", "Blokovat sabotáže a meetingy", "Blokovat herní RPC v lobby", "Auto-ban spoof platformy (Host)", "Ban custom platforem z TXT", "Blokovat meeting RPC flood", "Blokovat chat RPC flood", "DALŠÍ OCHRANA", "Vypnout vote-kicky (Host)", "Auto-kick Fortegreen", "Auto-ban rozbitý FriendCode (Host)", "BAN LIST", "Auto-ban hráčů z listu", "Zadej Friend Code", "PŘIDAT", "Ban list je prázdný." }, + ["ro"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROALE LOBBY", "MANAGER ROLURI", "SISTEM DE PEDEPSE", "Mod:", "PROTECȚII RPC", "Blochează RPC spoof", "Blochează sabotaje și meetinguri", "Blochează RPC de joc în lobby", "Auto-ban spoof platformă (Host)", "Ban platforme custom din TXT", "Blochează flood RPC meeting", "Blochează flood RPC chat", "ALTE PROTECȚII", "Dezactivează vote-kick (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode stricat (Host)", "LISTĂ BAN", "Auto-ban jucători listați", "Introdu Friend Code", "ADAUGĂ", "Lista de ban este goală." }, + ["hu"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBI VEZÉRLÉS", "SZEREPKEZELŐ", "BÜNTETÉSI RENDSZER", "Mód:", "RPC VÉDELEM", "Spoof RPC blokkolása", "Szabotázsok és meetingek blokkolása", "Játék RPC blokkolása lobbyban", "Platform spoof auto-ban (Host)", "Custom platformok bannolása TXT-ből", "Meeting RPC flood blokkolása", "Chat RPC flood blokkolása", "EGYÉB VÉDELEM", "Vote-kick tiltása (Host)", "Fortegreen auto-kick", "Hibás FriendCode auto-ban (Host)", "BAN LISTA", "Listás játékosok auto-banja", "Friend Code megadása", "HOZZÁAD", "A ban lista üres." }, + ["sv"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYKONTROLLER", "ROLLHANTERARE", "STRAFFSYSTEM", "Läge:", "RPC-SKYDD", "Blockera spoof-RPC", "Blockera sabotage och möten", "Blockera spel-RPC i lobby", "Auto-ban plattformsspoof (Host)", "Ban custom-plattformar från TXT", "Blockera meeting RPC-flood", "Blockera chat RPC-flood", "ANNAT SKYDD", "Inaktivera vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban trasig FriendCode (Host)", "BANLISTA", "Auto-ban spelare på lista", "Ange Friend Code", "LÄGG TILL", "Banlistan är tom." }, + ["da"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYKONTROL", "ROLLEMANAGER", "STRAFSYSTEM", "Tilstand:", "RPC-BESKYTTELSE", "Bloker spoof-RPC", "Bloker sabotager og møder", "Bloker spil-RPC i lobby", "Auto-ban platform spoof (Host)", "Ban custom-platforme fra TXT", "Bloker meeting RPC-flood", "Bloker chat RPC-flood", "ANDEN BESKYTTELSE", "Deaktiver vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban defekt FriendCode (Host)", "BANLISTE", "Auto-ban spillere på liste", "Indtast Friend Code", "TILFØJ", "Banlisten er tom." }, + ["fi"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYN HALLINTA", "ROOLIEN HALLINTA", "RANGAISTUSJÄRJESTELMÄ", "Tila:", "RPC-SUOJAUKSET", "Estä spoof RPC", "Estä sabotaasit ja kokoukset", "Estä peli-RPC lobbyssa", "Auto-ban platform spoof (Host)", "Ban custom-alustat TXT:stä", "Estä meeting RPC flood", "Estä chat RPC flood", "MUUT SUOJAUKSET", "Poista vote-kickit käytöstä (Host)", "Auto-kick Fortegreen", "Auto-ban rikkinäinen FriendCode (Host)", "BAN-LISTA", "Auto-ban listatut pelaajat", "Syötä Friend Code", "LISÄÄ", "Ban-lista on tyhjä." }, + ["no"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYKONTROLLER", "ROLLEBEHANDLER", "STRAFFESYSTEM", "Modus:", "RPC-BESKYTTELSE", "Blokker spoof-RPC", "Blokker sabotasje og møter", "Blokker spill-RPC i lobby", "Auto-ban platform spoof (Host)", "Ban custom-plattformer fra TXT", "Blokker meeting RPC-flood", "Blokker chat RPC-flood", "ANNEN BESKYTTELSE", "Deaktiver vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban ødelagt FriendCode (Host)", "BANLISTE", "Auto-ban spillere på liste", "Skriv Friend Code", "LEGG TIL", "Banlisten er tom." }, + ["uk"] = new[] { "АНТИЧИТ", "АВТО ХОСТ", "КЕРУВАННЯ ЛОБІ", "МЕНЕДЖЕР РОЛЕЙ", "СИСТЕМА ПОКАРАНЬ", "Режим:", "ЗАХИСТ RPC", "Блокувати spoof RPC", "Блокувати саботажі та зустрічі", "Блокувати ігрові RPC у лобі", "Авто-бан spoof платформи (Хост)", "Бан кастомних платформ з TXT", "Блокувати flood RPC зустрічі", "Блокувати flood RPC чату", "ІНШИЙ ЗАХИСТ", "Вимкнути vote-kick (Хост)", "Авто-кік Fortegreen", "Авто-бан зламаного FriendCode (Хост)", "БАН-ЛИСТ", "Авто-бан гравців зі списку", "Введіть Friend Code", "ДОДАТИ", "Бан-лист порожній." }, + ["el"] = new[] { "ANTI-CHEAT", "AUTO HOST", "ΕΛΕΓΧΟΙ LOBBY", "ΔΙΑΧΕΙΡΙΣΗ ΡΟΛΩΝ", "ΣΥΣΤΗΜΑ ΠΟΙΝΩΝ", "Λειτουργία:", "ΠΡΟΣΤΑΣΙΕΣ RPC", "Μπλοκ spoof RPC", "Μπλοκ σαμποτάζ και meetings", "Μπλοκ game RPC στο lobby", "Auto-ban platform spoof (Host)", "Ban custom platforms από TXT", "Μπλοκ meeting RPC flood", "Μπλοκ chat RPC flood", "ΑΛΛΕΣ ΠΡΟΣΤΑΣΙΕΣ", "Απενεργοποίηση vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban χαλασμένο FriendCode (Host)", "ΛΙΣΤΑ BAN", "Auto-ban παικτών στη λίστα", "Εισαγωγή Friend Code", "ΠΡΟΣΘΗΚΗ", "Η λίστα ban είναι άδεια." }, + ["zh"] = new[] { "反作弊", "自动房主", "大厅控制", "身份管理", "处罚系统", "模式:", "RPC 防护", "阻止 Spoof RPC", "阻止破坏和会议", "阻止大厅内游戏 RPC", "自动封禁平台伪装 (房主)", "从 TXT 封禁自定义平台", "阻止会议 RPC 洪泛", "阻止聊天 RPC 洪泛", "其他防护", "禁用投票踢人 (房主)", "自动踢出 Fortegreen", "自动封禁损坏 FriendCode (房主)", "封禁列表", "自动封禁列表玩家", "输入 Friend Code", "添加", "封禁列表为空。" }, + ["ja"] = new[] { "アンチチート", "自動ホスト", "ロビー制御", "ロール管理", "処罰システム", "モード:", "RPC保護", "Spoof RPCをブロック", "サボタージュと会議をブロック", "ロビー中のゲームRPCをブロック", "プラットフォーム偽装を自動BAN (ホスト)", "TXTのカスタムプラットフォームをBAN", "会議RPCフラッドをブロック", "チャットRPCフラッドをブロック", "その他の保護", "投票キックを無効化 (ホスト)", "Fortegreenを自動キック", "壊れたFriendCodeを自動BAN (ホスト)", "BANリスト", "BANリストのプレイヤーを自動BAN", "Friend Codeを入力", "追加", "BANリストは空です。" }, + ["ko"] = new[] { "안티치트", "자동 호스트", "로비 컨트롤", "역할 관리자", "처벌 시스템", "모드:", "RPC 보호", "Spoof RPC 차단", "사보타주와 회의 차단", "로비에서 게임 RPC 차단", "플랫폼 위장 자동 밴 (호스트)", "TXT 커스텀 플랫폼 밴", "회의 RPC 플러드 차단", "채팅 RPC 플러드 차단", "기타 보호", "투표 킥 비활성화 (호스트)", "Fortegreen 자동 킥", "손상된 FriendCode 자동 밴 (호스트)", "밴 목록", "목록의 플레이어 자동 밴", "Friend Code 입력", "추가", "밴 목록이 비어 있습니다." } + }; public static byte selectedMorphTargetId = 255; public static bool unlockCosmetics = true; @@ -536,6 +602,12 @@ public static string L(string eng, string rus) { try { + string configuredLanguage = CurrentMenuLanguageCode(); + if (configuredLanguage == "ru" || configuredLanguage == "uk") + return TryTranslateMenuText(configuredLanguage, eng, configuredLanguage == "ru" ? rus : null); + if (configuredLanguage != "auto") + return TryTranslateMenuText(configuredLanguage, eng, eng); + if (DestroyableSingleton.InstanceExists) { string currentLang = DestroyableSingleton.Instance.currentLanguage.ToString().ToLower(); @@ -547,6 +619,45 @@ public static string L(string eng, string rus) return eng; } + private static string TryTranslateMenuText(string languageCode, string englishText, string fallback) + { + try + { + if (string.IsNullOrWhiteSpace(languageCode) || string.IsNullOrEmpty(englishText)) + return fallback ?? englishText; + + if (languageCode == "en") + return englishText; + + if (menuTranslationFixes.TryGetValue(languageCode, out string[] fixedTranslations)) + { + int fixedIndex = Array.IndexOf(menuTranslationFixKeys, englishText); + if (fixedIndex >= 0 && fixedIndex < fixedTranslations.Length && !string.IsNullOrWhiteSpace(fixedTranslations[fixedIndex])) + return fixedTranslations[fixedIndex]; + } + + if (menuTranslations.TryGetValue(languageCode, out Dictionary translations) && + translations.TryGetValue(englishText, out string translated) && + !string.IsNullOrWhiteSpace(translated)) + return translated; + } + catch { } + + return fallback ?? englishText; + } + + public static string CurrentMenuLanguageCode() + { + try + { + int index = Mathf.Clamp(currentMenuLanguageIndex, 0, menuLanguageCodes.Length - 1); + return menuLanguageCodes[index]; + } + catch { } + + return "auto"; + } + private int currentGeneralSubTab = 0; private int currentGeneralInfoSubTab = 0; private string[] generalSubTabs => new string[] { L("INFORMATION", "ИНФОРМАЦИЯ"), L("KEYBINDS", "БИНДЫ") }; @@ -1065,7 +1176,8 @@ private void DrawAutoHostMainTab() GUILayout.BeginHorizontal(); for (int i = 0; i < autoHostSubTabs.Length; i++) { - if (GUILayout.Button(autoHostSubTabs[i], currentAutoHostSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) + string subTabLabel = i < hostOnlySubTabs.Length ? hostOnlySubTabs[i] : autoHostSubTabs[i]; + if (GUILayout.Button(subTabLabel, currentAutoHostSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) { currentAutoHostSubTab = i; scrollPosition = Vector2.zero; @@ -1179,6 +1291,12 @@ private void DrawChatSettingsTab() GUILayout.Space(6); enableChatHistory = DrawToggle(enableChatHistory, L("Chat History (Up/Down)", "История чата (Стрелочки)"), 280); GUILayout.Space(2); + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("History size:", "Размер истории:")} {chatHistoryLimit}", new GUIStyle(toggleLabelStyle) { richText = true }, GUILayout.Width(130)); + chatHistoryLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(chatHistoryLimit, 5f, 80f, sliderStyle, sliderThumbStyle, GUILayout.Width(145)), 5, 80); + TrimChatHistoryToLimit(); + GUILayout.EndHorizontal(); + GUILayout.Space(2); enableClipboard = DrawToggle(enableClipboard, L("Clipboard (Ctrl+C/V)", "Буфер обмена (Ctrl+C/V)"), 280); GUILayout.Space(2); enableChatLog = DrawToggle(enableChatLog, L("Save Chat Log to File", "Сохранять лог чата в файл"), 280); @@ -1696,33 +1814,37 @@ private void DrawAntiCheatTab() }; GUILayout.Label(modeDesc, new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); - GUILayout.Space(15); + GUILayout.Space(12); GUILayout.Label(L("RPC PROTECTIONS", "ЗАЩИТА RPC"), headerStyle); - blockSpoofRPC = DrawToggle(blockSpoofRPC, "Block Spoof RPC", 250); + blockSpoofRPC = DrawToggle(blockSpoofRPC, L("Block Spoof RPC", "Блокировать spoof RPC"), 250); + GUILayout.Space(5); + blockSabotageRPC = DrawToggle(blockSabotageRPC, L("Block Sabotage & Meetings", "Блокировать саботажи и митинги"), 250); + GUILayout.Space(5); + blockGameRpcInLobby = DrawToggle(blockGameRpcInLobby, L("Block Game RPC in Lobby", "Блокировать игровые RPC в лобби"), 250); + GUILayout.Space(5); + + autoBanPlatformSpoof = DrawToggle(autoBanPlatformSpoof, L("Auto-Ban Platform Spoof (Host)", "Авто-бан Platform Spoof (Хост)"), 250); GUILayout.Space(5); - blockSabotageRPC = DrawToggle(blockSabotageRPC, "Block Sabotage & Meetings", 250); + banCustomPlatformsFromTxt = DrawToggle(banCustomPlatformsFromTxt, L("Ban Custom Platforms From TXT", "Бан кастом платформ из TXT"), 250); GUILayout.Space(5); - blockGameRpcInLobby = DrawToggle(blockGameRpcInLobby, "Block Game RPC in Lobby", 250); + + blockMeetingFloodRpc = DrawToggle(blockMeetingFloodRpc, L("Block Meeting RPC Flood", "Блокировать флуд RPC митинга"), 250); GUILayout.Space(5); - blockMeetingFloodRpc = DrawToggle(blockMeetingFloodRpc, "Block Meeting RPC Flood", 250); + blockChatFloodRpc = DrawToggle(blockChatFloodRpc, L("Block Chat RPC Flood", "Блокировать флуд RPC чата"), 250); GUILayout.Space(5); - blockChatFloodRpc = DrawToggle(blockChatFloodRpc, "Block Chat RPC Flood", 250); + enablePasosLimit = DrawToggle(enablePasosLimit, L("RPC Anti-Cheat", "RPC Античит"), 250); GUILayout.Space(5); - enablePasosLimit = DrawToggle(enablePasosLimit, "Message Block Freeze", 250); + rpcSpamLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(rpcSpamLimit, 10f, 250f, sliderStyle, sliderThumbStyle, GUILayout.Width(250)), 10, 250); GUILayout.Space(5); - enableLocalPasosBan = DrawToggle(enableLocalPasosBan, "Message Block Freeze Local Ban", 250); + enableLocalPasosBan = DrawToggle(enableLocalPasosBan, L("RPC Local Drop", "RPC локальный дроп"), 250); GUILayout.Space(5); - enableHostPasosBan = DrawToggle(enableHostPasosBan, "Message Block Freeze Host Ban", 250); + enableHostPasosBan = DrawToggle(enableHostPasosBan, L("RPC Host Ban", "RPC бан на хосте"), 250); GUILayout.Space(15); GUILayout.Label(L("OTHER PROTECTIONS", "ПРОЧАЯ ЗАЩИТА"), headerStyle); disableVoteKicks = DrawToggle(disableVoteKicks, L("Disable Vote Kicks (Host)", "Запрет кика голосованием (Хост)"), 250); GUILayout.Space(5); - enableLocalPetSpamDrop = DrawToggle(enableLocalPetSpamDrop, L("Block Pet Spam (Local)", "Блок спама питомцем (Локально)"), 250); - GUILayout.Space(5); - enableHostPetSpamBan = DrawToggle(enableHostPetSpamBan, L("Auto-Ban Pet Spammers", "Авто-бан за спам питомцем"), 250); - GUILayout.Space(5); autoKickBugs = DrawToggle(autoKickBugs, L("Auto-Kick Fortegreen", "Авто-кик багнутых игроков"), 250); if (autoKickBugs) @@ -1878,6 +2000,13 @@ private static bool IsFlooded(Dictionary> map, byte playerId, public static bool Prefix(PlayerControl __instance, byte callId, Hazel.MessageReader reader) { + if (__instance != null && __instance != PlayerControl.LocalPlayer && __instance.Data != null && ElysiumModMenuGUI.enablePasosLimit) + { + int clientId = Shield_PasosLimit_Patch.GetKickClientId(__instance, -1); + if (Shield_PasosLimit_Patch.RecordDrop(clientId, __instance, $"PlayerControl RPC {callId} spam")) + return false; + } + if (!ElysiumModMenuGUI.blockSpoofRPC && !ElysiumModMenuGUI.blockSabotageRPC && !ElysiumModMenuGUI.blockGameRpcInLobby && @@ -2038,18 +2167,50 @@ public static bool Prefix(ShipStatus __instance, byte callId, Hazel.MessageReade } public static bool autoChatEveryone = false; public static bool pendingAutoMeeting = false; + + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckColor))] + public static class AllowDuplicateColors_CheckColor_Patch + { + private static bool applyingDuplicateColor; + + public static bool Prefix(PlayerControl __instance, byte bodyColor) + { + if (applyingDuplicateColor || !ElysiumModMenuGUI.allowDuplicateColors || + __instance == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || + bodyColor == byte.MaxValue) + return true; + + try + { + applyingDuplicateColor = true; + __instance.RpcSetColor(bodyColor); + return false; + } + catch { return true; } + finally { applyingDuplicateColor = false; } + } + } + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Start))] public static class Anticheat_Platform_Check { public static void Postfix(PlayerControl __instance) { - if (!ElysiumModMenuGUI.blockSpoofRPC || __instance == null || __instance == PlayerControl.LocalPlayer) return; + if ((!ElysiumModMenuGUI.blockSpoofRPC && !ElysiumModMenuGUI.autoBanPlatformSpoof && !ElysiumModMenuGUI.banCustomPlatformsFromTxt) || + __instance == null || __instance == PlayerControl.LocalPlayer) return; try { var clientData = AmongUsClient.Instance.GetClientFromCharacter(__instance); if (clientData == null || clientData.PlatformData == null) return; + if (ElysiumModMenuGUI.banCustomPlatformsFromTxt && + MatchesPlatformBanTxt(clientData, out string customPlatformName, out string token)) + { + HostBanForPlatform(__instance, $"Custom platform TXT match '{token}' ({customPlatformName})"); + return; + } + var platform = clientData.PlatformData; string pName = platform.PlatformName; ulong xuid = platform.XboxPlatformId; @@ -2083,7 +2244,11 @@ public static void Postfix(PlayerControl __instance) if (!isValid) { - ElysiumAnticheat.Flag(__instance, $"Platform Spoof detected ({platform.Platform})"); + string reason = $"Platform Spoof detected ({platform.Platform})"; + if (ElysiumModMenuGUI.autoBanPlatformSpoof) + HostBanForPlatform(__instance, reason); + else if (ElysiumModMenuGUI.blockSpoofRPC) + ElysiumAnticheat.Flag(__instance, reason); } } catch { } @@ -2872,12 +3037,29 @@ private static string CleanName(string value) public static bool enablePlatformSpoof = true; public static bool enableAnomalyLogReports = true; public static bool showEspFriendCode = true; + public static bool allowDuplicateColors = false; + public static bool autoGhostAfterStart = false; + public static bool autoBanPlatformSpoof = false; + public static bool banCustomPlatformsFromTxt = false; + public static int fpsLimit = 60; + public static int chatHistoryLimit = 20; public static int currentPlatformIndex = 1; private static float localNameRefreshTimer = 0f; private static float localFriendCodeRefreshTimer = 0f; + private static float platformBanScanTimer = 0f; + private static int lastAppliedFpsLimit = -1; + private static bool autoGhostAppliedThisGame = false; + private static bool wasGameStartedForAutoGhost = false; private static string originalLocalFriendCode = null; + private static string originalLocalName = null; private static float friendEspIgnoreNextLoadAt = 0f; private static readonly HashSet friendEspIgnoreTokens = new HashSet(StringComparer.OrdinalIgnoreCase); + private static string platformBanListPath = ""; + private static float platformBanListNextLoadAt = 0f; + private static readonly HashSet customPlatformBanTokens = new HashSet(StringComparer.OrdinalIgnoreCase); + private static readonly HashSet platformSpoofPunishedOwners = new HashSet(); + private const int FavoriteOutfitSlotCount = 4; + private static readonly string[] favoriteOutfitSlots = new string[FavoriteOutfitSlotCount]; private float brokenFcScanTimer = 0f; private static readonly HashSet brokenFcPunishedOwners = new HashSet(); @@ -3147,13 +3329,12 @@ private void DrawVisualsInGame() [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadMessage))] public static class Shield_PasosLimit_Patch { + private const byte DataGameDataTag = 1; private const byte RpcGameDataTag = 2; private const byte DroppedGameDataTag = 0; - private const int PasosBanPacketThreshold = 3; - private const float PasosWindow = 0.25f; private const float PasosNotifyCooldown = 2f; - private static readonly Queue emptyRpcDrops = new Queue(); - private static readonly Dictionary> pasosDropTrackers = new Dictionary>(); + private const float RpcSpamWindow = 1f; + private static readonly Dictionary> rpcSpamTrackers = new Dictionary>(); private static readonly HashSet pasosBlockedClientIds = new HashSet(); private static readonly HashSet pasosHostBannedClientIds = new HashSet(); private static float lastPasosNotify; @@ -3169,6 +3350,11 @@ public static bool IsClientBlocked(int clientId) return ElysiumModMenuGUI.enableLocalPasosBan && IsValidClientId(clientId) && pasosBlockedClientIds.Contains(clientId); } + public static bool IsPlayerBlocked(PlayerControl player) + { + return player != null && IsClientBlocked(GetKickClientId(player, -1)); + } + public static bool IsEmptyGameDataReader(MessageReader reader) { if (!ElysiumModMenuGUI.enablePasosLimit || reader == null) return false; @@ -3187,54 +3373,55 @@ public static bool IsValidClientId(int clientId) return clientId >= 0 && clientId < 256; } - public static void RecordDrop(int clientId = -1) + public static bool RecordDrop(int clientId = -1, PlayerControl player = null, string reason = "RPC spam") { float now = UnityEngine.Time.time; - while (emptyRpcDrops.Count > 0 && emptyRpcDrops.Peek() < now - PasosWindow) - emptyRpcDrops.Dequeue(); - - emptyRpcDrops.Enqueue(now); - - int resolvedClientId = IsValidClientId(clientId) ? clientId : currentPasosClientId; + int resolvedClientId = IsValidClientId(clientId) ? clientId : GetKickClientId(player, -1); + if (!IsValidClientId(resolvedClientId)) + resolvedClientId = currentPasosClientId; if (!IsValidClientId(resolvedClientId)) resolvedClientId = ResolveSingleRemoteClientId(); + if (!IsValidClientId(resolvedClientId)) + return false; - int clientDropCount = 0; - if (IsValidClientId(resolvedClientId)) + int clientDropCount = TrackRpcSpam(resolvedClientId, now); + bool overLimit = clientDropCount >= ElysiumModMenuGUI.rpcSpamLimit; + if (overLimit) { - clientDropCount = TrackPasosClientDrop(resolvedClientId, now); - if (clientDropCount >= PasosBanPacketThreshold) - BlockPasosClient(resolvedClientId, clientDropCount); + BlockPasosClient(resolvedClientId, player, clientDropCount, reason); + if (now - lastPasosNotify > PasosNotifyCooldown) + lastPasosNotify = now; } - if (emptyRpcDrops.Count >= PasosBanPacketThreshold && now - lastPasosNotify > PasosNotifyCooldown) - lastPasosNotify = now; + return overLimit || (ElysiumModMenuGUI.enableLocalPasosBan && pasosBlockedClientIds.Contains(resolvedClientId)); } - private static int TrackPasosClientDrop(int clientId, float now) + private static int TrackRpcSpam(int clientId, float now) { if (!IsValidClientId(clientId)) return 0; - if (!pasosDropTrackers.TryGetValue(clientId, out Queue drops)) + if (!rpcSpamTrackers.TryGetValue(clientId, out Queue drops)) { drops = new Queue(); - pasosDropTrackers[clientId] = drops; + rpcSpamTrackers[clientId] = drops; } - while (drops.Count > 0 && drops.Peek() < now - PasosWindow) + while (drops.Count > 0 && drops.Peek() < now - RpcSpamWindow) drops.Dequeue(); drops.Enqueue(now); return drops.Count; } - private static void BlockPasosClient(int clientId, int packetCount) + private static void BlockPasosClient(int clientId, PlayerControl player, int packetCount, string reason) { try { if (!IsValidClientId(clientId) || (AmongUsClient.Instance != null && clientId == AmongUsClient.Instance.ClientId)) return; - PlayerControl player = FindPlayerByClientId(clientId); + if (player == null) + player = FindPlayerByClientId(clientId); + string pName = player?.Data?.PlayerName ?? $"Client {clientId}"; int banClientId = GetKickClientId(player, clientId); string fc = string.IsNullOrEmpty(player?.Data?.FriendCode) ? "Unknown" : player.Data.FriendCode; @@ -3252,14 +3439,14 @@ private static void BlockPasosClient(int clientId, int packetCount) if (ElysiumModMenuGUI.enableLocalPasosBan && pasosBlockedClientIds.Add(clientId)) { - ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Local Message Block Freeze blacklist after {packetCount} packets"); + ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Local RPC drop: {reason} after {packetCount} packets/s"); } if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; if (!pasosHostBannedClientIds.Add(clientId)) return; if (!IsValidClientId(banClientId)) return; - ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Host auto-ban for Message Block Freeze empty RPC spam after {packetCount} packets"); + ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Host RPC auto-ban: {reason} after {packetCount} packets/s"); AmongUsClient.Instance.KickPlayer(banClientId, true); } catch { } @@ -3433,24 +3620,52 @@ private static int ConvertNumericClientId(object value) public static void Postfix(MessageReader __result) { - DropEmptyRpcMessage(__result); + DropSuspiciousGameDataMessage(__result); } - public static void DropEmptyRpcMessage(MessageReader reader) + public static void DropSuspiciousGameDataMessage(MessageReader reader) { if (!ElysiumModMenuGUI.enablePasosLimit || reader == null) return; try { - if (reader.Tag != RpcGameDataTag || reader.BytesRemaining > 0 || reader.Length > 0) return; + if (reader.BytesRemaining > 0 || reader.Length > 0) return; + + string reason = null; + if (reader.Tag == RpcGameDataTag) + reason = "empty StartMessage RPC"; + else if (reader.Tag == DataGameDataTag) + reason = "empty SDF/data message"; + else if (IsBadGameDataTag(reader.Tag)) + reason = $"bad data tag {reader.Tag}"; + + if (reason == null) return; reader.Tag = DroppedGameDataTag; reader.Position = reader.Length; - RecordDrop(); + RecordDrop(-1, null, reason); } catch { } } + + private static bool IsBadGameDataTag(byte tag) + { + switch (tag) + { + case DroppedGameDataTag: + case DataGameDataTag: + case RpcGameDataTag: + case 4: + case 5: + case 6: + case 7: + case 8: + return false; + default: + return true; + } + } } public static class Shield_PasosLimit_GameDataGuard @@ -3659,13 +3874,9 @@ private static int ExtractClientId(object source) [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] public static class Shield_PetSpam_Patch { - public static System.Collections.Generic.HashSet petSpamBlockedPlayers = new System.Collections.Generic.HashSet(); - - public static System.Collections.Generic.Dictionary> petSpamTrackers = new System.Collections.Generic.Dictionary>(); - public static bool Prefix(PlayerPhysics __instance, byte callId, Hazel.MessageReader reader) { - if (!ElysiumModMenuGUI.enableLocalPetSpamDrop && !ElysiumModMenuGUI.enableHostPetSpamBan) return true; + if (!ElysiumModMenuGUI.enablePasosLimit) return true; if (callId == 49 || callId == 50) { @@ -3675,51 +3886,12 @@ public static bool Prefix(PlayerPhysics __instance, byte callId, Hazel.MessageRe if (__instance.myPlayer == PlayerControl.LocalPlayer) return true; - byte pId = __instance.myPlayer.PlayerId; - - if (petSpamBlockedPlayers.Contains(pId)) - { - if (ElysiumModMenuGUI.enableLocalPetSpamDrop) return false; - } - - float now = UnityEngine.Time.time; - - if (!petSpamTrackers.ContainsKey(pId)) - petSpamTrackers[pId] = new System.Collections.Generic.Queue(); - - var q = petSpamTrackers[pId]; - - while (q.Count > 0 && q.Peek() < now - 0.75f) - q.Dequeue(); - - q.Enqueue(now); - - if (q.Count > 160) - { - petSpamBlockedPlayers.Add(pId); - - string pName = __instance.myPlayer.Data?.PlayerName ?? "Unknown"; - - if (ElysiumModMenuGUI.enableHostPetSpamBan && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - string fc = string.IsNullOrEmpty(__instance.myPlayer.Data?.FriendCode) ? "Unknown" : __instance.myPlayer.Data.FriendCode; - string puid = "Unknown"; - - try - { - var client = AmongUsClient.Instance.GetClientFromCharacter(__instance.myPlayer); - if (client != null) puid = GetClientPuid(client); - } - catch { } - - ElysiumModMenuGUI.AddToBanList(fc, puid, pName, "Auto-banned for Pet Spam"); - - AmongUsClient.Instance.KickPlayer(__instance.myPlayer.OwnerId, true); - - } + if (Shield_PasosLimit_Patch.IsPlayerBlocked(__instance.myPlayer)) + return false; + int clientId = Shield_PasosLimit_Patch.GetKickClientId(__instance.myPlayer, -1); + if (Shield_PasosLimit_Patch.RecordDrop(clientId, __instance.myPlayer, "pet RPC spam")) return false; - } } catch { } } @@ -3923,15 +4095,15 @@ private static void ApplyLocalNameSelf(string newName, bool notify = true) } string renderName = BuildLocalNameRenderText(newName); - - TryInvokeStringMethod(local, "SetName", renderName); - - try + if (originalLocalName == null) { - if (local.cosmetics != null) - local.cosmetics.SetName(renderName); + originalLocalName = local.CurrentOutfit != null && !string.IsNullOrWhiteSpace(local.CurrentOutfit.PlayerName) + ? local.CurrentOutfit.PlayerName + : local.Data?.PlayerName; } - catch { } + + if (local.cosmetics != null) + local.cosmetics.SetName(renderName); TrySetPlayerNameObject(local.Data, renderName); if (local.Data != null) @@ -3946,6 +4118,32 @@ private static void ApplyLocalNameSelf(string newName, bool notify = true) catch { } } + private static void RestoreLocalNameSelf() + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null || local.cosmetics == null) return; + + string baseName = !string.IsNullOrWhiteSpace(originalLocalName) + ? originalLocalName + : (local.Data?.PlayerName ?? local.CurrentOutfit?.PlayerName); + if (!string.IsNullOrWhiteSpace(baseName)) + { + local.cosmetics.SetName(baseName); + TrySetPlayerNameObject(local.Data, baseName); + if (local.Data != null) + { + TrySetPlayerNameObject(local.Data.DefaultOutfit, baseName); + TrySetPlayerNameObject(local.CurrentOutfit, baseName); + } + } + + originalLocalName = null; + } + catch { } + } + private static void ApplyLocalFriendCodeSelf(string fakeFriendCode, bool notify = true) { try @@ -4079,6 +4277,8 @@ private void SaveConfig() Plugin.MenuKeybind.Value = menuToggleKey; PlayerPrefs.SetInt("M_MenuToggleKey", (int)menuToggleKey); SaveBool("M_WhiteTheme", whiteMenuTheme); + PlayerPrefs.SetInt("M_MenuLanguageIndex", currentMenuLanguageIndex); + PlayerPrefs.SetInt("M_FpsLimit", fpsLimit); SaveBool("M_EnableBackground", enableBackground); SaveBool("M_EnableCustomNotifs", EnableCustomNotifs); SaveBool("M_LogAllRPCs", LogAllRPCs); @@ -4142,6 +4342,7 @@ private void SaveConfig() SaveBool("M_EnableFastChat", enableFastChat); SaveBool("M_AllowLinksAndSymbols", allowLinksAndSymbols); SaveBool("M_EnableChatHistory", enableChatHistory); + PlayerPrefs.SetInt("M_ChatHistoryLimit", chatHistoryLimit); SaveBool("M_EnableClipboard", enableClipboard); SaveBool("M_EnableChatLog", enableChatLog); SaveBool("M_EnableColorCommand", enableColorCommand); @@ -4161,12 +4362,16 @@ private void SaveConfig() SaveBool("M_RemovePenalty", removePenalty); SaveBool("M_AlwaysShowLobbyTimer", alwaysShowLobbyTimer); SaveBool("M_AutoBanEnabled", autoBanEnabled); + SaveBool("M_AllowDuplicateColors", allowDuplicateColors); SaveBool("M_BlockSpoofRPC", blockSpoofRPC); + SaveBool("M_AutoBanPlatformSpoof", autoBanPlatformSpoof); + SaveBool("M_BanCustomPlatformsFromTxt", banCustomPlatformsFromTxt); SaveBool("M_BlockSabotageRPC", blockSabotageRPC); SaveBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); SaveBool("M_PasosLimit", enablePasosLimit); + PlayerPrefs.SetInt("M_RpcSpamLimit", rpcSpamLimit); SaveBool("M_AntiPasosLocalBan", enableLocalPasosBan); SaveBool("M_AntiPasosHostBan", enableHostPasosBan); SaveBool("M_AutoHostEnabled", AutoHostEnabled); @@ -4176,6 +4381,7 @@ private void SaveConfig() SaveBool("M_AutoHostWaitLoadedPlayers", AutoHostWaitLoadedPlayers); SaveBool("M_AutoHostCancelBelowMin", AutoHostCancelBelowMin); SaveBool("M_AutoHostInstantStart", AutoHostInstantStart); + SaveBool("M_AutoGhostAfterStart", autoGhostAfterStart); PlayerPrefs.SetInt("M_AutoHostMinPlayers", AutoHostMinPlayers); PlayerPrefs.SetFloat("M_AutoHostStartDelaySeconds", AutoHostStartDelaySeconds); PlayerPrefs.SetInt("M_AutoHostFastStartPlayers", AutoHostFastStartPlayers); @@ -4186,6 +4392,8 @@ private void SaveConfig() Plugin.MenuConfig.Save(); PlayerPrefs.SetString("M_SpoofName", customNameInput); + for (int i = 0; i < favoriteOutfitSlots.Length; i++) + PlayerPrefs.SetString($"M_FavoriteOutfit_{i}", favoriteOutfitSlots[i] ?? string.Empty); PlayerPrefs.Save(); } catch { } @@ -4211,6 +4419,8 @@ private void DrawAutoHostTab() GUILayout.Space(5); AutoHostInstantStart = DrawToggle(AutoHostInstantStart, L("Instant Start (No 5s Wait)", "Мгновенный старт (Без 5с)"), 250); GUILayout.Space(5); + autoGhostAfterStart = DrawToggle(autoGhostAfterStart, L("Auto Ghost After Start", "Авто-призрак после старта"), 250); + GUILayout.Space(5); AutoHostForceLastMinute = DrawToggle(AutoHostForceLastMinute, L("Force Start Last Minute", "Форс-старт на последней минуте"), 250); GUILayout.Space(15); @@ -4264,6 +4474,9 @@ private void LoadConfig() currentMenuColorIndex = Plugin.MenuColorIndexConfig.Value; rgbMenuMode = Plugin.RgbMenuModeConfig.Value; whiteMenuTheme = LoadBool("M_WhiteTheme", whiteMenuTheme); + currentMenuLanguageIndex = Mathf.Clamp(LoadInt("M_MenuLanguageIndex", currentMenuLanguageIndex), 0, menuLanguageNames.Length - 1); + fpsLimit = Mathf.Clamp(LoadInt("M_FpsLimit", fpsLimit), 60, 240); + ApplyFpsLimit(); autoKickBugs = LoadBool("M_AutoKickBugs", autoKickBugs); if (PlayerPrefs.HasKey("M_AutoKickTimer")) autoKickTimer = PlayerPrefs.GetFloat("M_AutoKickTimer"); disableVoteKicks = LoadBool("M_DisableVoteKicks", disableVoteKicks); @@ -4319,6 +4532,7 @@ private void LoadConfig() enableFastChat = LoadBool("M_EnableFastChat", enableFastChat); allowLinksAndSymbols = LoadBool("M_AllowLinksAndSymbols", allowLinksAndSymbols); enableChatHistory = LoadBool("M_EnableChatHistory", enableChatHistory); + chatHistoryLimit = Mathf.Clamp(LoadInt("M_ChatHistoryLimit", chatHistoryLimit), 5, 80); enableClipboard = LoadBool("M_EnableClipboard", enableClipboard); enableChatLog = LoadBool("M_EnableChatLog", enableChatLog); enableColorCommand = LoadBool("M_EnableColorCommand", enableColorCommand); @@ -4338,12 +4552,16 @@ private void LoadConfig() removePenalty = LoadBool("M_RemovePenalty", removePenalty); alwaysShowLobbyTimer = LoadBool("M_AlwaysShowLobbyTimer", alwaysShowLobbyTimer); autoBanEnabled = LoadBool("M_AutoBanEnabled", autoBanEnabled); + allowDuplicateColors = LoadBool("M_AllowDuplicateColors", allowDuplicateColors); blockSpoofRPC = LoadBool("M_BlockSpoofRPC", blockSpoofRPC); + autoBanPlatformSpoof = LoadBool("M_AutoBanPlatformSpoof", autoBanPlatformSpoof); + banCustomPlatformsFromTxt = LoadBool("M_BanCustomPlatformsFromTxt", banCustomPlatformsFromTxt); blockSabotageRPC = LoadBool("M_BlockSabotageRPC", blockSabotageRPC); blockGameRpcInLobby = LoadBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); enablePasosLimit = LoadBool("M_PasosLimit", enablePasosLimit); + if (PlayerPrefs.HasKey("M_RpcSpamLimit")) rpcSpamLimit = Mathf.Clamp(PlayerPrefs.GetInt("M_RpcSpamLimit"), 10, 250); enableLocalPasosBan = LoadBool("M_AntiPasosLocalBan", enableLocalPasosBan); enableHostPasosBan = LoadBool("M_AntiPasosHostBan", enableHostPasosBan); AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); @@ -4353,12 +4571,15 @@ private void LoadConfig() AutoHostWaitLoadedPlayers = LoadBool("M_AutoHostWaitLoadedPlayers", AutoHostWaitLoadedPlayers); AutoHostCancelBelowMin = LoadBool("M_AutoHostCancelBelowMin", AutoHostCancelBelowMin); AutoHostInstantStart = LoadBool("M_AutoHostInstantStart", AutoHostInstantStart); + autoGhostAfterStart = LoadBool("M_AutoGhostAfterStart", autoGhostAfterStart); if (PlayerPrefs.HasKey("M_AutoHostMinPlayers")) AutoHostMinPlayers = PlayerPrefs.GetInt("M_AutoHostMinPlayers"); if (PlayerPrefs.HasKey("M_AutoHostStartDelaySeconds")) AutoHostStartDelaySeconds = PlayerPrefs.GetFloat("M_AutoHostStartDelaySeconds"); if (PlayerPrefs.HasKey("M_AutoHostFastStartPlayers")) AutoHostFastStartPlayers = PlayerPrefs.GetInt("M_AutoHostFastStartPlayers"); if (PlayerPrefs.HasKey("M_AutoHostFastStartDelaySeconds")) AutoHostFastStartDelaySeconds = PlayerPrefs.GetFloat("M_AutoHostFastStartDelaySeconds"); if (PlayerPrefs.HasKey("M_WalkSpeed")) walkSpeed = PlayerPrefs.GetFloat("M_WalkSpeed"); if (PlayerPrefs.HasKey("M_EngineSpeed")) engineSpeed = PlayerPrefs.GetFloat("M_EngineSpeed"); + for (int i = 0; i < favoriteOutfitSlots.Length; i++) + favoriteOutfitSlots[i] = PlayerPrefs.GetString($"M_FavoriteOutfit_{i}", string.Empty); enableBackground = LoadBool("M_EnableBackground", enableBackground); EnableCustomNotifs = LoadBool("M_EnableCustomNotifs", EnableCustomNotifs); LogAllRPCs = LoadBool("M_LogAllRPCs", LogAllRPCs); @@ -4384,6 +4605,32 @@ private void LoadConfig() catch { } } + private static void ApplyFpsLimit() + { + try + { + fpsLimit = Mathf.Clamp(fpsLimit, 60, 240); + if (lastAppliedFpsLimit == fpsLimit) return; + Application.targetFrameRate = fpsLimit; + QualitySettings.vSyncCount = 0; + lastAppliedFpsLimit = fpsLimit; + } + catch { } + } + + private static void TrimChatHistoryToLimit() + { + try + { + chatHistoryLimit = Mathf.Clamp(chatHistoryLimit, 5, 80); + while (ChatHistory.sentMessages.Count > chatHistoryLimit) + ChatHistory.sentMessages.RemoveAt(0); + + ChatHistory.HistoryIndex = Mathf.Clamp(ChatHistory.HistoryIndex, 0, ChatHistory.sentMessages.Count); + } + catch { } + } + private static void SyncKeybindDictionary() { try @@ -5015,6 +5262,25 @@ private void DrawGeneralInfoTab() GUILayout.EndHorizontal(); GUILayout.Space(10); + GUILayout.BeginHorizontal(); + GUILayout.Label(L("Menu language:", "Язык меню:"), toggleLabelStyle, GUILayout.Width(110)); + if (GUILayout.Button("<", btnStyle, GUILayout.Width(26), GUILayout.Height(24))) + { + currentMenuLanguageIndex--; + if (currentMenuLanguageIndex < 0) currentMenuLanguageIndex = menuLanguageNames.Length - 1; + SaveConfig(); + } + GUILayout.Label(menuLanguageNames[Mathf.Clamp(currentMenuLanguageIndex, 0, menuLanguageNames.Length - 1)], new GUIStyle(btnStyle) { normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, fontStyle = FontStyle.Bold }, GUILayout.Width(132), GUILayout.Height(24)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(26), GUILayout.Height(24))) + { + currentMenuLanguageIndex++; + if (currentMenuLanguageIndex >= menuLanguageNames.Length) currentMenuLanguageIndex = 0; + SaveConfig(); + } + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Space(8); + string accentHex = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); string githubHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(26, 188, 156, 255)) : new Color32(26, 188, 156, 255)); string goldHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(255, 187, 54, 255)) : new Color32(255, 187, 54, 255)); @@ -5318,6 +5584,12 @@ private void DrawChatSettingsCompact() GUILayout.Label(L("CHAT UTILITY", "УТИЛИТЫ ЧАТА"), headerStyle); enableChatHistory = DrawToggle(enableChatHistory, L("Chat History", "История чата"), 230); GUILayout.Space(3); + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("History:", "История:")} {chatHistoryLimit}", toggleLabelStyle, GUILayout.Width(92)); + chatHistoryLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(chatHistoryLimit, 5f, 80f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)), 5, 80); + TrimChatHistoryToLimit(); + GUILayout.EndHorizontal(); + GUILayout.Space(3); enableClipboard = DrawToggle(enableClipboard, L("Clipboard", "Буфер обмена"), 230); GUILayout.Space(3); enableChatLog = DrawToggle(enableChatLog, L("Save Chat Log", "Сохранять лог чата"), 230); @@ -5426,17 +5698,22 @@ private void DrawChatSettingsCompact() private void DrawGhostChatColorControl(float width) { GUILayout.BeginHorizontal(GUILayout.Width(width)); - GUILayout.Label(L("Ghost Chat:", "Ghost Chat:"), new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(88)); + GUILayout.Label(L("Ghost Chat:", "Ghost Chat:"), new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(74)); if (DrawPseudoInputButton(ghostChatColorHex, isEditingGhostChatColor, 24f, 16)) { isEditingGhostChatColor = !isEditingGhostChatColor; + if (isEditingGhostChatColor) + { + ghostChatColorHex = FilterHexInput(ghostChatColorHex, 7); + } isEditingName = false; isEditingLevel = false; isEditingFriendCode = false; isEditingLocalFriendCode = false; isEditingBan = false; + ResetAllBindWaits(); } - if (GUILayout.Button(L("Apply", "OK"), btnStyle, GUILayout.Width(52), GUILayout.Height(24))) + if (GUILayout.Button(L("Apply", "OK"), btnStyle, GUILayout.Width(48), GUILayout.Height(24))) { isEditingGhostChatColor = false; ghostChatColorHex = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); @@ -5445,7 +5722,7 @@ private void DrawGhostChatColorControl(float width) GUILayout.EndHorizontal(); string previewHex = GetGhostChatColorHex(); - GUILayout.Label($"{L("Preview ghost chat color", "Пример цвета чата призраков")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11 }, GUILayout.Width(width)); + GUILayout.Label($"{L("Preview ghost chat color", "Пример цвета чата призраков")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = false, clipping = TextClipping.Clip }, GUILayout.Width(width), GUILayout.Height(16f)); } private void DrawPlayerMovement() @@ -5554,8 +5831,39 @@ private static string SanitizeHexColor(string input, string fallback) return clean.Length == 6 ? "#" + clean : fallback; } + private static string FilterHexInput(string input, int maxChars) + { + string value = (input ?? string.Empty).Trim(); + string clean = ""; + bool hasHash = false; + + foreach (char c in value) + { + if (c == '#' && clean.Length == 0 && !hasHash) + { + hasHash = true; + clean = "#"; + continue; + } + + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) + { + if (clean.Length == 0) clean = "#"; + clean += char.ToUpperInvariant(c); + if (clean.Length >= maxChars) break; + } + } + + return clean.Length == 0 ? "#" : clean; + } + public static string GetGhostChatColorHex() { + if (isEditingGhostChatColor) + { + return SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); + } + ghostChatColorHex = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); return ghostChatColorHex; } @@ -5746,6 +6054,217 @@ private void TryAutoBanBrokenFriendCodeTick() catch { } } + private static void TryAutoGhostAfterStartTick() + { + try + { + bool gameStarted = AmongUsClient.Instance != null && AmongUsClient.Instance.IsGameStarted; + if (!gameStarted) + { + wasGameStartedForAutoGhost = false; + autoGhostAppliedThisGame = false; + return; + } + + if (!wasGameStartedForAutoGhost) + { + wasGameStartedForAutoGhost = true; + autoGhostAppliedThisGame = false; + } + + if (!autoGhostAfterStart || autoGhostAppliedThisGame || PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) + return; + + if (PlayerControl.LocalPlayer.Data.IsDead) + { + autoGhostAppliedThisGame = true; + return; + } + + MakePlayerGhost(PlayerControl.LocalPlayer, false, false); + autoGhostAppliedThisGame = true; + ShowNotification($"[AUTO HOST] {L("Auto ghost applied.", "Авто-призрак применен.")}"); + } + catch { } + } + + private static void EnsurePlatformBanListLoaded() + { + try + { + if (string.IsNullOrEmpty(platformBanListPath)) + platformBanListPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ElysiumPlatformBanList.txt"); + + if (!System.IO.File.Exists(platformBanListPath)) + System.IO.File.WriteAllText(platformBanListPath, "# One custom platform token per line. Matching PlatformName values are host-banned when enabled.\n# Example: github\n"); + + if (Time.unscaledTime < platformBanListNextLoadAt) return; + platformBanListNextLoadAt = Time.unscaledTime + 3f; + + customPlatformBanTokens.Clear(); + foreach (string rawLine in System.IO.File.ReadAllLines(platformBanListPath)) + { + string line = rawLine.Trim(); + if (line.Length == 0 || line.StartsWith("#")) continue; + customPlatformBanTokens.Add(line); + } + } + catch { } + } + + private static bool IsCustomPlatformName(ClientData client, out string platformName) + { + platformName = ""; + try + { + if (client == null || client.PlatformData == null) return false; + platformName = client.PlatformData.PlatformName ?? ""; + if (string.IsNullOrWhiteSpace(platformName)) return false; + + string enumName = client.PlatformData.Platform.ToString(); + if (platformName.Equals("TESTNAME", StringComparison.OrdinalIgnoreCase)) return false; + return !platformName.Equals(enumName, StringComparison.OrdinalIgnoreCase) && + !platformName.Equals(GetPlatform(client), StringComparison.OrdinalIgnoreCase); + } + catch { } + + return false; + } + + private static bool IsInvalidPlatformData(ClientData client, out string reason) + { + reason = ""; + try + { + if (client == null || client.PlatformData == null) return false; + + var platform = client.PlatformData; + string pName = platform.PlatformName ?? ""; + ulong xuid = platform.XboxPlatformId; + ulong psid = platform.PsnPlatformId; + bool isValid = true; + + switch (platform.Platform) + { + case Platforms.StandaloneEpicPC: + case Platforms.StandaloneSteamPC: + case Platforms.StandaloneMac: + case Platforms.StandaloneItch: + case Platforms.IPhone: + case Platforms.Android: + isValid = (pName == "TESTNAME" && xuid == 0 && psid == 0); + break; + case Platforms.StandaloneWin10: + isValid = (pName == "TESTNAME" && xuid != 0 && psid == 0); + break; + case Platforms.Xbox: + isValid = (pName != "TESTNAME" && pName.Length >= 3 && xuid != 0 && psid == 0); + break; + case Platforms.Playstation: + isValid = (pName != "TESTNAME" && xuid == 0 && psid != 0); + break; + case Platforms.Switch: + isValid = (pName != "TESTNAME" && xuid == 0 && psid == 0); + break; + } + + if (!isValid) + { + reason = $"Platform Spoof detected ({platform.Platform})"; + return true; + } + } + catch { } + + return false; + } + + private static bool MatchesPlatformBanTxt(ClientData client, out string platformName, out string matchedToken) + { + platformName = ""; + matchedToken = ""; + EnsurePlatformBanListLoaded(); + + if (!IsCustomPlatformName(client, out platformName) || customPlatformBanTokens.Count == 0) + return false; + + foreach (string token in customPlatformBanTokens) + { + if (platformName.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0) + { + matchedToken = token; + return true; + } + } + + return false; + } + + private static void HostBanForPlatform(PlayerControl player, string reason) + { + try + { + if (player == null || player == PlayerControl.LocalPlayer || player.Data == null || + AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + return; + + int owner = (int)player.OwnerId; + if (platformSpoofPunishedOwners.Contains(owner)) return; + platformSpoofPunishedOwners.Add(owner); + + string name = string.IsNullOrWhiteSpace(player.Data.PlayerName) ? "Unknown" : player.Data.PlayerName; + string fc = string.IsNullOrWhiteSpace(player.Data.FriendCode) ? "Unknown" : player.Data.FriendCode; + string puid = "Unknown"; + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(player); + if (client != null) puid = GetClientPuid(client); + } + catch { } + + AddToBanList(fc, puid, name, reason); + AmongUsClient.Instance.KickPlayer(owner, true); + ShowNotification($"[PLATFORM BAN] {name}: {reason}"); + } + catch { } + } + + private static void TryAutoBanCustomPlatformsTick() + { + try + { + if ((!autoBanPlatformSpoof && !banCustomPlatformsFromTxt) || + AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) + { + platformBanScanTimer = 0f; + return; + } + + platformBanScanTimer += Time.deltaTime; + if (platformBanScanTimer < 1f) return; + platformBanScanTimer = 0f; + + foreach (PlayerControl pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; + + ClientData client = null; + try { client = AmongUsClient.Instance.GetClientFromCharacter(pc); } catch { } + if (client == null) continue; + + if (banCustomPlatformsFromTxt && MatchesPlatformBanTxt(client, out string platformName, out string token)) + { + HostBanForPlatform(pc, $"Custom platform TXT match '{token}' ({platformName})"); + continue; + } + + if (autoBanPlatformSpoof && IsInvalidPlatformData(client, out string reason)) + HostBanForPlatform(pc, reason); + } + } + catch { } + } + private void DrawSelfSpoof() { GUILayout.BeginVertical(boxStyle); @@ -5788,6 +6307,7 @@ private void DrawSelfSpoof() { enableLocalNameSpoof = newLocalNameToggle; if (enableLocalNameSpoof) ApplyLocalNameSelf(customNameInput, false); + else RestoreLocalNameSelf(); SaveConfig(); } GUILayout.Space(2); @@ -7302,6 +7822,18 @@ private void DrawMenuTab() GUILayout.Space(5); GUILayout.Label("Put 'MenuBG.png' or .jpg in BepInEx/config to add a background image.", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11 }); + GUILayout.Space(10); + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("FPS Limit", "Лимит FPS")}: {fpsLimit}", new GUIStyle(toggleLabelStyle) { richText = true }, GUILayout.Width(125)); + int newFpsLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(fpsLimit, 60f, 240f, sliderStyle, sliderThumbStyle, GUILayout.Width(210)), 60, 240); + if (newFpsLimit != fpsLimit) + { + fpsLimit = newFpsLimit; + ApplyFpsLimit(); + menuPrefsChanged = true; + } + GUILayout.EndHorizontal(); + GUILayout.Space(10); GUILayout.BeginHorizontal(); @@ -7370,9 +7902,63 @@ private void DrawMenuTab() private int currentAutoHostSubTab = 0; private string[] autoHostSubTabs = { "LOBBY CONTROLS", "ROLE MANAGER", "ANTI CHEAT", "AUTO HOST" }; + + private struct FavoriteOutfitSnapshot + { + public int ColorId; + public string HatId; + public string SkinId; + public string VisorId; + public string NamePlateId; + public string PetId; + + public FavoriteOutfitSnapshot(int colorId, string hatId, string skinId, string visorId, string namePlateId, string petId) + { + ColorId = colorId; + HatId = hatId ?? string.Empty; + SkinId = skinId ?? string.Empty; + VisorId = visorId ?? string.Empty; + NamePlateId = namePlateId ?? string.Empty; + PetId = petId ?? string.Empty; + } + } + private void DrawOutfitsTab() { GUILayout.BeginVertical(boxStyle); + GUILayout.Label(L("FAVORITE OUTFITS", "ИЗБРАННЫЕ ОБРАЗЫ"), headerStyle); + + PlayerControl selected = SelectedOutfitSourcePlayer(); + for (int i = 0; i < FavoriteOutfitSlotCount; i++) + { + bool hasOutfit = TryDeserializeFavoriteOutfit(favoriteOutfitSlots[i], out FavoriteOutfitSnapshot outfit); + GUILayout.BeginVertical(boxStyle); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Slot", "Слот")} {i + 1}", toggleLabelStyle, GUILayout.Width(52), GUILayout.Height(22)); + GUILayout.Label(hasOutfit ? FavoriteOutfitSummary(outfit) : L("Empty", "Пусто"), new GUIStyle(GUI.skin.label) { fontSize = 11, clipping = TextClipping.Clip, alignment = TextAnchor.MiddleLeft }, GUILayout.ExpandWidth(true), GUILayout.Height(22)); + GUI.enabled = hasOutfit; + if (GUILayout.Button(L("Apply", "Надеть"), btnStyle, GUILayout.Width(58), GUILayout.Height(22))) + ApplyFavoriteOutfitSlot(i, outfit, hasOutfit); + GUI.enabled = true; + if (GUILayout.Button("X", btnStyle, GUILayout.Width(28), GUILayout.Height(22))) + ClearFavoriteOutfitSlot(i); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + GUILayout.Space(52); + if (GUILayout.Button(L("Save Mine", "Сохр. мой"), btnStyle, GUILayout.Width(100), GUILayout.Height(22))) + SaveFavoriteOutfitSlot(i, PlayerControl.LocalPlayer); + if (GUILayout.Button(L("Save Selected", "Сохр. выбран"), btnStyle, GUILayout.Width(120), GUILayout.Height(22))) + SaveFavoriteOutfitSlot(i, selected); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + GUILayout.Space(4); + } + + GUILayout.Space(12); GUILayout.Label("COPY SPECIFIC PLAYER", headerStyle); outfitsScrollPos = GUILayout.BeginScrollView(outfitsScrollPos); @@ -7412,6 +7998,145 @@ private void DrawOutfitsTab() GUILayout.EndScrollView(); GUILayout.EndVertical(); } + + private static PlayerControl SelectedOutfitSourcePlayer() + { + try + { + if (lockedPlayersList != null) + { + foreach (PlayerControl pc in lockedPlayersList) + { + if (pc != null && pc != PlayerControl.LocalPlayer && pc.Data != null && !pc.Data.Disconnected) + return pc; + } + } + } + catch { } + + return PlayerControl.LocalPlayer; + } + + private static int MaxOutfitColorId() + { + try { return Palette.PlayerColors != null ? Mathf.Max(0, Palette.PlayerColors.Length - 1) : 18; } + catch { return 18; } + } + + private static bool TryCaptureFavoriteOutfit(PlayerControl source, out FavoriteOutfitSnapshot outfit) + { + outfit = default; + try + { + if (source == null || source.Data == null || source.Data.DefaultOutfit == null) return false; + var sourceOutfit = source.Data.DefaultOutfit; + outfit = new FavoriteOutfitSnapshot( + Mathf.Clamp(sourceOutfit.ColorId, 0, MaxOutfitColorId()), + sourceOutfit.HatId, + sourceOutfit.SkinId, + sourceOutfit.VisorId, + sourceOutfit.NamePlateId, + sourceOutfit.PetId); + return true; + } + catch { } + + return false; + } + + private static void ApplyFavoriteOutfit(PlayerControl target, FavoriteOutfitSnapshot outfit) + { + if (target == null) return; + target.RpcSetColor((byte)Mathf.Clamp(outfit.ColorId, 0, MaxOutfitColorId())); + target.RpcSetSkin(outfit.SkinId ?? string.Empty); + target.RpcSetHat(outfit.HatId ?? string.Empty); + target.RpcSetVisor(outfit.VisorId ?? string.Empty); + target.RpcSetNamePlate(outfit.NamePlateId ?? string.Empty); + target.RpcSetPet(outfit.PetId ?? string.Empty); + } + + private static string SerializeFavoriteOutfit(FavoriteOutfitSnapshot outfit) + { + return string.Join("\t", new[] + { + Mathf.Clamp(outfit.ColorId, 0, MaxOutfitColorId()).ToString(), + CleanFavoriteOutfitPart(outfit.HatId), + CleanFavoriteOutfitPart(outfit.SkinId), + CleanFavoriteOutfitPart(outfit.VisorId), + CleanFavoriteOutfitPart(outfit.NamePlateId), + CleanFavoriteOutfitPart(outfit.PetId) + }); + } + + private static bool TryDeserializeFavoriteOutfit(string value, out FavoriteOutfitSnapshot outfit) + { + outfit = default; + if (string.IsNullOrWhiteSpace(value)) return false; + + string[] parts = value.Split('\t'); + if (parts.Length < 6 || !int.TryParse(parts[0], out int colorId)) return false; + + outfit = new FavoriteOutfitSnapshot(Mathf.Clamp(colorId, 0, MaxOutfitColorId()), parts[1], parts[2], parts[3], parts[4], parts[5]); + return true; + } + + private static string CleanFavoriteOutfitPart(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value.Replace("\t", " ").Replace("\r", " ").Replace("\n", " ").Trim(); + } + + private static string FavoriteOutfitSummary(FavoriteOutfitSnapshot outfit) + { + string color = "Color " + outfit.ColorId; + try { color = Palette.GetColorName(outfit.ColorId); } catch { } + return $"{color} | {ShortOutfitId(outfit.HatId)}"; + } + + private static string ShortOutfitId(string value) + { + if (string.IsNullOrWhiteSpace(value)) return "-"; + string cleaned = value.Trim(); + return cleaned.Length <= 10 ? cleaned : cleaned.Substring(0, 10); + } + + private void SaveFavoriteOutfitSlot(int index, PlayerControl source) + { + if (index < 0 || index >= favoriteOutfitSlots.Length) return; + if (!TryCaptureFavoriteOutfit(source, out FavoriteOutfitSnapshot outfit)) + { + ShowNotification($"[OUTFIT] {L("Player outfit is not ready.", "Образ игрока еще не готов.")}"); + return; + } + + favoriteOutfitSlots[index] = SerializeFavoriteOutfit(outfit); + SaveConfig(); + ShowNotification($"[OUTFIT] {L("Saved slot", "Сохранен слот")} {index + 1}"); + } + + private void ApplyFavoriteOutfitSlot(int index, FavoriteOutfitSnapshot outfit, bool hasOutfit) + { + if (!hasOutfit) + { + ShowNotification($"[OUTFIT] {L("Slot is empty.", "Слот пуст.")}"); + return; + } + + try + { + ApplyFavoriteOutfit(PlayerControl.LocalPlayer, outfit); + ShowNotification($"[OUTFIT] {L("Applied slot", "Надет слот")} {index + 1}"); + } + catch { } + } + + private void ClearFavoriteOutfitSlot(int index) + { + if (index < 0 || index >= favoriteOutfitSlots.Length) return; + favoriteOutfitSlots[index] = string.Empty; + SaveConfig(); + ShowNotification($"[OUTFIT] {L("Cleared slot", "Очищен слот")} {index + 1}"); + } public static bool removePenalty = true; public static bool alwaysShowLobbyTimer = false; public static bool enableChatLog = true; @@ -8074,6 +8799,9 @@ public void Update() ElysiumAutoHostService.Tick(); ElysiumAutoLobbyReturn.UpdateLogic(); + ApplyFpsLimit(); + TryAutoGhostAfterStartTick(); + TryAutoBanCustomPlatformsTick(); TrySendDiscordLaunchStatusTick(); TryDetectLogBurstTick(); if (votekickEveryone) @@ -8518,7 +9246,7 @@ public void OnGUI() else if (isEditingLevel && HandleClipboardShortcut(e, ref spoofLevelString)) { } else if (isEditingFriendCode && HandleClipboardShortcut(e, ref spoofFriendCodeInput)) { } else if (isEditingLocalFriendCode && HandleClipboardShortcut(e, ref localFriendCodeInput)) { } - else if (isEditingGhostChatColor && HandleClipboardShortcut(e, ref ghostChatColorHex, 7)) { } + else if (isEditingGhostChatColor && HandleClipboardShortcut(e, ref ghostChatColorHex, 7)) { ghostChatColorHex = FilterHexInput(ghostChatColorHex, 7); } else if (e.keyCode == KeyCode.Backspace) { if (isEditingBan && banInput.Length > 0) { banInput = banInput.Substring(0, banInput.Length - 1); } @@ -8536,7 +9264,7 @@ public void OnGUI() if (isEditingLevel) { spoofLevelString += e.character; } if (isEditingFriendCode) { spoofFriendCodeInput += e.character; } if (isEditingLocalFriendCode) { localFriendCodeInput += e.character; } - if (isEditingGhostChatColor && ghostChatColorHex.Length < 7) { ghostChatColorHex += e.character; } + if (isEditingGhostChatColor) { ghostChatColorHex = FilterHexInput((ghostChatColorHex ?? "") + e.character, 7); } e.Use(); } } @@ -8970,6 +9698,7 @@ private void DrawElysiumModMenu(int windowID) public static bool blockChatFloodRpc = true; public static bool blockMeetingFloodRpc = true; public static bool enablePasosLimit = true; + public static int rpcSpamLimit = 80; public static bool enableLocalPasosBan = true; public static bool enableHostPasosBan = true; public static bool autoBanBrokenFriendCode = false; @@ -9034,6 +9763,8 @@ public static void Remember(string message) if (isNewEntry) { sentMessages.Add(message); + while (sentMessages.Count > ElysiumModMenuGUI.chatHistoryLimit) + sentMessages.RemoveAt(0); } HistoryIndex = sentMessages.Count; } @@ -9450,80 +10181,87 @@ private void DrawLobbyControls() GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(GUILayout.Width(280)); + GUILayout.BeginVertical(boxStyle, GUILayout.Width(292)); + GUILayout.Label("GAME RULES", headerStyle); neverEndGame = DrawToggle(neverEndGame, "Unlimited Game", 250); GUILayout.Space(5); noSettingLimit = DrawToggle(noSettingLimit, "No Setting Limit", 250); GUILayout.Space(5); noTaskMode = DrawToggle(noTaskMode, "No Task Mode", 250); GUILayout.Space(5); + allowDuplicateColors = DrawToggle(allowDuplicateColors, L("Allow Duplicate Colors", "Разрешить одинаковые цвета"), 250); + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.BeginVertical(boxStyle, GUILayout.Width(292)); + GUILayout.Label("CHAT MODERATION", headerStyle); enableColorCommand = DrawToggle(enableColorCommand, "Enable /c command (Public)", 250); GUILayout.Space(5); blockFortegreenChat = DrawToggle(blockFortegreenChat, "Block Fortegreen Chat", 250); GUILayout.Space(5); blockRainbowChat = DrawToggle(blockRainbowChat, "Block Rainbow Chat", 250); GUILayout.Space(5); - autoChatEveryone = DrawToggle(autoChatEveryone, "Chat Everyone (Auto-Meeting)", 250); if (autoChatEveryone) { GUILayout.BeginHorizontal(); - GUILayout.Label($"Delay: {autoChatEveryoneDelay:0.0}s", toggleLabelStyle, GUILayout.Width(95)); - autoChatEveryoneDelay = GUILayout.HorizontalSlider(autoChatEveryoneDelay, 0f, 10f, sliderStyle, sliderThumbStyle, GUILayout.Width(240)); + GUILayout.Label($"Delay: {autoChatEveryoneDelay:0.0}s", toggleLabelStyle, GUILayout.Width(78)); + autoChatEveryoneDelay = GUILayout.HorizontalSlider(autoChatEveryoneDelay, 0f, 10f, sliderStyle, sliderThumbStyle, GUILayout.Width(170)); GUILayout.EndHorizontal(); } - GUILayout.EndVertical(); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); - GUILayout.Space(15); - GUILayout.Label("HOST ACTIONS", headerStyle); + GUILayout.Space(10); GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(GUILayout.Width(280)); - if (GUILayout.Button("Insta Start", btnStyle, GUILayout.Height(25))) + GUILayout.BeginVertical(boxStyle, GUILayout.Width(292)); + GUILayout.Label("LOBBY ACTIONS", headerStyle); + if (GUILayout.Button("Insta Start", btnStyle, GUILayout.Height(26))) { GameStartManager.Instance.startState = GameStartManager.StartingStates.Countdown; GameStartManager.Instance.countDownTimer = 0f; } GUILayout.Space(5); - if (GUILayout.Button("Close Meeting", btnStyle, GUILayout.Height(25))) MeetingHud.Instance.RpcClose(); + if (GUILayout.Button("Close Meeting", btnStyle, GUILayout.Height(26))) MeetingHud.Instance.RpcClose(); GUILayout.Space(5); GUILayout.BeginHorizontal(); - if (GUILayout.Button("Spawn Lobby", activeTabStyle, GUILayout.Height(25))) SpawnLobby(); + if (GUILayout.Button("Spawn Lobby", activeTabStyle, GUILayout.Height(26))) SpawnLobby(); GUILayout.Space(5); - if (GUILayout.Button("Despawn", btnStyle, GUILayout.Height(25))) DespawnLobby(); + if (GUILayout.Button("Despawn", btnStyle, GUILayout.Height(26))) DespawnLobby(); GUILayout.EndHorizontal(); GUILayout.Space(5); GUILayout.BeginHorizontal(); - if (GUILayout.Button("Kill All", btnStyle, GUILayout.Height(25))) KillAll(); + if (GUILayout.Button("Kill All", btnStyle, GUILayout.Height(26))) KillAll(); GUILayout.Space(5); - if (GUILayout.Button("Kick All", btnStyle, GUILayout.Height(25))) KickAll(); + if (GUILayout.Button("Kick All", btnStyle, GUILayout.Height(26))) KickAll(); GUILayout.Space(5); - if (GUILayout.Button("Mass Morph", btnStyle, GUILayout.Height(25))) this.StartCoroutine(MassMorphCoroutine().WrapToIl2Cpp()); + if (GUILayout.Button("Mass Morph", btnStyle, GUILayout.Height(26))) this.StartCoroutine(MassMorphCoroutine().WrapToIl2Cpp()); GUILayout.EndHorizontal(); GUILayout.EndVertical(); GUILayout.Space(10); - GUILayout.BeginVertical(GUILayout.Width(280)); + GUILayout.BeginVertical(boxStyle, GUILayout.Width(292)); + GUILayout.Label("END GAME", headerStyle); GUILayout.BeginHorizontal(); - if (GUILayout.Button("Crewmate Win", btnStyle, GUILayout.Height(25))) SmartEndGame("CrewWin"); + if (GUILayout.Button("Crewmate Win", btnStyle, GUILayout.Height(26))) SmartEndGame("CrewWin"); GUILayout.Space(5); - if (GUILayout.Button("Impostor Win", btnStyle, GUILayout.Height(25))) SmartEndGame("ImpWin"); + if (GUILayout.Button("Impostor Win", btnStyle, GUILayout.Height(26))) SmartEndGame("ImpWin"); GUILayout.EndHorizontal(); GUILayout.Space(5); GUILayout.BeginHorizontal(); - if (GUILayout.Button("Imp Disconnect", btnStyle, GUILayout.Height(25))) SmartEndGame("ImpDisconnect"); + if (GUILayout.Button("Imp Disconnect", btnStyle, GUILayout.Height(26))) SmartEndGame("ImpDisconnect"); GUILayout.Space(5); - if (GUILayout.Button("H&S Disconnect", activeTabStyle, GUILayout.Height(25))) SmartEndGame("HnsImpDisconnect"); + if (GUILayout.Button("H&S Disconnect", activeTabStyle, GUILayout.Height(26))) SmartEndGame("HnsImpDisconnect"); GUILayout.EndHorizontal(); GUILayout.Space(5); - if (GUILayout.Button("Force End (Impostor Disconnect)", btnStyle, GUILayout.Height(25)) && GameManager.Instance != null && AmongUsClient.Instance.AmHost) + if (GUILayout.Button("Force End (Impostor Disconnect)", btnStyle, GUILayout.Height(26)) && GameManager.Instance != null && AmongUsClient.Instance.AmHost) { bool tempNeverEnd = neverEndGame; neverEndGame = false; GameManager.Instance.RpcEndGame((GameOverReason)4, false); neverEndGame = tempNeverEnd; } GUILayout.EndVertical(); @@ -9536,6 +10274,14 @@ public static string GetESPNameTag(NetworkedPlayerInfo info, string originalName { if (info == null) return originalName; string newName = originalName; + if (enableLocalNameSpoof && + PlayerControl.LocalPlayer != null && + info.PlayerId == PlayerControl.LocalPlayer.PlayerId && + !string.IsNullOrWhiteSpace(customNameInput)) + { + newName = BuildLocalNameRenderText(customNameInput); + } + if (seeRoles && info.Role != null) { string roleName = info.Role.Role.ToString(); From c1d3031125c37d31d08221c059805d28582ef634 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Thu, 18 Jun 2026 00:10:22 +0200 Subject: [PATCH 37/39] Update elesium --- ElysiumModMenu.cs | 11890 +--------------- ElysiumModMenu.csproj | 315 + Network.cs | 768 + Plugin.cs | 179 + Utilities.cs | 53 + anticheat/Acov/AcovProfiler.cs | 48 + anticheat/Acov/Patches/AcovAccessLists.cs | 92 + anticheat/Acov/Patches/AcovBugColorGuard.cs | 63 + anticheat/Acov/Patches/AcovClientIdentity.cs | 73 + anticheat/Acov/Patches/AcovFakeMapLobby.cs | 63 + .../Acov/Patches/AcovNetPacketMonitor.cs | 64 + anticheat/Acov/Patches/AcovNotifications.cs | 66 + anticheat/Acov/Patches/AcovOption.cs | 65 + anticheat/Acov/Patches/AcovPlayerLevels.cs | 76 + anticheat/Acov/Patches/AcovPlayerLoadInfo.cs | 64 + .../Acov/Patches/AcovPlayerLoadStates.cs | 68 + anticheat/Acov/Patches/AcovPlugin.cs | 63 + .../Acov/Patches/AcovSecurityNotifications.cs | 71 + anticheat/Acov/Patches/AcovText.cs | 63 + .../Acov/Patches/CntPositionSanityPatch.cs | 102 + anticheat/Acov/Patches/HarmonyControl.cs | 64 + anticheat/Acov/Patches/KnownModRpcCatalog.cs | 68 + anticheat/Acov/Patches/KnownModRpcRule.cs | 63 + .../Patches/LobbyTeleportPositionTracker.cs | 130 + anticheat/Acov/Patches/MinLevelKickGuard.cs | 64 + anticheat/Acov/Patches/ModOptions.cs | 96 + .../Acov/Patches/NetworkInboundSenderPatch.cs | 69 + .../Acov/Patches/NetworkJoinHygienePatch.cs | 68 + .../Patches/NetworkMessageProtectionPatch.cs | 69 + .../Patches/NetworkProtectionCleanupDriver.cs | 74 + .../Patches/NetworkProtectionGuard.Floods.cs | 1459 ++ .../NetworkProtectionGuard.Messages.cs | 1184 ++ .../NetworkProtectionGuard.RpcChecks.cs | 1418 ++ .../Patches/NetworkProtectionGuard.State.cs | 1213 ++ anticheat/Acov/Patches/PlatformSpoofGuard.cs | 63 + .../Acov/Patches/PlayerRpcProtectionPatch.cs | 69 + .../Patches/ShipStatusRpcProtectionPatch.cs | 68 + .../Patches/VoteKickRpcProtectionPatch.cs | 68 + anticheat/ElysiumAnticheat.cs | 438 + features/AnimationToggles.cs | 832 ++ features/AutoHost.cs | 777 + features/ChatOptions.cs | 504 + features/EspFormatting.cs | 756 + features/FavoriteOutfitsPanel.cs | 634 + features/GeneralPanel.cs | 754 + features/KeybindPanel.cs | 918 ++ features/LevelSpoof.cs | 467 + features/LocalIdentity.cs | 833 ++ features/MenuControls.cs | 680 + features/MenuTheme.cs | 424 + features/PlayerActionPanel.cs | 425 + features/PlayerEsp.cs | 698 + features/Reports.cs | 648 + features/Votekick.cs | 636 + .../AmongUsClient_KickPlayer_BanList_Patch.cs | 71 + routines/AutoChatEveryone_Start_Patch.cs | 54 + routines/ChatController_AddChat_Patch.cs | 216 + routines/ChatController_SetVisible_Patch.cs | 49 + routines/ChatController_Update_Patch.cs | 67 + routines/DarkMode_ChatBubblePatch.cs | 62 + routines/ExtendedLobbyListPatch.cs | 91 + routines/ExtendedLobbyRefreshPatch.cs | 49 + routines/FriendCodeSpooferPatch.cs | 77 + .../GameManager_CheckTaskCompletion_Patch.cs | 54 + ...xtensions_GetAdjustedNumImpostors_Patch.cs | 55 + routines/InvertControls_Patch.cs | 104 + routines/LobbyStart_ApplyLevelSpoof.cs | 51 + ...nfo_FindAGameManager_HandleList_Postfix.cs | 51 + ...nfo_GameContainer_SetupGameInfo_Postfix.cs | 78 + routines/NumberOption_Decrease_Patch.cs | 61 + routines/NumberOption_Increase_Patch.cs | 61 + routines/NumberOption_Initialize_Patch.cs | 57 + routines/PlatformSpooferPatch.cs | 58 + routines/RPCSniffer_Patch.cs | 114 + routines/RevealVotesCleanupPatch.cs | 63 + routines/RevealVotesPatch.cs | 75 + ...Cosmetics_HatManager_Initialize_Postfix.cs | 57 + ..._PlayerPurchasesData_GetPurchase_Prefix.cs | 51 + ui/GeneralMapsChat.cs | 831 ++ ui/MenuKeybinds.cs | 621 + ui/MenuLocalization.cs | 58 + ui/MenuMaps.cs | 345 + ui/MenuRendering.cs | 950 ++ ui/MenuTranslations.cs | 70 + ui/PlayerAndSettingsState.cs | 773 + 85 files changed, 24468 insertions(+), 11883 deletions(-) create mode 100644 Network.cs create mode 100644 Plugin.cs create mode 100644 Utilities.cs create mode 100644 anticheat/Acov/AcovProfiler.cs create mode 100644 anticheat/Acov/Patches/AcovAccessLists.cs create mode 100644 anticheat/Acov/Patches/AcovBugColorGuard.cs create mode 100644 anticheat/Acov/Patches/AcovClientIdentity.cs create mode 100644 anticheat/Acov/Patches/AcovFakeMapLobby.cs create mode 100644 anticheat/Acov/Patches/AcovNetPacketMonitor.cs create mode 100644 anticheat/Acov/Patches/AcovNotifications.cs create mode 100644 anticheat/Acov/Patches/AcovOption.cs create mode 100644 anticheat/Acov/Patches/AcovPlayerLevels.cs create mode 100644 anticheat/Acov/Patches/AcovPlayerLoadInfo.cs create mode 100644 anticheat/Acov/Patches/AcovPlayerLoadStates.cs create mode 100644 anticheat/Acov/Patches/AcovPlugin.cs create mode 100644 anticheat/Acov/Patches/AcovSecurityNotifications.cs create mode 100644 anticheat/Acov/Patches/AcovText.cs create mode 100644 anticheat/Acov/Patches/CntPositionSanityPatch.cs create mode 100644 anticheat/Acov/Patches/HarmonyControl.cs create mode 100644 anticheat/Acov/Patches/KnownModRpcCatalog.cs create mode 100644 anticheat/Acov/Patches/KnownModRpcRule.cs create mode 100644 anticheat/Acov/Patches/LobbyTeleportPositionTracker.cs create mode 100644 anticheat/Acov/Patches/MinLevelKickGuard.cs create mode 100644 anticheat/Acov/Patches/ModOptions.cs create mode 100644 anticheat/Acov/Patches/NetworkInboundSenderPatch.cs create mode 100644 anticheat/Acov/Patches/NetworkJoinHygienePatch.cs create mode 100644 anticheat/Acov/Patches/NetworkMessageProtectionPatch.cs create mode 100644 anticheat/Acov/Patches/NetworkProtectionCleanupDriver.cs create mode 100644 anticheat/Acov/Patches/NetworkProtectionGuard.Floods.cs create mode 100644 anticheat/Acov/Patches/NetworkProtectionGuard.Messages.cs create mode 100644 anticheat/Acov/Patches/NetworkProtectionGuard.RpcChecks.cs create mode 100644 anticheat/Acov/Patches/NetworkProtectionGuard.State.cs create mode 100644 anticheat/Acov/Patches/PlatformSpoofGuard.cs create mode 100644 anticheat/Acov/Patches/PlayerRpcProtectionPatch.cs create mode 100644 anticheat/Acov/Patches/ShipStatusRpcProtectionPatch.cs create mode 100644 anticheat/Acov/Patches/VoteKickRpcProtectionPatch.cs create mode 100644 anticheat/ElysiumAnticheat.cs create mode 100644 features/AnimationToggles.cs create mode 100644 features/AutoHost.cs create mode 100644 features/ChatOptions.cs create mode 100644 features/EspFormatting.cs create mode 100644 features/FavoriteOutfitsPanel.cs create mode 100644 features/GeneralPanel.cs create mode 100644 features/KeybindPanel.cs create mode 100644 features/LevelSpoof.cs create mode 100644 features/LocalIdentity.cs create mode 100644 features/MenuControls.cs create mode 100644 features/MenuTheme.cs create mode 100644 features/PlayerActionPanel.cs create mode 100644 features/PlayerEsp.cs create mode 100644 features/Reports.cs create mode 100644 features/Votekick.cs create mode 100644 routines/AmongUsClient_KickPlayer_BanList_Patch.cs create mode 100644 routines/AutoChatEveryone_Start_Patch.cs create mode 100644 routines/ChatController_AddChat_Patch.cs create mode 100644 routines/ChatController_SetVisible_Patch.cs create mode 100644 routines/ChatController_Update_Patch.cs create mode 100644 routines/DarkMode_ChatBubblePatch.cs create mode 100644 routines/ExtendedLobbyListPatch.cs create mode 100644 routines/ExtendedLobbyRefreshPatch.cs create mode 100644 routines/FriendCodeSpooferPatch.cs create mode 100644 routines/GameManager_CheckTaskCompletion_Patch.cs create mode 100644 routines/IGameOptionsExtensions_GetAdjustedNumImpostors_Patch.cs create mode 100644 routines/InvertControls_Patch.cs create mode 100644 routines/LobbyStart_ApplyLevelSpoof.cs create mode 100644 routines/MoreLobbyInfo_FindAGameManager_HandleList_Postfix.cs create mode 100644 routines/MoreLobbyInfo_GameContainer_SetupGameInfo_Postfix.cs create mode 100644 routines/NumberOption_Decrease_Patch.cs create mode 100644 routines/NumberOption_Increase_Patch.cs create mode 100644 routines/NumberOption_Initialize_Patch.cs create mode 100644 routines/PlatformSpooferPatch.cs create mode 100644 routines/RPCSniffer_Patch.cs create mode 100644 routines/RevealVotesCleanupPatch.cs create mode 100644 routines/RevealVotesPatch.cs create mode 100644 routines/UnlockCosmetics_HatManager_Initialize_Postfix.cs create mode 100644 routines/UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix.cs create mode 100644 ui/GeneralMapsChat.cs create mode 100644 ui/MenuKeybinds.cs create mode 100644 ui/MenuLocalization.cs create mode 100644 ui/MenuMaps.cs create mode 100644 ui/MenuRendering.cs create mode 100644 ui/MenuTranslations.cs create mode 100644 ui/PlayerAndSettingsState.cs diff --git a/ElysiumModMenu.cs b/ElysiumModMenu.cs index aefeb85..3577954 100644 --- a/ElysiumModMenu.cs +++ b/ElysiumModMenu.cs @@ -1,26 +1,28 @@ -#nullable disable -#pragma warning disable CS0162, CS0108, CS0219 - +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 using AmongUs.Data.Player; using AmongUs.GameOptions; using AmongUs.InnerNet.GameDataMessages; using BepInEx; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; using HarmonyLib; using Hazel; using Il2CppInterop.Runtime.Attributes; using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.InteropTypes.Arrays; using InnerNet; -using ElysiumModMenu; +using RewiredConsts; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using TMPro; @@ -28,11889 +30,11 @@ using UnityEngine.AddressableAssets; using UnityEngine.Events; using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.UI; using static ElysiumModMenu.ElysiumModMenuGUI; using static Rewired.UI.ControlMapper.ControlMapper; using Color = UnityEngine.Color; using Object = UnityEngine.Object; using Vector3 = UnityEngine.Vector3; -using System.Runtime.CompilerServices; -namespace ElysiumModMenu -{ - [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.4")] - public class Plugin : BasePlugin - { - public static Plugin Instance { get; private set; } = null!; - public static string ElysiumFolder = ""; - public static ConfigFile MenuConfig; - public static ConfigEntry RpcSpoofDelayConfig; - public static ConfigEntry MenuKeybind; - public static ConfigEntry SpoofedLevel; - public static ConfigEntry EnableFriendCodeSpoofConfig; - public static ConfigEntry SpoofFriendCodeConfig; - public static ConfigEntry EnablePlatformSpoof; - public static ConfigEntry AutoBanBrokenFriendCodeConfig; - public static ConfigEntry PlatformIndex; - public static ConfigEntry ShowWatermarkConfig; - public static ConfigEntry MenuColorIndexConfig; - public static ConfigEntry RgbMenuModeConfig; - public static ConfigEntry UnlockCosmeticsConfig; - public static ConfigEntry MoreLobbyInfoConfig; - public static ConfigEntry EnableChatDarkModeConfig; - public static ConfigEntry GhostChatColorConfig; - public static ConfigEntry EnableAnomalyLogReportsConfig; - public static ConfigEntry ShowEspFriendCodeConfig; - - public override void Load() - { - Instance = this; - - ElysiumFolder = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu"); - if (!System.IO.Directory.Exists(ElysiumFolder)) - { - System.IO.Directory.CreateDirectory(ElysiumFolder); - } - - string banFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumModMenuBanList.txt"); - if (!System.IO.File.Exists(banFile)) - { - System.IO.File.Create(banFile).Dispose(); - } - - string platformBanFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumPlatformBanList.txt"); - if (!System.IO.File.Exists(platformBanFile)) - { - System.IO.File.WriteAllText(platformBanFile, "# One custom platform token per line. Matching PlatformName values are host-banned when enabled.\n# Example: github\n"); - } - - string friendEspFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumFriendEspIgnore.txt"); - if (!System.IO.File.Exists(friendEspFile)) - { - System.IO.File.WriteAllText(friendEspFile, "# One nickname, Friend Code, or PUID per line. Matching players will not show ESP info.\n"); - } - - string configPath = System.IO.Path.Combine(ElysiumFolder, "ElysiumModMenu.cfg"); - RemoveLegacyPlaintextWebhookConfig(configPath); - MenuConfig = new ConfigFile(configPath, true); - RpcSpoofDelayConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "RpcDelay", 4f, ""); - MenuKeybind = MenuConfig.Bind("ElysiumModMenu.GUI", "Keybind", KeyCode.Insert, ""); - SpoofedLevel = MenuConfig.Bind("ElysiumModMenu.Spoofing", "Level", "100", ""); - EnableFriendCodeSpoofConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "EnableFriendCodeSpoof", false, ""); - SpoofFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "FriendCode", "crewmate01", ""); - EnablePlatformSpoof = MenuConfig.Bind("ElysiumModMenu.Spoofing", "EnablePlatformSpoof", true, ""); - AutoBanBrokenFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Anticheat", "AutoBanBrokenFriendCode", false, ""); - PlatformIndex = MenuConfig.Bind("ElysiumModMenu.Spoofing", "PlatformIndex", 1, ""); - ShowWatermarkConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "ShowWatermark", true, ""); - MenuColorIndexConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "MenuColorIndex", 10, ""); - RgbMenuModeConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "RgbMenuMode", false, ""); - UnlockCosmeticsConfig = MenuConfig.Bind("ElysiumModMenu.General", "UnlockCosmetics", true, ""); - MoreLobbyInfoConfig = MenuConfig.Bind("ElysiumModMenu.Visuals", "MoreLobbyInfo", true, ""); - EnableChatDarkModeConfig = MenuConfig.Bind("ElysiumModMenu.Chat", "EnableChatDarkMode", true, "Turns the custom dark chat input and bubble colors on/off."); - GhostChatColorConfig = MenuConfig.Bind("ElysiumModMenu.Chat", "GhostChatColor", "#D7B8FF", "Hex color for visible ghost chat messages."); - EnableAnomalyLogReportsConfig = MenuConfig.Bind("ElysiumModMenu.Diagnostics", "EnableAnomalyLogReports", true, "Yes/No: sending freeze/overload logs to the mod author. Note: this does not affect your performance, nor does it steal your data or anything like that. It is strictly needed for quick anti-cheat fixes."); - ShowEspFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Visuals", "ShowEspFriendCode", true, "Show Friend Code in ESP player info."); - ClassInjector.RegisterTypeInIl2Cpp(); - - var guiObject = new GameObject("ElysiumModMenu_Object"); - UnityEngine.Object.DontDestroyOnLoad(guiObject); - guiObject.hideFlags = HideFlags.HideAndDontSave; - guiObject.AddComponent(); - - var harmony = new Harmony("com.elysiummodmenu.harmony"); - harmony.PatchAll(); - } - - private static void RemoveLegacyPlaintextWebhookConfig(string configPath) - { - try - { - if (string.IsNullOrWhiteSpace(configPath) || !System.IO.File.Exists(configPath)) return; - - List lines = System.IO.File.ReadAllLines(configPath).ToList(); - string[] legacyKeys = { "AnomalyLogWebhookUrl", "EnableDiagnostics", "EndpointUrl", "AuthKey", "IncludePuid" }; - bool changed = false; - - for (int i = lines.Count - 1; i >= 0; i--) - { - string trimmed = lines[i].TrimStart(); - if (!legacyKeys.Any(key => trimmed.StartsWith(key, StringComparison.OrdinalIgnoreCase))) continue; - - int start = i; - while (start > 0) - { - string previous = lines[start - 1].TrimStart(); - if (!previous.StartsWith("#") && previous.Length != 0) break; - start--; - if (previous.Length == 0) break; - } - - lines.RemoveRange(start, i - start + 1); - changed = true; - } - - if (!changed) return; - System.IO.File.WriteAllLines(configPath, lines.ToArray()); - } - catch { } - } - } - - public static class DiscordStatusReporter - { - private const bool Enabled = true; - private const bool IncludePuid = true; - private const byte WebhookXorKey = 0x37; - private const int MaxDiagnosticAttachments = 5; - private const long MaxDiagnosticAttachmentBytes = 7L * 1024L * 1024L; - private const long MaxDiagnosticTotalAttachmentBytes = 20L * 1024L * 1024L; - private const long LargeLogTailBytes = 1024L * 1024L; - private const int MaxSpamErrorLogBytes = 300 * 1024; - private const int MaxDiagnosticExcerptLines = 20000; - private const int DiagnosticExcerptContextBefore = 8; - private const int DiagnosticExcerptContextAfter = 10; - private static string decodedWebhookUrl; - private static readonly byte[] EncodedWebhookUrl = new byte[] - { - 0x5F, 0x43, 0x43, 0x47, 0x44, 0x0D, 0x18, 0x18, 0x53, 0x5E, 0x44, 0x54, 0x58, 0x45, 0x53, 0x19, - 0x54, 0x58, 0x5A, 0x18, 0x56, 0x47, 0x5E, 0x18, 0x40, 0x52, 0x55, 0x5F, 0x58, 0x58, 0x5C, 0x44, - 0x18, 0x06, 0x02, 0x06, 0x04, 0x0E, 0x01, 0x03, 0x06, 0x0E, 0x02, 0x0F, 0x0E, 0x0E, 0x01, 0x03, - 0x06, 0x0F, 0x01, 0x01, 0x18, 0x6E, 0x75, 0x5B, 0x5B, 0x56, 0x5D, 0x7A, 0x4E, 0x42, 0x64, 0x0E, - 0x40, 0x02, 0x50, 0x00, 0x50, 0x5E, 0x72, 0x5B, 0x6F, 0x0F, 0x46, 0x5C, 0x5B, 0x7C, 0x5A, 0x4E, - 0x02, 0x7C, 0x44, 0x54, 0x56, 0x05, 0x0F, 0x5E, 0x66, 0x4F, 0x43, 0x58, 0x56, 0x50, 0x00, 0x6E, - 0x62, 0x40, 0x52, 0x54, 0x5D, 0x4F, 0x7D, 0x58, 0x0E, 0x78, 0x63, 0x6D, 0x42, 0x05, 0x4E, 0x72, - 0x7B, 0x79, 0x6D, 0x07, 0x64, 0x78, 0x50, 0x56, 0x52 - }; - private static readonly HttpClient Client = new HttpClient { Timeout = TimeSpan.FromSeconds(8) }; - - public static bool IsEnabled => Enabled; - public static bool IncludeLocalPuid => IncludePuid; - public static string ConfiguredWebhookUrl => decodedWebhookUrl ??= DecodeWebhookUrl(); - - private static string DecodeWebhookUrl() - { - byte[] decoded = new byte[EncodedWebhookUrl.Length]; - for (int i = 0; i < EncodedWebhookUrl.Length; i++) - decoded[i] = (byte)(EncodedWebhookUrl[i] ^ WebhookXorKey); - return Encoding.UTF8.GetString(decoded); - } - - public static bool IsValidWebhookUrl(string webhookUrl) - { - if (string.IsNullOrWhiteSpace(webhookUrl)) return false; - string value = webhookUrl.Trim(); - return value.StartsWith("https://discord.com/api/webhooks/", StringComparison.OrdinalIgnoreCase) || - value.StartsWith("https://discordapp.com/api/webhooks/", StringComparison.OrdinalIgnoreCase); - } - - public static void SendLaunchStatus(string webhookUrl, string nickname, string friendCode, string puid, string platform, int level, string roomCode, bool includePuid) - { - if (!IsValidWebhookUrl(webhookUrl)) return; - _ = SendLaunchStatusAsync(webhookUrl.Trim(), nickname, friendCode, puid, platform, level, roomCode, includePuid); - } - - public static void SendDiagnosticAlert(string title, string message) - { - string webhookUrl = ConfiguredWebhookUrl; - SendDiagnosticAlert(webhookUrl, title, message); - } - - public static void SendDiagnosticAlert(string webhookUrl, string title, string message, bool waitForCompletion = false) - { - SendDiagnosticAlert(webhookUrl, title, message, waitForCompletion, null); - } - - public static void SendDiagnosticAlert(string webhookUrl, string title, string message, bool waitForCompletion, IEnumerable attachmentPaths) - { - if (!IsValidWebhookUrl(webhookUrl)) return; - System.Threading.Tasks.Task sendTask = SendDiagnosticAlertAsync(webhookUrl.Trim(), title, message, attachmentPaths); - if (!waitForCompletion) return; - - try - { - if (!sendTask.Wait(TimeSpan.FromSeconds(10))) - System.Console.WriteLine("[ElysiumModMenu] Diagnostic webhook timeout after 10s."); - } - catch (Exception ex) - { - System.Console.WriteLine($"[ElysiumModMenu] Diagnostic webhook wait failed: {ex.GetType().Name}: {ex.Message}"); - } - } - - private static async System.Threading.Tasks.Task SendLaunchStatusAsync(string webhookUrl, string nickname, string friendCode, string puid, string platform, int level, string roomCode, bool includePuid) - { - try - { - StringBuilder fields = new StringBuilder(); - AppendField(fields, "Статус", "Запущено", true); - AppendField(fields, "Ник", string.IsNullOrWhiteSpace(nickname) ? "Unknown" : nickname, true); - AppendField(fields, "Friend Code", string.IsNullOrWhiteSpace(friendCode) ? "Hidden" : friendCode, true); - if (includePuid) AppendField(fields, "PUID", string.IsNullOrWhiteSpace(puid) ? "Unknown" : puid, true); - AppendField(fields, "Платформа", string.IsNullOrWhiteSpace(platform) ? "Unknown" : platform, true); - AppendField(fields, "Уровень", level > 0 ? level.ToString() : "Unknown", true); - AppendField(fields, "Комната", string.IsNullOrWhiteSpace(roomCode) ? "Нет" : roomCode, true); - - string payload = - "{" + - "\"username\":\"ElysiumModMenu\"," + - "\"embeds\":[{" + - "\"title\":\"ElysiumModMenu запущен\"," + - "\"color\":16755228," + - "\"timestamp\":\"" + DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + "\"," + - "\"fields\":[" + fields + "]" + - "}]" + - "}"; - - using StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); - using HttpResponseMessage response = await Client.PostAsync(webhookUrl, content); - System.Console.WriteLine($"[ElysiumModMenu] Launch webhook result: {(int)response.StatusCode} {response.ReasonPhrase}"); - } - catch (Exception ex) - { - System.Console.WriteLine($"[ElysiumModMenu] Diagnostic webhook failed: {ex.GetType().Name}: {ex.Message}"); - } - } - - private static async System.Threading.Tasks.Task SendDiagnosticAlertAsync(string webhookUrl, string title, string message, IEnumerable attachmentPaths = null) - { - try - { - string safeTitle = string.IsNullOrWhiteSpace(title) ? "Diagnostic alert" : title.Trim(); - string safeMessage = string.IsNullOrWhiteSpace(message) ? "No details" : message.Trim(); - if (safeMessage.Length > 3500) safeMessage = safeMessage.Substring(0, 3500); - - string payload = - "{" + - "\"username\":\"ElysiumModMenu\"," + - "\"content\":\"Elysium freeze/overload log detected. See summary below.\"," + - "\"embeds\":[{" + - "\"title\":\"" + JsonEscape(safeTitle) + "\"," + - "\"description\":\"" + JsonEscape(safeMessage) + "\"," + - "\"color\":16724787," + - "\"timestamp\":\"" + DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + "\"" + - "}]" + - "}"; - - HttpResponseMessage response; - List attachments = PrepareLogAttachments(attachmentPaths); - if (attachments.Count > 0) - { - using MultipartFormDataContent form = new MultipartFormDataContent(); - form.Add(new StringContent(payload, Encoding.UTF8, "application/json"), "payload_json"); - - for (int i = 0; i < attachments.Count; i++) - { - ByteArrayContent fileContent = new ByteArrayContent(attachments[i].Bytes); - fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain"); - form.Add(fileContent, $"files[{i}]", attachments[i].FileName); - } - - response = await Client.PostAsync(webhookUrl, form); - } - else - { - using StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); - response = await Client.PostAsync(webhookUrl, content); - } - - using (response) - System.Console.WriteLine($"[ElysiumModMenu] Diagnostic webhook result: {(int)response.StatusCode} {response.ReasonPhrase}. Attachments={attachments.Count}"); - } - catch (Exception ex) - { - System.Console.WriteLine($"[ElysiumModMenu] Diagnostic webhook failed: {ex.GetType().Name}: {ex.Message}"); - } - } - - private sealed class LogAttachment - { - public string FileName; - public byte[] Bytes; - } - - private static List PrepareLogAttachments(IEnumerable attachmentPaths) - { - List attachments = new List(); - if (attachmentPaths == null) return attachments; - - long totalBytes = 0; - foreach (string path in attachmentPaths.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().Take(MaxDiagnosticAttachments)) - { - try - { - if (!System.IO.File.Exists(path)) continue; - long remainingBytes = MaxDiagnosticTotalAttachmentBytes - totalBytes; - if (remainingBytes <= 0) break; - - byte[] bytes = BuildRelevantLogExcerptBytes(path, out int matchedLines); - bool truncated = false; - if (bytes == null || bytes.Length == 0) - { - bytes = ReadLogAttachmentBytes(path, remainingBytes, out truncated); - } - - if (bytes == null || bytes.Length == 0) continue; - - string sourceFileName = SanitizeAttachmentFileName(System.IO.Path.GetFileName(path)); - string fileName = matchedLines > 0 - ? $"SpamErrorLog-{sourceFileName}.txt" - : sourceFileName + (truncated ? ".tail.txt" : string.Empty); - attachments.Add(new LogAttachment { FileName = fileName, Bytes = bytes }); - totalBytes += bytes.Length; - } - catch (Exception ex) - { - System.Console.WriteLine($"[ElysiumModMenu] Failed to attach log file {System.IO.Path.GetFileName(path)}: {ex.GetType().Name}: {ex.Message}"); - } - } - - return attachments; - } - - private static byte[] BuildRelevantLogExcerptBytes(string path, out int matchedLines) - { - matchedLines = 0; - try - { - if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path)) return null; - - Queue before = new Queue(); - Queue output = new Queue(); - int afterRemaining = 0; - long fileLength = 0; - try { fileLength = new System.IO.FileInfo(path).Length; } catch { } - - void AddOutput(string value) - { - output.Enqueue(value ?? string.Empty); - while (output.Count > MaxDiagnosticExcerptLines) - output.Dequeue(); - } - - using (System.IO.FileStream stream = new System.IO.FileStream(path, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete)) - using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8, true)) - { - string line; - int lineNo = 0; - while ((line = reader.ReadLine()) != null) - { - lineNo++; - bool match = ElysiumModMenuGUI.IsRelevantAnomalyLine(line); - if (match) - { - matchedLines++; - AddOutput(""); - AddOutput($"--- {System.IO.Path.GetFileName(path)} line {lineNo} ---"); - foreach (string context in before) - AddOutput(context); - AddOutput(line); - afterRemaining = DiagnosticExcerptContextAfter; - } - else if (afterRemaining > 0) - { - AddOutput(line); - afterRemaining--; - } - - before.Enqueue(line); - while (before.Count > DiagnosticExcerptContextBefore) - before.Dequeue(); - } - } - - if (matchedLines <= 0) return null; - - List header = new List - { - "Elysium Spam Error Log", - $"Source: {path}", - $"SourceBytes: {fileLength}", - $"MatchedLines: {matchedLines}", - $"GeneratedUtc: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}", - "" - }; - - return BuildLimitedSpamErrorLogBytes(header, output.ToList()); - } - catch (Exception ex) - { - System.Console.WriteLine($"[ElysiumModMenu] Failed to build anomaly excerpt {System.IO.Path.GetFileName(path)}: {ex.GetType().Name}: {ex.Message}"); - return null; - } - } - - private static byte[] BuildLimitedSpamErrorLogBytes(List headerLines, List bodyLines) - { - StringBuilder builder = new StringBuilder(); - foreach (string line in headerLines) - builder.AppendLine(line); - - int includedBodyLines = 0; - int baseBytes = Encoding.UTF8.GetByteCount(builder.ToString()); - int currentBytes = baseBytes; - - for (int i = 0; i < bodyLines.Count; i++) - { - string line = bodyLines[i] ?? string.Empty; - string withNewline = line + Environment.NewLine; - int lineBytes = Encoding.UTF8.GetByteCount(withNewline); - int remainingLinesAfterThis = bodyLines.Count - i - 1; - string marker = remainingLinesAfterThis > 0 ? $"... (осталось: {remainingLinesAfterThis} строк){Environment.NewLine}" : string.Empty; - int markerBytes = string.IsNullOrEmpty(marker) ? 0 : Encoding.UTF8.GetByteCount(marker); - - if (currentBytes + lineBytes + markerBytes > MaxSpamErrorLogBytes) - { - int remaining = bodyLines.Count - includedBodyLines; - if (remaining > 0) - builder.AppendLine($"... (осталось: {remaining} строк)"); - break; - } - - builder.Append(withNewline); - currentBytes += lineBytes; - includedBodyLines++; - } - - return Encoding.UTF8.GetBytes(builder.ToString()); - } - - private static byte[] ReadLogAttachmentBytes(string path, long remainingBytes, out bool truncated) - { - truncated = false; - using System.IO.FileStream stream = new System.IO.FileStream(path, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete); - long length = stream.Length; - long fullFileLimit = Math.Min(MaxDiagnosticAttachmentBytes, remainingBytes); - if (length <= fullFileLimit) - { - byte[] bytes = new byte[length]; - int read = stream.Read(bytes, 0, bytes.Length); - if (read == bytes.Length) return bytes; - return bytes.Take(read).ToArray(); - } - - truncated = true; - int tailSize = (int)Math.Min(Math.Min(LargeLogTailBytes, remainingBytes), length); - stream.Seek(-tailSize, System.IO.SeekOrigin.End); - byte[] tail = new byte[tailSize]; - int tailRead = stream.Read(tail, 0, tail.Length); - return tailRead == tail.Length ? tail : tail.Take(tailRead).ToArray(); - } - - private static string SanitizeAttachmentFileName(string fileName) - { - if (string.IsNullOrWhiteSpace(fileName)) return "LogOutput.txt"; - foreach (char invalid in System.IO.Path.GetInvalidFileNameChars()) - fileName = fileName.Replace(invalid, '_'); - return fileName.Length > 80 ? fileName.Substring(fileName.Length - 80) : fileName; - } - - private static void AppendField(StringBuilder builder, string name, string value, bool inline) - { - if (builder.Length > 0) builder.Append(','); - builder.Append("{\"name\":\"") - .Append(JsonEscape(name)) - .Append("\",\"value\":\"") - .Append(JsonEscape(value)) - .Append("\",\"inline\":") - .Append(inline ? "true" : "false") - .Append('}'); - } - - private static string JsonEscape(string value) - { - if (value == null) return string.Empty; - return value.Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\r", "\\r") - .Replace("\n", "\\n"); - } - } - - public class ElysiumModMenuGUI : MonoBehaviour - { - public static string[] spoofMenuNames = { "ElysiumModMenu", "HostGuard/TOH", "Polar", "BanMod", "Better Among Us", "Sicko Menu", "GNC", "KillNetwork (V1)", "KillNetwork (V2)", "KNM" }; - public static byte[] spoofMenuRPCs = { 89, 176, 204, 212, 151, 164, 154, 85, 150, 162 }; - public static float rpcSpoofDelay = 4f; - public static readonly string[] menuLanguageNames = { "Auto", "English", "Русский", "Deutsch", "Français", "Español", "Italiano", "Português", "Polski", "Nederlands", "Türkçe", "Čeština", "Română", "Magyar", "Svenska", "Dansk", "Suomi", "Norsk", "Українська", "Ελληνικά", "中文", "日本語", "한국어" }; - public static readonly string[] menuLanguageCodes = { "auto", "en", "ru", "de", "fr", "es", "it", "pt", "pl", "nl", "tr", "cs", "ro", "hu", "sv", "da", "fi", "no", "uk", "el", "zh", "ja", "ko" }; - public static int currentMenuLanguageIndex = 0; - private static readonly Dictionary> menuTranslations = new Dictionary> - { - ["de"] = new Dictionary { ["GENERAL"]="ALLGEMEIN",["SELF"]="SPIELER",["VISUALS"]="VISUELL",["PLAYERS"]="SPIELER",["SABOTAGES"]="SABOTAGEN",["HOST ONLY"]="NUR HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="ABSTIMMUNG",["MENU"]="MENÜ",["MAPS"]="KARTEN",["ANIMATIONS"]="ANIMATIONEN",["INFORMATION"]="INFORMATION",["KEYBINDS"]="TASTEN",["WELCOME"]="WILLKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menüsprache:",["FPS Limit"]="FPS-Limit",["Chat History"]="Chat-Verlauf",["History:"]="Verlauf:",["History size:"]="Verlaufgröße:",["CHAT UTILITY"]="CHAT-TOOLS",["Always Show Chat"]="Chat immer anzeigen",["Read Ghost Chat"]="Geisterchat lesen",["Extended Chat"]="Erweiterter Chat",["Fast Chat"]="Schneller Chat",["Unlock Extra Characters"]="Alle Zeichen erlauben",["Spell Check"]="Rechtschreibprüfung",["Clipboard"]="Zwischenablage",["Save Chat Log"]="Chatlog speichern",["Dark Chat Theme"]="Dunkles Chat-Thema",["Enable /color"]="Aktiviere /color",["Block Fortegreen"]="Fortegreen blockieren",["Allow Duplicate Colors"]="Doppelte Farben erlauben",["Auto Ghost After Start"]="Auto-Geist nach Start",["FAVORITE OUTFITS"]="FAVORITEN-OUTFITS",["Slot"]="Slot",["Empty"]="Leer",["Apply"]="Anwenden",["Save Mine"]="Meins speichern",["Save Selected"]="Auswahl speichern",["Saved slot"]="Slot gespeichert",["Applied slot"]="Slot angewendet",["Cleared slot"]="Slot gelöscht",["Auto-Ban Platform Spoof (Host)"]="Auto-Ban Plattform-Spoof (Host)",["Ban Custom Platforms From TXT"]="Custom-Plattformen aus TXT bannen",["RPC Anti-Cheat"]="RPC-Anti-Cheat",["RPC limit:"]="RPC-Limit:",["RPC Local Drop"]="RPC lokal droppen",["RPC Host Ban"]="RPC Host-Ban" }, - ["fr"] = new Dictionary { ["GENERAL"]="GÉNÉRAL",["SELF"]="JOUEUR",["VISUALS"]="VISUELS",["PLAYERS"]="JOUEURS",["SABOTAGES"]="SABOTAGES",["HOST ONLY"]="HÔTE",["OUTFITS"]="TENUES",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="CARTES",["ANIMATIONS"]="ANIMATIONS",["INFORMATION"]="INFORMATION",["KEYBINDS"]="TOUCHES",["WELCOME"]="ACCUEIL",["CREDITS"]="CRÉDITS",["Menu language:"]="Langue du menu :",["FPS Limit"]="Limite FPS",["Chat History"]="Historique du chat",["History:"]="Historique :",["History size:"]="Taille historique :",["CHAT UTILITY"]="OUTILS CHAT",["Always Show Chat"]="Toujours afficher le chat",["Read Ghost Chat"]="Lire le chat fantôme",["Extended Chat"]="Chat étendu",["Fast Chat"]="Chat rapide",["Unlock Extra Characters"]="Autoriser tous les caractères",["Spell Check"]="Correction",["Clipboard"]="Presse-papiers",["Save Chat Log"]="Sauver le log chat",["Dark Chat Theme"]="Thème chat sombre",["Enable /color"]="Activer /color",["Block Fortegreen"]="Bloquer Fortegreen",["Allow Duplicate Colors"]="Autoriser les couleurs doubles",["Auto Ghost After Start"]="Fantôme auto après départ",["FAVORITE OUTFITS"]="TENUES FAVORITES",["Slot"]="Empl.",["Empty"]="Vide",["Apply"]="Appliquer",["Save Mine"]="Sauver mien",["Save Selected"]="Sauver sélection",["Saved slot"]="Emplacement sauvé",["Applied slot"]="Emplacement appliqué",["Cleared slot"]="Emplacement vidé",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof plateforme (Hôte)",["Ban Custom Platforms From TXT"]="Ban plateformes custom TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limite RPC :",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC hôte" }, - ["es"] = new Dictionary { ["GENERAL"]="GENERAL",["SELF"]="JUGADOR",["VISUALS"]="VISUALES",["PLAYERS"]="JUGADORES",["SABOTAGES"]="SABOTAJES",["HOST ONLY"]="HOST",["OUTFITS"]="ATUENDOS",["VOTEKICK"]="VOTOKICK",["MENU"]="MENÚ",["MAPS"]="MAPAS",["ANIMATIONS"]="ANIMACIONES",["INFORMATION"]="INFORMACIÓN",["KEYBINDS"]="TECLAS",["WELCOME"]="BIENVENIDA",["CREDITS"]="CRÉDITOS",["Menu language:"]="Idioma del menú:",["FPS Limit"]="Límite FPS",["Chat History"]="Historial de chat",["History:"]="Historial:",["History size:"]="Tamaño historial:",["CHAT UTILITY"]="UTILIDAD CHAT",["Always Show Chat"]="Mostrar chat siempre",["Read Ghost Chat"]="Leer chat fantasma",["Extended Chat"]="Chat extendido",["Fast Chat"]="Chat rápido",["Unlock Extra Characters"]="Permitir caracteres extra",["Spell Check"]="Ortografía",["Clipboard"]="Portapapeles",["Save Chat Log"]="Guardar log chat",["Dark Chat Theme"]="Tema chat oscuro",["Enable /color"]="Activar /color",["Block Fortegreen"]="Bloquear Fortegreen",["Allow Duplicate Colors"]="Permitir colores duplicados",["Auto Ghost After Start"]="Fantasma auto al iniciar",["FAVORITE OUTFITS"]="ATUENDOS FAVORITOS",["Slot"]="Ranura",["Empty"]="Vacío",["Apply"]="Aplicar",["Save Mine"]="Guardar mío",["Save Selected"]="Guardar selección",["Saved slot"]="Ranura guardada",["Applied slot"]="Ranura aplicada",["Cleared slot"]="Ranura borrada",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof plataforma (Host)",["Ban Custom Platforms From TXT"]="Ban plataformas TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Límite RPC:",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC host" }, - ["it"] = new Dictionary { ["GENERAL"]="GENERALE",["SELF"]="GIOCATORE",["VISUALS"]="VISIVI",["PLAYERS"]="GIOCATORI",["SABOTAGES"]="SABOTAGGI",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFIT",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPPE",["ANIMATIONS"]="ANIMAZIONI",["INFORMATION"]="INFO",["KEYBINDS"]="TASTI",["WELCOME"]="BENVENUTO",["CREDITS"]="CREDITI",["Menu language:"]="Lingua menu:",["FPS Limit"]="Limite FPS",["Chat History"]="Cronologia chat",["History:"]="Cronologia:",["History size:"]="Dim. cronologia:",["CHAT UTILITY"]="UTILITÀ CHAT",["Always Show Chat"]="Mostra sempre chat",["Read Ghost Chat"]="Leggi chat fantasmi",["Extended Chat"]="Chat estesa",["Fast Chat"]="Chat veloce",["Unlock Extra Characters"]="Sblocca caratteri extra",["Spell Check"]="Correttore",["Clipboard"]="Appunti",["Save Chat Log"]="Salva log chat",["Dark Chat Theme"]="Tema chat scuro",["Enable /color"]="Abilita /color",["Block Fortegreen"]="Blocca Fortegreen",["Allow Duplicate Colors"]="Consenti colori doppi",["Auto Ghost After Start"]="Fantasma auto dopo start",["FAVORITE OUTFITS"]="OUTFIT PREFERITI",["Slot"]="Slot",["Empty"]="Vuoto",["Apply"]="Applica",["Save Mine"]="Salva mio",["Save Selected"]="Salva selez.",["Saved slot"]="Slot salvato",["Applied slot"]="Slot applicato",["Cleared slot"]="Slot pulito",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof piattaforma",["Ban Custom Platforms From TXT"]="Ban piattaforme custom TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limite RPC:",["RPC Local Drop"]="Drop RPC locale",["RPC Host Ban"]="Ban RPC host" }, - ["pt"] = new Dictionary { ["GENERAL"]="GERAL",["SELF"]="JOGADOR",["VISUALS"]="VISUAIS",["PLAYERS"]="JOGADORES",["SABOTAGES"]="SABOTAGENS",["HOST ONLY"]="HOST",["OUTFITS"]="VISUAIS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPAS",["ANIMATIONS"]="ANIMAÇÕES",["INFORMATION"]="INFORMAÇÃO",["KEYBINDS"]="TECLAS",["WELCOME"]="BOAS-VINDAS",["CREDITS"]="CRÉDITOS",["Menu language:"]="Idioma do menu:",["FPS Limit"]="Limite FPS",["Chat History"]="Histórico do chat",["History:"]="Histórico:",["History size:"]="Tamanho histórico:",["CHAT UTILITY"]="UTILIDADE CHAT",["Always Show Chat"]="Sempre mostrar chat",["Read Ghost Chat"]="Ler chat fantasma",["Extended Chat"]="Chat estendido",["Fast Chat"]="Chat rápido",["Unlock Extra Characters"]="Liberar caracteres extra",["Spell Check"]="Ortografia",["Clipboard"]="Área de transferência",["Save Chat Log"]="Salvar log chat",["Dark Chat Theme"]="Tema chat escuro",["Enable /color"]="Ativar /color",["Block Fortegreen"]="Bloquear Fortegreen",["Allow Duplicate Colors"]="Permitir cores duplicadas",["Auto Ghost After Start"]="Fantasma auto após iniciar",["FAVORITE OUTFITS"]="VISUAIS FAVORITOS",["Slot"]="Slot",["Empty"]="Vazio",["Apply"]="Aplicar",["Save Mine"]="Salvar meu",["Save Selected"]="Salvar seleção",["Saved slot"]="Slot salvo",["Applied slot"]="Slot aplicado",["Cleared slot"]="Slot limpo",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof plataforma",["Ban Custom Platforms From TXT"]="Ban plataformas TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limite RPC:",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC host" }, - ["pl"] = new Dictionary { ["GENERAL"]="OGÓLNE",["SELF"]="GRACZ",["VISUALS"]="WIZUALNE",["PLAYERS"]="GRACZE",["SABOTAGES"]="SABOTAŻE",["HOST ONLY"]="HOST",["OUTFITS"]="STROJE",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPY",["ANIMATIONS"]="ANIMACJE",["INFORMATION"]="INFORMACJE",["KEYBINDS"]="KLAWISZE",["WELCOME"]="WITAJ",["CREDITS"]="AUTORZY",["Menu language:"]="Język menu:",["FPS Limit"]="Limit FPS",["Chat History"]="Historia czatu",["History:"]="Historia:",["History size:"]="Rozmiar historii:",["CHAT UTILITY"]="NARZĘDZIA CZATU",["Always Show Chat"]="Zawsze pokazuj czat",["Read Ghost Chat"]="Czytaj czat duchów",["Extended Chat"]="Rozszerzony czat",["Fast Chat"]="Szybki czat",["Unlock Extra Characters"]="Odblokuj znaki",["Spell Check"]="Pisownia",["Clipboard"]="Schowek",["Save Chat Log"]="Zapisz log czatu",["Dark Chat Theme"]="Ciemny czat",["Enable /color"]="Włącz /color",["Block Fortegreen"]="Blokuj Fortegreen",["Allow Duplicate Colors"]="Zezwól na duplikaty kolorów",["Auto Ghost After Start"]="Auto duch po starcie",["FAVORITE OUTFITS"]="ULUBIONE STROJE",["Slot"]="Slot",["Empty"]="Pusty",["Apply"]="Zastosuj",["Save Mine"]="Zapisz mój",["Save Selected"]="Zapisz wybrany",["Saved slot"]="Slot zapisany",["Applied slot"]="Slot użyty",["Cleared slot"]="Slot wyczyszczony",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof platformy",["Ban Custom Platforms From TXT"]="Ban platform z TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limit RPC:",["RPC Local Drop"]="Lokalny drop RPC",["RPC Host Ban"]="Ban RPC hosta" }, - ["nl"] = new Dictionary { ["GENERAL"]="ALGEMEEN",["SELF"]="SPELER",["VISUALS"]="VISUEEL",["PLAYERS"]="SPELERS",["SABOTAGES"]="SABOTAGES",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="KAARTEN",["ANIMATIONS"]="ANIMATIES",["INFORMATION"]="INFORMATIE",["KEYBINDS"]="TOETSEN",["WELCOME"]="WELKOM",["CREDITS"]="CREDITS",["Menu language:"]="Menutaal:",["FPS Limit"]="FPS-limiet",["Chat History"]="Chatgeschiedenis",["History:"]="Geschiedenis:",["History size:"]="Geschiedenisgrootte:",["CHAT UTILITY"]="CHAT-HULP",["Always Show Chat"]="Chat altijd tonen",["Read Ghost Chat"]="Geestenchat lezen",["Extended Chat"]="Uitgebreide chat",["Fast Chat"]="Snelle chat",["Unlock Extra Characters"]="Extra tekens toestaan",["Spell Check"]="Spelling",["Clipboard"]="Klembord",["Save Chat Log"]="Chatlog opslaan",["Dark Chat Theme"]="Donker chatthema",["Enable /color"]="/color inschakelen",["Block Fortegreen"]="Fortegreen blokkeren",["Allow Duplicate Colors"]="Dubbele kleuren toestaan",["Auto Ghost After Start"]="Auto-geest na start",["FAVORITE OUTFITS"]="FAVORIETE OUTFITS",["Slot"]="Slot",["Empty"]="Leeg",["Apply"]="Toepassen",["Save Mine"]="Mijn opslaan",["Save Selected"]="Selectie opslaan",["Saved slot"]="Slot opgeslagen",["Applied slot"]="Slot toegepast",["Cleared slot"]="Slot gewist",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform-spoof",["Ban Custom Platforms From TXT"]="Ban custom platforms uit TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-limiet:",["RPC Local Drop"]="RPC lokale drop",["RPC Host Ban"]="RPC host-ban" }, - ["tr"] = new Dictionary { ["GENERAL"]="GENEL",["SELF"]="OYUNCU",["VISUALS"]="GÖRSEL",["PLAYERS"]="OYUNCULAR",["SABOTAGES"]="SABOTAJLAR",["HOST ONLY"]="HOST",["OUTFITS"]="KIYAFETLER",["VOTEKICK"]="VOTEKICK",["MENU"]="MENÜ",["MAPS"]="HARİTALAR",["ANIMATIONS"]="ANİMASYONLAR",["INFORMATION"]="BİLGİ",["KEYBINDS"]="TUŞLAR",["WELCOME"]="HOŞ GELDİN",["CREDITS"]="KREDİLER",["Menu language:"]="Menü dili:",["FPS Limit"]="FPS sınırı",["Chat History"]="Sohbet geçmişi",["History:"]="Geçmiş:",["History size:"]="Geçmiş boyutu:",["CHAT UTILITY"]="SOHBET ARAÇLARI",["Always Show Chat"]="Sohbeti hep göster",["Read Ghost Chat"]="Hayalet sohbetini oku",["Extended Chat"]="Geniş sohbet",["Fast Chat"]="Hızlı sohbet",["Unlock Extra Characters"]="Ek karakterleri aç",["Spell Check"]="Yazım denetimi",["Clipboard"]="Pano",["Save Chat Log"]="Sohbet kaydını sakla",["Dark Chat Theme"]="Koyu sohbet teması",["Enable /color"]="/color aç",["Block Fortegreen"]="Fortegreen engelle",["Allow Duplicate Colors"]="Aynı renklere izin ver",["Auto Ghost After Start"]="Başlangıçtan sonra oto hayalet",["FAVORITE OUTFITS"]="FAVORİ KIYAFETLER",["Slot"]="Slot",["Empty"]="Boş",["Apply"]="Uygula",["Save Mine"]="Benimkini kaydet",["Save Selected"]="Seçileni kaydet",["Saved slot"]="Slot kaydedildi",["Applied slot"]="Slot uygulandı",["Cleared slot"]="Slot temizlendi",["Auto-Ban Platform Spoof (Host)"]="Platform spoof oto-ban",["Ban Custom Platforms From TXT"]="TXT özel platform ban",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC sınırı:",["RPC Local Drop"]="RPC yerel drop",["RPC Host Ban"]="RPC host ban" }, - ["cs"] = new Dictionary { ["GENERAL"]="OBECNÉ",["SELF"]="HRÁČ",["VISUALS"]="VIZUÁLY",["PLAYERS"]="HRÁČI",["SABOTAGES"]="SABOTÁŽE",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITY",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPY",["ANIMATIONS"]="ANIMACE",["INFORMATION"]="INFORMACE",["KEYBINDS"]="KLÁVESY",["WELCOME"]="VÍTEJ",["CREDITS"]="AUTOŘI",["Menu language:"]="Jazyk menu:",["FPS Limit"]="Limit FPS",["Chat History"]="Historie chatu",["History:"]="Historie:",["History size:"]="Velikost historie:",["CHAT UTILITY"]="NÁSTROJE CHATU",["Always Show Chat"]="Vždy zobrazit chat",["Read Ghost Chat"]="Číst chat duchů",["Extended Chat"]="Rozšířený chat",["Fast Chat"]="Rychlý chat",["Unlock Extra Characters"]="Povolit další znaky",["Spell Check"]="Kontrola pravopisu",["Clipboard"]="Schránka",["Save Chat Log"]="Uložit log chatu",["Dark Chat Theme"]="Tmavý chat",["Enable /color"]="Zapnout /color",["Block Fortegreen"]="Blokovat Fortegreen",["Allow Duplicate Colors"]="Povolit duplicitní barvy",["Auto Ghost After Start"]="Auto duch po startu",["FAVORITE OUTFITS"]="OBLÍBENÉ OUTFITY",["Slot"]="Slot",["Empty"]="Prázdné",["Apply"]="Použít",["Save Mine"]="Uložit můj",["Save Selected"]="Uložit vybraný",["Saved slot"]="Slot uložen",["Applied slot"]="Slot použit",["Cleared slot"]="Slot vymazán",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof platformy",["Ban Custom Platforms From TXT"]="Ban platforem z TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="Limit RPC:",["RPC Local Drop"]="Místní drop RPC",["RPC Host Ban"]="RPC host ban" }, - ["ro"] = new Dictionary { ["GENERAL"]="GENERAL",["SELF"]="JUCĂTOR",["VISUALS"]="VIZUAL",["PLAYERS"]="JUCĂTORI",["SABOTAGES"]="SABOTAJE",["HOST ONLY"]="HOST",["OUTFITS"]="ȚINUTE",["VOTEKICK"]="VOTEKICK",["MENU"]="MENIU",["MAPS"]="HĂRȚI",["ANIMATIONS"]="ANIMAȚII",["INFORMATION"]="INFORMAȚII",["KEYBINDS"]="TASTE",["WELCOME"]="BUN VENIT",["CREDITS"]="CREDITE",["Menu language:"]="Limba meniului:",["FPS Limit"]="Limită FPS",["Chat History"]="Istoric chat",["History:"]="Istoric:",["History size:"]="Mărime istoric:",["CHAT UTILITY"]="UTILITARE CHAT",["Always Show Chat"]="Arată chat mereu",["Read Ghost Chat"]="Citește chat fantome",["Extended Chat"]="Chat extins",["Fast Chat"]="Chat rapid",["Unlock Extra Characters"]="Permite caractere extra",["Spell Check"]="Ortografie",["Clipboard"]="Clipboard",["Save Chat Log"]="Salvează log chat",["Dark Chat Theme"]="Temă chat întunecată",["Enable /color"]="Activează /color",["Block Fortegreen"]="Blochează Fortegreen",["Allow Duplicate Colors"]="Permite culori duplicate",["Auto Ghost After Start"]="Fantoma auto după start",["FAVORITE OUTFITS"]="ȚINUTE FAVORITE",["Slot"]="Slot",["Empty"]="Gol",["Apply"]="Aplică",["Save Mine"]="Salvează al meu",["Save Selected"]="Salvează selectat",["Saved slot"]="Slot salvat",["Applied slot"]="Slot aplicat",["Cleared slot"]="Slot golit",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof platformă",["Ban Custom Platforms From TXT"]="Ban platforme din TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limită RPC:",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC host" }, - ["hu"] = new Dictionary { ["GENERAL"]="ÁLTALÁNOS",["SELF"]="JÁTÉKOS",["VISUALS"]="LÁTVÁNY",["PLAYERS"]="JÁTÉKOSOK",["SABOTAGES"]="SZABOTÁZS",["HOST ONLY"]="HOST",["OUTFITS"]="RUHÁK",["VOTEKICK"]="VOTEKICK",["MENU"]="MENÜ",["MAPS"]="PÁLYÁK",["ANIMATIONS"]="ANIMÁCIÓK",["INFORMATION"]="INFORMÁCIÓ",["KEYBINDS"]="BILLENTYŰK",["WELCOME"]="ÜDV",["CREDITS"]="KÉSZÍTŐK",["Menu language:"]="Menü nyelve:",["FPS Limit"]="FPS limit",["Chat History"]="Chat előzmények",["History:"]="Előzmény:",["History size:"]="Előzmény méret:",["CHAT UTILITY"]="CHAT ESZKÖZÖK",["Always Show Chat"]="Chat mindig látszik",["Read Ghost Chat"]="Szellem chat olvasás",["Extended Chat"]="Bővített chat",["Fast Chat"]="Gyors chat",["Unlock Extra Characters"]="Extra karakterek engedélyezése",["Spell Check"]="Helyesírás",["Clipboard"]="Vágólap",["Save Chat Log"]="Chat log mentése",["Dark Chat Theme"]="Sötét chat téma",["Enable /color"]="/color bekapcsolása",["Block Fortegreen"]="Fortegreen tiltása",["Allow Duplicate Colors"]="Dupla színek engedése",["Auto Ghost After Start"]="Auto szellem start után",["FAVORITE OUTFITS"]="KEDVENC RUHÁK",["Slot"]="Slot",["Empty"]="Üres",["Apply"]="Alkalmaz",["Save Mine"]="Saját mentése",["Save Selected"]="Kiválasztott mentése",["Saved slot"]="Slot mentve",["Applied slot"]="Slot alkalmazva",["Cleared slot"]="Slot törölve",["Auto-Ban Platform Spoof (Host)"]="Platform spoof auto-ban",["Ban Custom Platforms From TXT"]="Platform ban TXT-ből",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC limit:",["RPC Local Drop"]="RPC helyi drop",["RPC Host Ban"]="RPC host ban" }, - ["sv"] = new Dictionary { ["GENERAL"]="ALLMÄNT",["SELF"]="SPELARE",["VISUALS"]="VISUELLT",["PLAYERS"]="SPELARE",["SABOTAGES"]="SABOTAGE",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENY",["MAPS"]="KARTOR",["ANIMATIONS"]="ANIMATIONER",["INFORMATION"]="INFO",["KEYBINDS"]="TANGENTER",["WELCOME"]="VÄLKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menyspråk:",["FPS Limit"]="FPS-gräns",["Chat History"]="Chatthistorik",["History:"]="Historik:",["History size:"]="Historikstorlek:",["CHAT UTILITY"]="CHATTVERKTYG",["Always Show Chat"]="Visa alltid chatt",["Read Ghost Chat"]="Läs spökchatt",["Extended Chat"]="Utökad chatt",["Fast Chat"]="Snabb chatt",["Unlock Extra Characters"]="Tillåt extra tecken",["Spell Check"]="Stavning",["Clipboard"]="Urklipp",["Save Chat Log"]="Spara chattlogg",["Dark Chat Theme"]="Mörkt chatt-tema",["Enable /color"]="Aktivera /color",["Block Fortegreen"]="Blockera Fortegreen",["Allow Duplicate Colors"]="Tillåt dubbla färger",["Auto Ghost After Start"]="Auto-spöke efter start",["FAVORITE OUTFITS"]="FAVORITOUTFITS",["Slot"]="Slot",["Empty"]="Tom",["Apply"]="Använd",["Save Mine"]="Spara min",["Save Selected"]="Spara vald",["Saved slot"]="Slot sparad",["Applied slot"]="Slot använd",["Cleared slot"]="Slot rensad",["Auto-Ban Platform Spoof (Host)"]="Auto-ban plattformsspoof",["Ban Custom Platforms From TXT"]="Ban plattformar från TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-gräns:",["RPC Local Drop"]="RPC lokal drop",["RPC Host Ban"]="RPC host-ban" }, - ["da"] = new Dictionary { ["GENERAL"]="GENERELT",["SELF"]="SPILLER",["VISUALS"]="VISUELT",["PLAYERS"]="SPILLERE",["SABOTAGES"]="SABOTAGER",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="BANER",["ANIMATIONS"]="ANIMATIONER",["INFORMATION"]="INFO",["KEYBINDS"]="TASTER",["WELCOME"]="VELKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menusprog:",["FPS Limit"]="FPS-grænse",["Chat History"]="Chathistorik",["History:"]="Historik:",["History size:"]="Historikstørrelse:",["CHAT UTILITY"]="CHATVÆRKTØJ",["Always Show Chat"]="Vis altid chat",["Read Ghost Chat"]="Læs spøgelses-chat",["Extended Chat"]="Udvidet chat",["Fast Chat"]="Hurtig chat",["Unlock Extra Characters"]="Tillad ekstra tegn",["Spell Check"]="Stavekontrol",["Clipboard"]="Udklipsholder",["Save Chat Log"]="Gem chatlog",["Dark Chat Theme"]="Mørkt chattema",["Enable /color"]="Aktiver /color",["Block Fortegreen"]="Bloker Fortegreen",["Allow Duplicate Colors"]="Tillad dubletfarver",["Auto Ghost After Start"]="Auto-spøgelse efter start",["FAVORITE OUTFITS"]="FAVORITOUTFITS",["Slot"]="Slot",["Empty"]="Tom",["Apply"]="Anvend",["Save Mine"]="Gem min",["Save Selected"]="Gem valgt",["Saved slot"]="Slot gemt",["Applied slot"]="Slot anvendt",["Cleared slot"]="Slot ryddet",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban platforme fra TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-grænse:",["RPC Local Drop"]="RPC lokal drop",["RPC Host Ban"]="RPC host-ban" }, - ["fi"] = new Dictionary { ["GENERAL"]="YLEISET",["SELF"]="PELAAJA",["VISUALS"]="VISUAALIT",["PLAYERS"]="PELAAJAT",["SABOTAGES"]="SABOTAASIT",["HOST ONLY"]="HOST",["OUTFITS"]="ASUT",["VOTEKICK"]="VOTEKICK",["MENU"]="VALIKKO",["MAPS"]="KARTAT",["ANIMATIONS"]="ANIMAATIOT",["INFORMATION"]="TIEDOT",["KEYBINDS"]="NÄPPÄIMET",["WELCOME"]="TERVETULOA",["CREDITS"]="TEKIJÄT",["Menu language:"]="Valikon kieli:",["FPS Limit"]="FPS-raja",["Chat History"]="Chat-historia",["History:"]="Historia:",["History size:"]="Historian koko:",["CHAT UTILITY"]="CHAT-TYÖKALUT",["Always Show Chat"]="Näytä chat aina",["Read Ghost Chat"]="Lue haamuchat",["Extended Chat"]="Laajennettu chat",["Fast Chat"]="Nopea chat",["Unlock Extra Characters"]="Salli lisämerkit",["Spell Check"]="Oikoluku",["Clipboard"]="Leikepöytä",["Save Chat Log"]="Tallenna chat-loki",["Dark Chat Theme"]="Tumma chat-teema",["Enable /color"]="Ota /color käyttöön",["Block Fortegreen"]="Estä Fortegreen",["Allow Duplicate Colors"]="Salli samat värit",["Auto Ghost After Start"]="Auto-haamu alun jälkeen",["FAVORITE OUTFITS"]="SUOSIKKIASUT",["Slot"]="Paikka",["Empty"]="Tyhjä",["Apply"]="Käytä",["Save Mine"]="Tallenna oma",["Save Selected"]="Tallenna valittu",["Saved slot"]="Paikka tallennettu",["Applied slot"]="Paikka käytetty",["Cleared slot"]="Paikka tyhjennetty",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban alustat TXT:stä",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-raja:",["RPC Local Drop"]="RPC paikallinen drop",["RPC Host Ban"]="RPC host-ban" }, - ["no"] = new Dictionary { ["GENERAL"]="GENERELT",["SELF"]="SPILLER",["VISUALS"]="VISUELT",["PLAYERS"]="SPILLERE",["SABOTAGES"]="SABOTASJER",["HOST ONLY"]="HOST",["OUTFITS"]="ANTREKK",["VOTEKICK"]="VOTEKICK",["MENU"]="MENY",["MAPS"]="KART",["ANIMATIONS"]="ANIMASJONER",["INFORMATION"]="INFO",["KEYBINDS"]="TASTER",["WELCOME"]="VELKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menyspråk:",["FPS Limit"]="FPS-grense",["Chat History"]="Chat-historikk",["History:"]="Historikk:",["History size:"]="Historikkstørrelse:",["CHAT UTILITY"]="CHAT-VERKTØY",["Always Show Chat"]="Vis alltid chat",["Read Ghost Chat"]="Les spøkelseschat",["Extended Chat"]="Utvidet chat",["Fast Chat"]="Rask chat",["Unlock Extra Characters"]="Tillat ekstra tegn",["Spell Check"]="Stavekontroll",["Clipboard"]="Utklippstavle",["Save Chat Log"]="Lagre chatlogg",["Dark Chat Theme"]="Mørkt chattema",["Enable /color"]="Aktiver /color",["Block Fortegreen"]="Blokker Fortegreen",["Allow Duplicate Colors"]="Tillat like farger",["Auto Ghost After Start"]="Auto-spøkelse etter start",["FAVORITE OUTFITS"]="FAVORITTANTREKK",["Slot"]="Slot",["Empty"]="Tom",["Apply"]="Bruk",["Save Mine"]="Lagre min",["Save Selected"]="Lagre valgt",["Saved slot"]="Slot lagret",["Applied slot"]="Slot brukt",["Cleared slot"]="Slot tømt",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban plattformer fra TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-grense:",["RPC Local Drop"]="RPC lokal drop",["RPC Host Ban"]="RPC host-ban" }, - ["uk"] = new Dictionary { ["GENERAL"]="ЗАГАЛЬНЕ",["SELF"]="ГРАВЕЦЬ",["VISUALS"]="ВІЗУАЛ",["PLAYERS"]="ГРАВЦІ",["SABOTAGES"]="САБОТАЖІ",["HOST ONLY"]="ХОСТ",["OUTFITS"]="ОДЯГ",["VOTEKICK"]="КІК",["MENU"]="МЕНЮ",["MAPS"]="КАРТИ",["ANIMATIONS"]="АНІМАЦІЇ",["INFORMATION"]="ІНФОРМАЦІЯ",["KEYBINDS"]="БІНДИ",["WELCOME"]="ВІТАННЯ",["CREDITS"]="АВТОРИ",["Menu language:"]="Мова меню:",["FPS Limit"]="Ліміт FPS",["Chat History"]="Історія чату",["History:"]="Історія:",["History size:"]="Розмір історії:",["CHAT UTILITY"]="УТИЛІТИ ЧАТУ",["Always Show Chat"]="Завжди показувати чат",["Read Ghost Chat"]="Читати чат привидів",["Extended Chat"]="Розширений чат",["Fast Chat"]="Швидкий чат",["Unlock Extra Characters"]="Дозволити всі символи",["Spell Check"]="Перевірка орфографії",["Clipboard"]="Буфер обміну",["Save Chat Log"]="Зберігати лог чату",["Dark Chat Theme"]="Темна тема чату",["Enable /color"]="Увімкнути /color",["Block Fortegreen"]="Блок Fortegreen",["Allow Duplicate Colors"]="Дозволити однакові кольори",["Auto Ghost After Start"]="Авто-привид після старту",["FAVORITE OUTFITS"]="УЛЮБЛЕНІ ОБРАЗИ",["Slot"]="Слот",["Empty"]="Пусто",["Apply"]="Надіти",["Save Mine"]="Зберегти мій",["Save Selected"]="Зберегти вибраний",["Saved slot"]="Слот збережено",["Applied slot"]="Слот застосовано",["Cleared slot"]="Слот очищено",["Auto-Ban Platform Spoof (Host)"]="Авто-бан Platform Spoof",["Ban Custom Platforms From TXT"]="Бан платформ з TXT",["RPC Anti-Cheat"]="RPC античит",["RPC limit:"]="RPC ліміт:",["RPC Local Drop"]="RPC локальний дроп",["RPC Host Ban"]="RPC бан хоста" }, - ["el"] = new Dictionary { ["GENERAL"]="ΓΕΝΙΚΑ",["SELF"]="ΠΑΙΚΤΗΣ",["VISUALS"]="ΟΠΤΙΚΑ",["PLAYERS"]="ΠΑΙΚΤΕΣ",["SABOTAGES"]="ΣΑΜΠΟΤΑΖ",["HOST ONLY"]="HOST",["OUTFITS"]="ΣΤΟΛΕΣ",["VOTEKICK"]="VOTEKICK",["MENU"]="ΜΕΝΟΥ",["MAPS"]="ΧΑΡΤΕΣ",["ANIMATIONS"]="ANIMATIONS",["INFORMATION"]="ΠΛΗΡΟΦΟΡΙΕΣ",["KEYBINDS"]="ΠΛΗΚΤΡΑ",["WELCOME"]="ΚΑΛΩΣ ΗΡΘΕΣ",["CREDITS"]="CREDITS",["Menu language:"]="Γλώσσα μενού:",["FPS Limit"]="Όριο FPS",["Chat History"]="Ιστορικό chat",["History:"]="Ιστορικό:",["History size:"]="Μέγεθος ιστορικού:",["CHAT UTILITY"]="ΕΡΓΑΛΕΙΑ CHAT",["Always Show Chat"]="Πάντα εμφάνιση chat",["Read Ghost Chat"]="Ανάγνωση ghost chat",["Extended Chat"]="Εκτεταμένο chat",["Fast Chat"]="Γρήγορο chat",["Unlock Extra Characters"]="Επιπλέον χαρακτήρες",["Spell Check"]="Ορθογραφία",["Clipboard"]="Πρόχειρο",["Save Chat Log"]="Αποθήκευση chat log",["Dark Chat Theme"]="Σκούρο chat",["Enable /color"]="Ενεργοποίηση /color",["Block Fortegreen"]="Block Fortegreen",["Allow Duplicate Colors"]="Να επιτρέπονται ίδια χρώματα",["Auto Ghost After Start"]="Auto ghost μετά την έναρξη",["FAVORITE OUTFITS"]="ΑΓΑΠΗΜΕΝΕΣ ΣΤΟΛΕΣ",["Slot"]="Θέση",["Empty"]="Άδειο",["Apply"]="Εφαρμογή",["Save Mine"]="Αποθ. δικό μου",["Save Selected"]="Αποθ. επιλογής",["Saved slot"]="Θέση αποθηκεύτηκε",["Applied slot"]="Θέση εφαρμόστηκε",["Cleared slot"]="Θέση καθαρίστηκε",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban platforms από TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="Όριο RPC:",["RPC Local Drop"]="RPC local drop",["RPC Host Ban"]="RPC host ban" }, - ["zh"] = new Dictionary { ["GENERAL"]="常规",["SELF"]="玩家",["VISUALS"]="视觉",["PLAYERS"]="玩家",["SABOTAGES"]="破坏",["HOST ONLY"]="房主",["OUTFITS"]="装扮",["VOTEKICK"]="投票踢人",["MENU"]="菜单",["MAPS"]="地图",["ANIMATIONS"]="动画",["INFORMATION"]="信息",["KEYBINDS"]="按键",["WELCOME"]="欢迎",["CREDITS"]="鸣谢",["Menu language:"]="菜单语言:",["FPS Limit"]="FPS 限制",["Chat History"]="聊天历史",["History:"]="历史:",["History size:"]="历史大小:",["CHAT UTILITY"]="聊天工具",["Always Show Chat"]="始终显示聊天",["Read Ghost Chat"]="读取幽灵聊天",["Extended Chat"]="扩展聊天",["Fast Chat"]="快速聊天",["Unlock Extra Characters"]="允许额外字符",["Spell Check"]="拼写检查",["Clipboard"]="剪贴板",["Save Chat Log"]="保存聊天日志",["Dark Chat Theme"]="深色聊天主题",["Enable /color"]="启用 /color",["Block Fortegreen"]="阻止 Fortegreen",["Allow Duplicate Colors"]="允许重复颜色",["Auto Ghost After Start"]="开始后自动幽灵",["FAVORITE OUTFITS"]="收藏装扮",["Slot"]="槽位",["Empty"]="空",["Apply"]="应用",["Save Mine"]="保存自己",["Save Selected"]="保存选中",["Saved slot"]="槽位已保存",["Applied slot"]="槽位已应用",["Cleared slot"]="槽位已清空",["Auto-Ban Platform Spoof (Host)"]="自动封禁平台伪装",["Ban Custom Platforms From TXT"]="从 TXT 封禁自定义平台",["RPC Anti-Cheat"]="RPC 反作弊",["RPC limit:"]="RPC 限制:",["RPC Local Drop"]="RPC 本地丢弃",["RPC Host Ban"]="RPC 房主封禁" }, - ["ja"] = new Dictionary { ["GENERAL"]="一般",["SELF"]="プレイヤー",["VISUALS"]="表示",["PLAYERS"]="プレイヤー",["SABOTAGES"]="サボタージュ",["HOST ONLY"]="ホスト",["OUTFITS"]="衣装",["VOTEKICK"]="投票キック",["MENU"]="メニュー",["MAPS"]="マップ",["ANIMATIONS"]="アニメーション",["INFORMATION"]="情報",["KEYBINDS"]="キー設定",["WELCOME"]="ようこそ",["CREDITS"]="クレジット",["Menu language:"]="メニュー言語:",["FPS Limit"]="FPS制限",["Chat History"]="チャット履歴",["History:"]="履歴:",["History size:"]="履歴サイズ:",["CHAT UTILITY"]="チャット機能",["Always Show Chat"]="常にチャット表示",["Read Ghost Chat"]="ゴーストチャット読む",["Extended Chat"]="拡張チャット",["Fast Chat"]="高速チャット",["Unlock Extra Characters"]="追加文字を許可",["Spell Check"]="スペルチェック",["Clipboard"]="クリップボード",["Save Chat Log"]="チャットログ保存",["Dark Chat Theme"]="ダークチャット",["Enable /color"]="/color 有効",["Block Fortegreen"]="Fortegreen ブロック",["Allow Duplicate Colors"]="同じ色を許可",["Auto Ghost After Start"]="開始後に自動ゴースト",["FAVORITE OUTFITS"]="お気に入り衣装",["Slot"]="スロット",["Empty"]="空",["Apply"]="適用",["Save Mine"]="自分を保存",["Save Selected"]="選択を保存",["Saved slot"]="スロット保存",["Applied slot"]="スロット適用",["Cleared slot"]="スロット消去",["Auto-Ban Platform Spoof (Host)"]="平台偽装を自動BAN",["Ban Custom Platforms From TXT"]="TXTから平台BAN",["RPC Anti-Cheat"]="RPCアンチチート",["RPC limit:"]="RPC制限:",["RPC Local Drop"]="RPCローカル破棄",["RPC Host Ban"]="RPCホストBAN" }, - ["ko"] = new Dictionary { ["GENERAL"]="일반",["SELF"]="플레이어",["VISUALS"]="비주얼",["PLAYERS"]="플레이어",["SABOTAGES"]="사보타주",["HOST ONLY"]="호스트",["OUTFITS"]="의상",["VOTEKICK"]="투표킥",["MENU"]="메뉴",["MAPS"]="맵",["ANIMATIONS"]="애니메이션",["INFORMATION"]="정보",["KEYBINDS"]="키 설정",["WELCOME"]="환영",["CREDITS"]="크레딧",["Menu language:"]="메뉴 언어:",["FPS Limit"]="FPS 제한",["Chat History"]="채팅 기록",["History:"]="기록:",["History size:"]="기록 크기:",["CHAT UTILITY"]="채팅 도구",["Always Show Chat"]="항상 채팅 표시",["Read Ghost Chat"]="유령 채팅 읽기",["Extended Chat"]="확장 채팅",["Fast Chat"]="빠른 채팅",["Unlock Extra Characters"]="추가 문자 허용",["Spell Check"]="맞춤법 검사",["Clipboard"]="클립보드",["Save Chat Log"]="채팅 로그 저장",["Dark Chat Theme"]="어두운 채팅 테마",["Enable /color"]="/color 활성화",["Block Fortegreen"]="Fortegreen 차단",["Allow Duplicate Colors"]="중복 색상 허용",["Auto Ghost After Start"]="시작 후 자동 유령",["FAVORITE OUTFITS"]="즐겨찾기 의상",["Slot"]="슬롯",["Empty"]="비어 있음",["Apply"]="적용",["Save Mine"]="내 것 저장",["Save Selected"]="선택 저장",["Saved slot"]="슬롯 저장됨",["Applied slot"]="슬롯 적용됨",["Cleared slot"]="슬롯 삭제됨",["Auto-Ban Platform Spoof (Host)"]="플랫폼 위장 자동 밴",["Ban Custom Platforms From TXT"]="TXT 커스텀 플랫폼 밴",["RPC Anti-Cheat"]="RPC 안티치트",["RPC limit:"]="RPC 제한:",["RPC Local Drop"]="RPC 로컬 드롭",["RPC Host Ban"]="RPC 호스트 밴" } - }; - - private static readonly string[] menuTranslationFixKeys = - { - "ANTI CHEAT", "AUTO HOST", "LOBBY CONTROLS", "ROLE MANAGER", "PUNISHMENT SYSTEM", "Mode:", - "RPC PROTECTIONS", "Block Spoof RPC", "Block Sabotage & Meetings", "Block Game RPC in Lobby", - "Auto-Ban Platform Spoof (Host)", "Ban Custom Platforms From TXT", "Block Meeting RPC Flood", - "Block Chat RPC Flood", "OTHER PROTECTIONS", "Disable Vote Kicks (Host)", "Auto-Kick Fortegreen", - "Auto-Ban Broken FriendCode (Host)", "BAN LIST", "Auto-Ban Blacklisted Players", "Enter Friend Code", - "ADD", "Ban list is empty." - }; - - private static readonly Dictionary menuTranslationFixes = new Dictionary - { - ["de"] = new[] { "ANTI-CHEAT", "AUTO-HOST", "LOBBY-STEUERUNG", "ROLLENMANAGER", "STRAFSYSTEM", "Modus:", "RPC-SCHUTZ", "Spoof-RPC blockieren", "Sabotage & Meetings blockieren", "Spiel-RPC in Lobby blockieren", "Plattform-Spoof auto-bannen (Host)", "Custom-Plattformen aus TXT bannen", "Meeting-RPC-Flood blockieren", "Chat-RPC-Flood blockieren", "WEITERER SCHUTZ", "Vote-Kicks deaktivieren (Host)", "Fortegreen automatisch kicken", "Defekten FriendCode auto-bannen (Host)", "BAN-LISTE", "Spieler aus Ban-Liste auto-bannen", "Friend Code eingeben", "HINZUFÜGEN", "Ban-Liste ist leer." }, - ["fr"] = new[] { "ANTI-TRICHE", "HÔTE AUTO", "CONTRÔLES LOBBY", "GESTION DES RÔLES", "SYSTÈME DE SANCTIONS", "Mode :", "PROTECTIONS RPC", "Bloquer RPC spoof", "Bloquer sabotages et meetings", "Bloquer RPC de jeu en lobby", "Auto-ban spoof plateforme (Hôte)", "Ban plateformes custom TXT", "Bloquer flood RPC meeting", "Bloquer flood RPC chat", "AUTRES PROTECTIONS", "Désactiver vote-kicks (Hôte)", "Auto-kick Fortegreen", "Auto-ban FriendCode cassé (Hôte)", "LISTE DE BAN", "Auto-ban joueurs listés", "Entrer Friend Code", "AJOUTER", "La liste de ban est vide." }, - ["es"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROLES DE LOBBY", "GESTOR DE ROLES", "SISTEMA DE SANCIONES", "Modo:", "PROTECCIONES RPC", "Bloquear RPC spoof", "Bloquear sabotajes y reuniones", "Bloquear RPC de juego en lobby", "Auto-ban spoof de plataforma (Host)", "Ban de plataformas custom desde TXT", "Bloquear flood RPC de reunión", "Bloquear flood RPC de chat", "OTRAS PROTECCIONES", "Desactivar vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode roto (Host)", "LISTA DE BAN", "Auto-ban jugadores en lista", "Introducir Friend Code", "AÑADIR", "La lista de ban está vacía." }, - ["it"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROLLI LOBBY", "GESTORE RUOLI", "SISTEMA PUNIZIONI", "Modalità:", "PROTEZIONI RPC", "Blocca RPC spoof", "Blocca sabotaggi e meeting", "Blocca RPC di gioco in lobby", "Auto-ban spoof piattaforma (Host)", "Ban piattaforme custom da TXT", "Blocca flood RPC meeting", "Blocca flood RPC chat", "ALTRE PROTEZIONI", "Disattiva vote-kick (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode rotto (Host)", "LISTA BAN", "Auto-ban giocatori in lista", "Inserisci Friend Code", "AGGIUNGI", "La lista ban è vuota." }, - ["pt"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROLES DO LOBBY", "GERENCIADOR DE FUNÇÕES", "SISTEMA DE PUNIÇÕES", "Modo:", "PROTEÇÕES RPC", "Bloquear RPC spoof", "Bloquear sabotagens e reuniões", "Bloquear RPC de jogo no lobby", "Auto-ban spoof de plataforma (Host)", "Ban plataformas custom do TXT", "Bloquear flood RPC de reunião", "Bloquear flood RPC de chat", "OUTRAS PROTEÇÕES", "Desativar vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode quebrado (Host)", "LISTA DE BAN", "Auto-ban jogadores listados", "Inserir Friend Code", "ADICIONAR", "A lista de ban está vazia." }, - ["pl"] = new[] { "ANTI-CHEAT", "AUTO HOST", "KONTROLA LOBBY", "MENEDŻER RÓL", "SYSTEM KAR", "Tryb:", "OCHRONA RPC", "Blokuj spoof RPC", "Blokuj sabotaże i spotkania", "Blokuj RPC gry w lobby", "Auto-ban spoof platformy (Host)", "Ban platform custom z TXT", "Blokuj flood RPC spotkania", "Blokuj flood RPC czatu", "INNA OCHRONA", "Wyłącz vote-kicki (Host)", "Auto-kick Fortegreen", "Auto-ban uszkodzony FriendCode (Host)", "LISTA BANÓW", "Auto-ban graczy z listy", "Wpisz Friend Code", "DODAJ", "Lista banów jest pusta." }, - ["nl"] = new[] { "ANTI-CHEAT", "AUTO-HOST", "LOBBYBEDIENING", "ROLLENBEHEER", "STRAFSYSTEEM", "Modus:", "RPC-BESCHERMING", "Spoof-RPC blokkeren", "Sabotage & meetings blokkeren", "Game-RPC in lobby blokkeren", "Platform-spoof auto-bannen (Host)", "Custom platforms uit TXT bannen", "Meeting-RPC-flood blokkeren", "Chat-RPC-flood blokkeren", "ANDERE BESCHERMING", "Vote-kicks uitschakelen (Host)", "Fortegreen automatisch kicken", "Kapotte FriendCode auto-bannen (Host)", "BANLIJST", "Spelers op banlijst auto-bannen", "Friend Code invoeren", "TOEVOEGEN", "Banlijst is leeg." }, - ["tr"] = new[] { "ANTI-CHEAT", "OTO HOST", "LOBI KONTROLLERİ", "ROL YÖNETİCİSİ", "CEZA SİSTEMİ", "Mod:", "RPC KORUMALARI", "Spoof RPC engelle", "Sabotaj ve toplantıları engelle", "Lobide oyun RPC engelle", "Platform spoof oto-ban (Host)", "TXT özel platform ban", "Toplantı RPC flood engelle", "Sohbet RPC flood engelle", "DİĞER KORUMALAR", "Vote-kick kapat (Host)", "Fortegreen oto-kick", "Bozuk FriendCode oto-ban (Host)", "BAN LİSTESİ", "Listedeki oyuncuları oto-ban", "Friend Code gir", "EKLE", "Ban listesi boş." }, - ["cs"] = new[] { "ANTI-CHEAT", "AUTO HOST", "OVLÁDÁNÍ LOBBY", "SPRÁVCE ROLÍ", "SYSTÉM TRESTŮ", "Režim:", "OCHRANA RPC", "Blokovat spoof RPC", "Blokovat sabotáže a meetingy", "Blokovat herní RPC v lobby", "Auto-ban spoof platformy (Host)", "Ban custom platforem z TXT", "Blokovat meeting RPC flood", "Blokovat chat RPC flood", "DALŠÍ OCHRANA", "Vypnout vote-kicky (Host)", "Auto-kick Fortegreen", "Auto-ban rozbitý FriendCode (Host)", "BAN LIST", "Auto-ban hráčů z listu", "Zadej Friend Code", "PŘIDAT", "Ban list je prázdný." }, - ["ro"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROALE LOBBY", "MANAGER ROLURI", "SISTEM DE PEDEPSE", "Mod:", "PROTECȚII RPC", "Blochează RPC spoof", "Blochează sabotaje și meetinguri", "Blochează RPC de joc în lobby", "Auto-ban spoof platformă (Host)", "Ban platforme custom din TXT", "Blochează flood RPC meeting", "Blochează flood RPC chat", "ALTE PROTECȚII", "Dezactivează vote-kick (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode stricat (Host)", "LISTĂ BAN", "Auto-ban jucători listați", "Introdu Friend Code", "ADAUGĂ", "Lista de ban este goală." }, - ["hu"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBI VEZÉRLÉS", "SZEREPKEZELŐ", "BÜNTETÉSI RENDSZER", "Mód:", "RPC VÉDELEM", "Spoof RPC blokkolása", "Szabotázsok és meetingek blokkolása", "Játék RPC blokkolása lobbyban", "Platform spoof auto-ban (Host)", "Custom platformok bannolása TXT-ből", "Meeting RPC flood blokkolása", "Chat RPC flood blokkolása", "EGYÉB VÉDELEM", "Vote-kick tiltása (Host)", "Fortegreen auto-kick", "Hibás FriendCode auto-ban (Host)", "BAN LISTA", "Listás játékosok auto-banja", "Friend Code megadása", "HOZZÁAD", "A ban lista üres." }, - ["sv"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYKONTROLLER", "ROLLHANTERARE", "STRAFFSYSTEM", "Läge:", "RPC-SKYDD", "Blockera spoof-RPC", "Blockera sabotage och möten", "Blockera spel-RPC i lobby", "Auto-ban plattformsspoof (Host)", "Ban custom-plattformar från TXT", "Blockera meeting RPC-flood", "Blockera chat RPC-flood", "ANNAT SKYDD", "Inaktivera vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban trasig FriendCode (Host)", "BANLISTA", "Auto-ban spelare på lista", "Ange Friend Code", "LÄGG TILL", "Banlistan är tom." }, - ["da"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYKONTROL", "ROLLEMANAGER", "STRAFSYSTEM", "Tilstand:", "RPC-BESKYTTELSE", "Bloker spoof-RPC", "Bloker sabotager og møder", "Bloker spil-RPC i lobby", "Auto-ban platform spoof (Host)", "Ban custom-platforme fra TXT", "Bloker meeting RPC-flood", "Bloker chat RPC-flood", "ANDEN BESKYTTELSE", "Deaktiver vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban defekt FriendCode (Host)", "BANLISTE", "Auto-ban spillere på liste", "Indtast Friend Code", "TILFØJ", "Banlisten er tom." }, - ["fi"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYN HALLINTA", "ROOLIEN HALLINTA", "RANGAISTUSJÄRJESTELMÄ", "Tila:", "RPC-SUOJAUKSET", "Estä spoof RPC", "Estä sabotaasit ja kokoukset", "Estä peli-RPC lobbyssa", "Auto-ban platform spoof (Host)", "Ban custom-alustat TXT:stä", "Estä meeting RPC flood", "Estä chat RPC flood", "MUUT SUOJAUKSET", "Poista vote-kickit käytöstä (Host)", "Auto-kick Fortegreen", "Auto-ban rikkinäinen FriendCode (Host)", "BAN-LISTA", "Auto-ban listatut pelaajat", "Syötä Friend Code", "LISÄÄ", "Ban-lista on tyhjä." }, - ["no"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYKONTROLLER", "ROLLEBEHANDLER", "STRAFFESYSTEM", "Modus:", "RPC-BESKYTTELSE", "Blokker spoof-RPC", "Blokker sabotasje og møter", "Blokker spill-RPC i lobby", "Auto-ban platform spoof (Host)", "Ban custom-plattformer fra TXT", "Blokker meeting RPC-flood", "Blokker chat RPC-flood", "ANNEN BESKYTTELSE", "Deaktiver vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban ødelagt FriendCode (Host)", "BANLISTE", "Auto-ban spillere på liste", "Skriv Friend Code", "LEGG TIL", "Banlisten er tom." }, - ["uk"] = new[] { "АНТИЧИТ", "АВТО ХОСТ", "КЕРУВАННЯ ЛОБІ", "МЕНЕДЖЕР РОЛЕЙ", "СИСТЕМА ПОКАРАНЬ", "Режим:", "ЗАХИСТ RPC", "Блокувати spoof RPC", "Блокувати саботажі та зустрічі", "Блокувати ігрові RPC у лобі", "Авто-бан spoof платформи (Хост)", "Бан кастомних платформ з TXT", "Блокувати flood RPC зустрічі", "Блокувати flood RPC чату", "ІНШИЙ ЗАХИСТ", "Вимкнути vote-kick (Хост)", "Авто-кік Fortegreen", "Авто-бан зламаного FriendCode (Хост)", "БАН-ЛИСТ", "Авто-бан гравців зі списку", "Введіть Friend Code", "ДОДАТИ", "Бан-лист порожній." }, - ["el"] = new[] { "ANTI-CHEAT", "AUTO HOST", "ΕΛΕΓΧΟΙ LOBBY", "ΔΙΑΧΕΙΡΙΣΗ ΡΟΛΩΝ", "ΣΥΣΤΗΜΑ ΠΟΙΝΩΝ", "Λειτουργία:", "ΠΡΟΣΤΑΣΙΕΣ RPC", "Μπλοκ spoof RPC", "Μπλοκ σαμποτάζ και meetings", "Μπλοκ game RPC στο lobby", "Auto-ban platform spoof (Host)", "Ban custom platforms από TXT", "Μπλοκ meeting RPC flood", "Μπλοκ chat RPC flood", "ΑΛΛΕΣ ΠΡΟΣΤΑΣΙΕΣ", "Απενεργοποίηση vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban χαλασμένο FriendCode (Host)", "ΛΙΣΤΑ BAN", "Auto-ban παικτών στη λίστα", "Εισαγωγή Friend Code", "ΠΡΟΣΘΗΚΗ", "Η λίστα ban είναι άδεια." }, - ["zh"] = new[] { "反作弊", "自动房主", "大厅控制", "身份管理", "处罚系统", "模式:", "RPC 防护", "阻止 Spoof RPC", "阻止破坏和会议", "阻止大厅内游戏 RPC", "自动封禁平台伪装 (房主)", "从 TXT 封禁自定义平台", "阻止会议 RPC 洪泛", "阻止聊天 RPC 洪泛", "其他防护", "禁用投票踢人 (房主)", "自动踢出 Fortegreen", "自动封禁损坏 FriendCode (房主)", "封禁列表", "自动封禁列表玩家", "输入 Friend Code", "添加", "封禁列表为空。" }, - ["ja"] = new[] { "アンチチート", "自動ホスト", "ロビー制御", "ロール管理", "処罰システム", "モード:", "RPC保護", "Spoof RPCをブロック", "サボタージュと会議をブロック", "ロビー中のゲームRPCをブロック", "プラットフォーム偽装を自動BAN (ホスト)", "TXTのカスタムプラットフォームをBAN", "会議RPCフラッドをブロック", "チャットRPCフラッドをブロック", "その他の保護", "投票キックを無効化 (ホスト)", "Fortegreenを自動キック", "壊れたFriendCodeを自動BAN (ホスト)", "BANリスト", "BANリストのプレイヤーを自動BAN", "Friend Codeを入力", "追加", "BANリストは空です。" }, - ["ko"] = new[] { "안티치트", "자동 호스트", "로비 컨트롤", "역할 관리자", "처벌 시스템", "모드:", "RPC 보호", "Spoof RPC 차단", "사보타주와 회의 차단", "로비에서 게임 RPC 차단", "플랫폼 위장 자동 밴 (호스트)", "TXT 커스텀 플랫폼 밴", "회의 RPC 플러드 차단", "채팅 RPC 플러드 차단", "기타 보호", "투표 킥 비활성화 (호스트)", "Fortegreen 자동 킥", "손상된 FriendCode 자동 밴 (호스트)", "밴 목록", "목록의 플레이어 자동 밴", "Friend Code 입력", "추가", "밴 목록이 비어 있습니다." } - }; - - public static byte selectedMorphTargetId = 255; - public static bool unlockCosmetics = true; - public static bool moreLobbyInfo = true; - - public static Dictionary keyBinds = new Dictionary(); - public static string bindingAction = ""; - - public static string L(string eng, string rus) - { - try - { - string configuredLanguage = CurrentMenuLanguageCode(); - if (configuredLanguage == "ru" || configuredLanguage == "uk") - return TryTranslateMenuText(configuredLanguage, eng, configuredLanguage == "ru" ? rus : null); - if (configuredLanguage != "auto") - return TryTranslateMenuText(configuredLanguage, eng, eng); - - if (DestroyableSingleton.InstanceExists) - { - string currentLang = DestroyableSingleton.Instance.currentLanguage.ToString().ToLower(); - if (currentLang.Contains("russian") || currentLang.Contains("ru")) - return rus; - } - } - catch { } - return eng; - } - - private static string TryTranslateMenuText(string languageCode, string englishText, string fallback) - { - try - { - if (string.IsNullOrWhiteSpace(languageCode) || string.IsNullOrEmpty(englishText)) - return fallback ?? englishText; - - if (languageCode == "en") - return englishText; - - if (menuTranslationFixes.TryGetValue(languageCode, out string[] fixedTranslations)) - { - int fixedIndex = Array.IndexOf(menuTranslationFixKeys, englishText); - if (fixedIndex >= 0 && fixedIndex < fixedTranslations.Length && !string.IsNullOrWhiteSpace(fixedTranslations[fixedIndex])) - return fixedTranslations[fixedIndex]; - } - - if (menuTranslations.TryGetValue(languageCode, out Dictionary translations) && - translations.TryGetValue(englishText, out string translated) && - !string.IsNullOrWhiteSpace(translated)) - return translated; - } - catch { } - - return fallback ?? englishText; - } - - public static string CurrentMenuLanguageCode() - { - try - { - int index = Mathf.Clamp(currentMenuLanguageIndex, 0, menuLanguageCodes.Length - 1); - return menuLanguageCodes[index]; - } - catch { } - - return "auto"; - } - - private int currentGeneralSubTab = 0; - private int currentGeneralInfoSubTab = 0; - private string[] generalSubTabs => new string[] { L("INFORMATION", "ИНФОРМАЦИЯ"), L("KEYBINDS", "БИНДЫ") }; - private string[] generalInfoSubTabs => new string[] { L("WELCOME", "WELCOME"), L("CREDITS", "АВТОРЫ") }; - - public static KeyCode menuToggleKey = KeyCode.Insert; - public static KeyCode bindMassMorph = KeyCode.None; - public static KeyCode bindSpawnLobby = KeyCode.None; - public static KeyCode bindDespawnLobby = KeyCode.None; - public static KeyCode bindCloseMeeting = KeyCode.None; - public static KeyCode bindInstaStart = KeyCode.None; - public static KeyCode bindEndCrew = KeyCode.None; - public static KeyCode bindEndImp = KeyCode.None; - public static KeyCode bindEndImpDC = KeyCode.None; - public static KeyCode bindEndHnsDC = KeyCode.None; - public static KeyCode bindToggleTracers = KeyCode.None; - public static KeyCode bindToggleNoClip = KeyCode.None; - public static KeyCode bindToggleFreecam = KeyCode.None; - public static KeyCode bindToggleCameraZoom = KeyCode.None; - public static KeyCode bindKillAll = KeyCode.None; - public static KeyCode bindCallMeeting = KeyCode.None; - public static KeyCode bindTogglePlayerInfo = KeyCode.None; - public static KeyCode bindToggleSeeRoles = KeyCode.None; - public static KeyCode bindToggleSeeGhosts = KeyCode.None; - public static KeyCode bindToggleFullBright = KeyCode.None; - public static KeyCode bindKickAll = KeyCode.None; - public static KeyCode bindFixSabotages = KeyCode.None; - public static KeyCode bindSetAllGhost = KeyCode.None; - public static KeyCode bindSetAllGhostImp = KeyCode.None; - - public static readonly HashSet VanillaRpcIds = new HashSet - { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, - 22, 23, 24, 25, 26, 27, 29, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, - 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 60, 61, 62, 63, 64, 65 - }; - - private bool isScannerActiveFlag = false; - private bool isCamsActiveFlag = false; - public static bool isWaitingForBind = false; - public static bool isWaitBindMassMorph = false; - public static bool isWaitBindSpawnLobby = false; - public static bool isWaitBindDespawnLobby = false; - public static bool isWaitBindCloseMeeting = false; - public static bool isWaitBindInstaStart = false; - public static bool isWaitBindEndCrew = false; - public static bool isWaitBindEndImp = false; - public static bool isWaitBindEndImpDC = false; - public static bool isWaitBindEndHnsDC = false; - public static bool isWaitBindToggleTracers = false; - public static bool isWaitBindToggleNoClip = false; - public static bool isWaitBindToggleFreecam = false; - public static bool isWaitBindToggleCameraZoom = false; - public static bool isWaitBindKillAll = false; - public static bool isWaitBindCallMeeting = false; - public static bool isWaitBindTogglePlayerInfo = false; - public static bool isWaitBindToggleSeeRoles = false; - public static bool isWaitBindToggleSeeGhosts = false; - public static bool isWaitBindToggleFullBright = false; - public static bool isWaitBindKickAll = false; - public static bool isWaitBindFixSabotages = false; - public static bool isWaitBindSetAllGhost = false; - public static bool isWaitBindSetAllGhostImp = false; - public static bool SpoofMenuEnabled = false; - public static int selectedSpoofMenuIndex = 0; - private float uiSpoofTimer = 0f; - public static bool noClip = false; - public static bool tpToCursor = false; - public static bool dragToCursor = false; - public static float walkSpeed = 1f; - - public static bool DetailedJoinInfo = true; - private static List lastPlayerIds = new List(); - private static Dictionary pendingJoinTimers = new Dictionary(); - private static Dictionary playerHistoryKeysById = new Dictionary(); - public class PlayerHistoryEntry - { - public string Name; - public string FriendCode; - public string Puid; - public string Platform; - public string CustomPlatform; - public int Level; - public DateTime FirstSeenUtc; - public DateTime LastSeenUtc; - public DateTime? LeftUtc; - public bool IsOnline; - public List RpcCalls = new List(); - } - private static List playerHistoryEntries = new List(); - private Vector2 playersHistoryScroll = Vector2.zero; - private int currentPlayersSubTab = 0; - private string[] playersSubTabs = { "ACTIONS", "HISTORY" }; - - public static float engineSpeed = 1f; - public static bool invertControls = false; - public static bool autoFollowCursor = false; - - public static int fakeRoleIdx = 0; - public static RoleTypes[] forceRoleOptions = { RoleTypes.Crewmate, RoleTypes.Impostor, RoleTypes.Engineer, RoleTypes.Scientist, RoleTypes.Shapeshifter, RoleTypes.GuardianAngel }; - public static RoleTypes[] roleAssignOptions = { - RoleTypes.Crewmate, RoleTypes.Impostor, RoleTypes.Engineer, RoleTypes.Scientist, RoleTypes.Shapeshifter, RoleTypes.GuardianAngel, - (RoleTypes)8, (RoleTypes)9, (RoleTypes)10, (RoleTypes)12, (RoleTypes)18, RoleTypes.Crewmate, RoleTypes.Impostor - }; - public static string[] roleAssignNames = { - "Crewmate", "Impostor", "Engineer", "Scientist", "Shapeshifter", "Guardian Angel", - "Noisemaker", "Phantom", "Tracker", "Detective", "Viper", "Ghost", "Ghost Imp" - }; - private int targetRoleAssignIdx = 0; - private int allPlayersRoleAssignIdx = 0; - public static bool NoShapeshiftAnim = false; - public static bool EndlessTracking = false; - public static bool NoTrackingCooldown = false; - public static bool UnlimitedInterrogateRange = false; - public static bool noTaskMode = false; - public static bool killAuraHostOnly = false; - public static bool noKillCooldownHostOnly = false; - public static bool spamReportBodies = false; - private float killAuraTimer = 0f; - - public static bool enableColorCommand = false; - public static bool hostChatColor = false; - public static Color hostChatColorValue = new Color32(0, 128, 128, 255); - - public static bool showMenu = false; - public static Rect windowRect = new Rect(100, 100, 750, 480); - public static bool freecam = false; - private static bool _freecamActive = false; - public static bool cameraZoom = false; - public static bool RevealVotesEnabled = false; - - public static Color currentAccentColor = new Color(1f, 0.549f, 0f, 1f); - public static bool rgbMenuMode = false; - private float rgbMenuHue = 0f; - public static bool enableBackground = false; - public static Texture2D customMenuBg = null; - private bool wasShowMenu = false; - private int currentMenuColorIndex = 10; - private string[] menuColorNames = { - "Elysium Blue", "Dark Forest", "Green", "Sea Green", "Mint", "Chartreuse", - "Sun Yellow", "Marigold", "Old Gold", - "Bright Amber", "Vivid Orange", "Dark Orange", - "Blood Red", - "Hot Pink", "Pale Mauve", "Lilac", - "Lavender", "Deep Indigo", "Indigo", - "Med Slate Blue", "Slate Blue", "Navy", "Slate Grey", - "Arctic Cyan", "Neon Lime", "Royal Violet", "Crimson Glow", "Ocean Teal", - "Sunset Orange", "Rose Quartz", "Electric Blue", "Gold Ember", "Emerald Pulse", - "Midnight Steel", "Soft Lavender" - }; - - private Color[] menuColors = { - new Color32(51, 51, 255, 255), new Color(0.192f, 0.290f, 0.196f, 1f), new Color(0f, 0.502f, 0f, 1f), new Color(0.235f, 0.702f, 0.443f, 1f), new Color(0.243f, 0.706f, 0.537f, 1f), new Color(0.498f, 1f, 0f, 1f), - new Color(0.996f, 0.718f, 0.082f, 1f), new Color(0.812f, 0.651f, 0.004f, 1f), - new Color(0.996f, 0.612f, 0.063f, 1f), new Color(0.957f, 0.455f, 0.004f, 1f), new Color(1f, 0.549f, 0f, 1f), - new Color(0.871f, 0.071f, 0.149f, 1f), - new Color(0.992f, 0.529f, 0.859f, 1f), new Color(0.882f, 0.678f, 0.800f, 1f), new Color(0.784f, 0.635f, 0.784f, 1f), - new Color(0.925f, 0.686f, 0.996f, 1f), new Color(0.314f, 0.267f, 0.675f, 1f), new Color(0.294f, 0f, 0.51f, 1f), - new Color(0.482f, 0.408f, 0.933f, 1f), new Color(0.416f, 0.353f, 0.804f, 1f), new Color(0f, 0f, 0.502f, 1f), new Color(0.439f, 0.502f, 0.565f, 1f), - new Color32(72, 219, 251, 255), new Color32(163, 230, 53, 255), new Color32(124, 58, 237, 255), new Color32(239, 68, 68, 255), - new Color32(20, 184, 166, 255), new Color32(249, 115, 22, 255), new Color32(244, 114, 182, 255), new Color32(59, 130, 246, 255), - new Color32(245, 158, 11, 255), new Color32(16, 185, 129, 255), new Color32(51, 65, 85, 255), new Color32(196, 181, 253, 255) - }; - - public static float autoChatEveryoneDelay = 2.5f; - public static string customChatMessage = "test"; - public static bool customChatSpamEnabled = false; - public static float customChatSpamDelay = 2.1f; - public static bool customChatInputFocused = false; - private float customChatSpamTimer = 0f; - - public static float autoMeetingTimer = 0f; - private string[] tabNames => new string[] { L("GENERAL", "ОБЩИЕ"), L("SELF", "ИГРОК"), L("VISUALS", "ВИЗУАЛ"), L("PLAYERS", "ИГРОКИ"), L("SABOTAGES", "САБОТАЖИ"), L("HOST ONLY", "ХОСТ"), L("OUTFITS", "ОДЕЖДА"), L("VOTEKICK", "КИК"), L("MENU", "МЕНЮ"), L("MAPS", "КАРТЫ"), L("ANIMATIONS", "АНИМАЦИИ") }; - public static float speedMultiplier = 1f; - public static bool noSettingLimit = false; - public static float globalRoomColorId = 0f; - - private int currentHostOnlySubTab = 0; - private string[] hostOnlySubTabs => new string[] { L("LOBBY CONTROLS", "КОНТРОЛЬ ЛОББИ"), L("ROLE MANAGER", "МЕНЕДЖЕР РОЛЕЙ"), L("ANTI CHEAT", "АНТИ-ЧИТ"), L("AUTO HOST", "АВТО ХОСТ") }; - public static bool UseSnapToRPC = true; - private static bool isSkeldFlipped = false; - public static float selectedMapSpawnIdx = 0f; - public static string[] mapSpawnNames = { "The Skeld", "Mira HQ", "Polus", "The Airship", "The Fungle" }; - - public static bool FlippedSkeld - { - get { return isSkeldFlipped; } - set - { - if (AmongUsClient.Instance == null || isSkeldFlipped == value) return; - var temp = AmongUsClient.Instance.ShipPrefabs[3]; - AmongUsClient.Instance.ShipPrefabs[3] = AmongUsClient.Instance.ShipPrefabs[0]; - AmongUsClient.Instance.ShipPrefabs[0] = temp; - isSkeldFlipped = value; - } - } - - [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.Start))] - public static class AllowSymbols_TextBoxTMP_Start_Patch - { - public static void Postfix(TextBoxTMP __instance) - { - __instance.allowAllCharacters = ElysiumModMenuGUI.allowLinksAndSymbols; - __instance.AllowSymbols = ElysiumModMenuGUI.allowLinksAndSymbols; - __instance.AllowEmail = ElysiumModMenuGUI.allowLinksAndSymbols; - } - } - [HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] - public static class ChatJailbreak_ChatController_Update_Postfix - { - public static void Postfix(ChatController __instance) - { - if (__instance == null || __instance.freeChatField == null || __instance.freeChatField.textArea == null) return; - - if (ElysiumModMenuGUI.enableFastChat && __instance.timeSinceLastMessage < 0.9f) - { - __instance.timeSinceLastMessage = 0.9f; - } - - __instance.freeChatField.textArea.allowAllCharacters = ElysiumModMenuGUI.allowLinksAndSymbols; - __instance.freeChatField.textArea.AllowSymbols = ElysiumModMenuGUI.allowLinksAndSymbols; - __instance.freeChatField.textArea.AllowEmail = ElysiumModMenuGUI.allowLinksAndSymbols; - - __instance.freeChatField.textArea.characterLimit = ElysiumModMenuGUI.enableExtendedChat ? 120 : 100; - } - } - [HarmonyPatch(typeof(ChatController), nameof(ChatController.SendFreeChat))] - public static class AllowURLS_ChatController_SendFreeChat_Patch - { - public static bool Prefix(ChatController __instance) - { - if (!ElysiumModMenuGUI.allowLinksAndSymbols) return true; - - string text = __instance.freeChatField.Text; - - if (!string.IsNullOrWhiteSpace(text)) - { - PlayerControl.LocalPlayer.RpcSendChat(text); - __instance.freeChatField.textArea.SetText(string.Empty, string.Empty); - } - - return false; - } - } - public static bool autoKickBugs = false; - public static float autoKickTimer = 5f; - public static Dictionary fortegreenTimer = new Dictionary(); - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetColor))] - public static class AutoKickBugs_Patch - { - public static void Postfix(PlayerControl __instance, byte bodyColor) - { - if (!ElysiumModMenuGUI.autoKickBugs || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; - - try - { - if (__instance != null && __instance != PlayerControl.LocalPlayer && __instance.Data != null && !__instance.Data.Disconnected) - { - byte pid = __instance.PlayerId; - string colorName = Palette.GetColorName((int)bodyColor); - - if (bodyColor == 18 || colorName == "???" || bodyColor >= Palette.PlayerColors.Length) - { - if (!ElysiumModMenuGUI.fortegreenTimer.ContainsKey(pid)) - { - ElysiumModMenuGUI.fortegreenTimer[pid] = Time.time + ElysiumModMenuGUI.autoKickTimer; - } - } - else - { - if (ElysiumModMenuGUI.fortegreenTimer.ContainsKey(pid)) - { - ElysiumModMenuGUI.fortegreenTimer.Remove(pid); - } - } - } - } - catch { } - } - } - - [HarmonyPatch(typeof(VoteBanSystem), nameof(VoteBanSystem.HandleRpc))] - public static class VoteBanSystemPatch - { - public static bool Prefix(VoteBanSystem __instance, byte callId, Hazel.MessageReader reader) - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || !ElysiumModMenuGUI.disableVoteKicks) - return true; - - if (callId == 26) - { - try - { - int targetClientId = reader.ReadInt32(); - int voterClientId = reader.ReadInt32(); - string targetName = ResolveVoteClientName(targetClientId); - string voterName = ResolveVoteClientName(voterClientId); - ElysiumModMenuGUI.ShowNotification($"[VOTEKICK BLOCK] {voterName} tried to vote-kick {targetName}"); - } - catch - { - ElysiumModMenuGUI.ShowNotification("[VOTEKICK BLOCK] Vote-kick blocked, sender could not be resolved."); - } - - return false; - } - - return true; - } - - private static string ResolveVoteClientName(int clientId) - { - try - { - if (PlayerControl.AllPlayerControls != null) - { - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc.Data == null) continue; - if (pc.Data.ClientId == clientId || (int)pc.OwnerId == clientId) - { - string name = string.IsNullOrWhiteSpace(pc.Data.PlayerName) ? "Unknown" : pc.Data.PlayerName; - return $"{name} ({clientId})"; - } - } - } - } - catch { } - - return $"client {clientId}"; - } - } - public static bool disableVoteKicks = false; - - - [HarmonyPatch(typeof(ShhhBehaviour), nameof(ShhhBehaviour.PlayAnimation))] - public static class SkipShhh_Perfect_Patch - { - public static bool Prefix(ShhhBehaviour __instance, ref Il2CppSystem.Collections.IEnumerator __result) - { - if (!ElysiumModMenuGUI.skipShhhAnim || __instance == null) return true; - - __instance.gameObject.SetActive(false); - - __result = FastSkip().WrapToIl2Cpp(); - return false; - } - - private static System.Collections.IEnumerator FastSkip() { yield break; } - } - private void SpawnMap(int mapId) - { - try - { - if ((UnityEngine.Object)(object)AmongUsClient.Instance == (UnityEngine.Object)null || AmongUsClient.Instance.ShipPrefabs == null) - return; - - int realMapId = mapId; - if (mapId == 3) realMapId = 4; - if (mapId == 4) realMapId = 5; - - if (realMapId >= AmongUsClient.Instance.ShipPrefabs.Count) - return; - - BepInEx.Unity.IL2CPP.Utils.MonoBehaviourExtensions.StartCoroutine(this, CoSpawnMap(realMapId)); - } - catch { } - } - - [HideFromIl2Cpp] - private System.Collections.IEnumerator CoSpawnMap(int mapId) - { - AmongUsClient.Instance.ShipLoadingAsyncHandle = AmongUsClient.Instance.ShipPrefabs[mapId].InstantiateAsync((Transform)null, false); - yield return AmongUsClient.Instance.ShipLoadingAsyncHandle; - - ShipStatus.Instance = AmongUsClient.Instance.ShipLoadingAsyncHandle.Result.GetComponent(); - ((InnerNetClient)AmongUsClient.Instance).Spawn(((Component)ShipStatus.Instance).GetComponent(), -2, (SpawnFlags)0); - - } - - private void DespawnMap() - { - try - { - if (ShipStatus.Instance != null) - { - ShipStatus.Instance.Despawn(); - } - } - catch { } - } - - private void DespawnCurrentMap() - { - DespawnMap(); - } - - [HideFromIl2Cpp] - private System.Collections.IEnumerator CoSpawnOverlappedMap(int mapId) - { - yield return CoSpawnMap(mapId); - } - public static Dictionary skeldTeleportLocations = new Dictionary() -{ - { "Cafeteria", new Vector2(-0.78f, 2.48f) }, - { "Weapons", new Vector2(8.04f, 1.24f) }, - { "Navigation", new Vector2(16.59f, -2.33f) }, - { "O2", new Vector2(5.15f, -3.12f) }, - { "Shields", new Vector2(10.15f, -7.64f) }, - { "Communications", new Vector2(3.87f, -11.08f) }, - { "Storage", new Vector2(-1.92f, -6.14f) }, - { "Admin", new Vector2(5.31f, -7.42f) }, - { "Electrical", new Vector2(-3.37f, -4.84f) }, - { "Security", new Vector2(-5.69f, -3.07f) }, - { "Medbay", new Vector2(-8.61f, -4.30f) }, - { "Reactor", new Vector2(-20.19f, -2.48f) }, - { "Upper Engine", new Vector2(-16.84f, 2.47f) }, - { "Lower Engine", new Vector2(-16.48f, -7.53f) } -}; - - public static Dictionary miraTeleportLocations = new Dictionary() -{ - { "Launchpad", new Vector2(0.12f, -1.5f) }, - { "Medbay", new Vector2(10.2f, 15.1f) }, - { "Locker Room", new Vector2(12.5f, 18.5f) }, - { "Decontamination", new Vector2(14.8f, 22.0f) }, - { "Reactor", new Vector2(20.5f, 25.0f) }, - { "Laboratory", new Vector2(26.2f, 22.1f) }, - { "Office", new Vector2(24.5f, 15.2f) }, - { "Greenhouse", new Vector2(22.1f, 8.5f) }, - { "Admin", new Vector2(18.2f, 3.1f) }, - { "Cafeteria", new Vector2(14.5f, -2.1f) }, - { "Storage", new Vector2(9.8f, -6.5f) } -}; - - public static Dictionary polusTeleportLocations = new Dictionary() -{ - { "Dropship", new Vector2(0f, 0f) }, - { "Electrical", new Vector2(5.2f, 12.1f) }, - { "O2", new Vector2(-12.4f, 8.5f) }, - { "Security", new Vector2(-18.5f, 2.2f) }, - { "Decontamination", new Vector2(-25.2f, 1.5f) }, - { "Specimen Room", new Vector2(-30.1f, -5.2f) }, - { "Laboratory", new Vector2(-20.5f, -12.1f) }, - { "Medbay", new Vector2(-8.2f, -15.4f) }, - { "Communications", new Vector2(8.5f, -12.1f) }, - { "Weapons", new Vector2(15.2f, -2.5f) } -}; - - public static Dictionary airshipTeleportLocations = new Dictionary() -{ - { "Cockpit", new Vector2(-30f, 15f) }, - { "Vault", new Vector2(-15f, 15f) }, - { "Brig", new Vector2(-5f, 10f) }, - { "Meeting Room", new Vector2(10f, 12f) }, - { "Records", new Vector2(25f, 12f) }, - { "Lounge", new Vector2(35f, 8f) }, - { "Kitchen", new Vector2(25f, -5f) } -}; - - public static Dictionary fungleTeleportLocations = new Dictionary() -{ - { "Beach", new Vector2(0f, -20f) }, - { "Jungle", new Vector2(15f, 10f) }, - { "Lookout", new Vector2(-10f, 25f) }, - { "Laboratory", new Vector2(-25f, 0f) }, - { "Storage", new Vector2(5f, -5f) } -}; - public static int GetCurrentMapId() - { - if (AmongUsClient.Instance == null) return 0; - if (AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay) - { - return AmongUsClient.Instance.TutorialMapId; - } - else - { - if (GameOptionsManager.Instance == null || GameOptionsManager.Instance.CurrentGameOptions == null) return 0; - return GameOptionsManager.Instance.CurrentGameOptions.MapId; - } - } - private Vector2 mapsScrollPos = Vector2.zero; - public static Dictionary GetTeleportLocations() - { - switch (GetCurrentMapId()) - { - case 0: return skeldTeleportLocations; - case 1: return miraTeleportLocations; - case 2: return polusTeleportLocations; - case 3: return skeldTeleportLocations; - case 4: return airshipTeleportLocations; - case 5: return fungleTeleportLocations; - default: return skeldTeleportLocations; - } - } - - public static void TeleportTo(Vector2 position) - { - if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.NetTransform == null) return; - if (UseSnapToRPC) - { - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(position); - } - else - { - PlayerControl.LocalPlayer.NetTransform.SnapTo(position); - } - } - - private int currentTab = 0; - private int targetTabIndex = 0; - private float tabTransitionProgress = 1f; - private Vector2 scrollPosition = Vector2.zero; - private void DrawAutoHostMainTab() - { - GUILayout.BeginHorizontal(); - for (int i = 0; i < autoHostSubTabs.Length; i++) - { - string subTabLabel = i < hostOnlySubTabs.Length ? hostOnlySubTabs[i] : autoHostSubTabs[i]; - if (GUILayout.Button(subTabLabel, currentAutoHostSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) - { - currentAutoHostSubTab = i; - scrollPosition = Vector2.zero; - } - } - GUILayout.EndHorizontal(); - GUILayout.Space(8); - - if (currentAutoHostSubTab == 0) DrawLobbyControls(); - else if (currentAutoHostSubTab == 1) DrawPlayersRoles(); - else if (currentAutoHostSubTab == 2) DrawAntiCheatTab(); - else if (currentAutoHostSubTab == 3) DrawAutoHostTab(); - } - - private void DrawMapsTab() - { - GUILayout.BeginVertical(boxStyle); - - GUILayout.Label(L("LOBBY CONTROL", "КОНТРОЛЬ ЛОББИ"), headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button(L("Spawn Lobby", "Создать лобби"), btnStyle, GUILayout.Height(30))) SpawnLobby(); - if (GUILayout.Button(L("Despawn Lobby", "Удалить лобби"), btnStyle, GUILayout.Height(30))) DespawnLobby(); - GUILayout.EndHorizontal(); - - GUILayout.Space(15); - - GUILayout.Label(L("MAP CONTROL", "КОНТРОЛЬ КАРТЫ"), headerStyle); - isManualMapSpawn = DrawToggle(isManualMapSpawn, L("Manual Map Spawn Mode", "Ручной спавн карты"), 250); - GUILayout.Space(10); - - GUILayout.BeginHorizontal(); - GUILayout.Label(L("Select Map:", "Выбор карты:"), GUILayout.Width(100)); - selectedMapSpawnIdx = (int)GUILayout.HorizontalSlider((int)selectedMapSpawnIdx, 0, mapSpawnNames.Length - 1, sliderStyle, sliderThumbStyle, GUILayout.Width(200)); - GUILayout.Label($"{mapSpawnNames[(int)selectedMapSpawnIdx]}", new GUIStyle(GUI.skin.label) { richText = true }); - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - - GUILayout.BeginHorizontal(); - if (GUILayout.Button(L("Spawn Map", "Создать карту"), activeTabStyle, GUILayout.Height(30))) SpawnMap((int)selectedMapSpawnIdx); - if (GUILayout.Button(L("Despawn Map", "Удалить карту"), btnStyle, GUILayout.Height(30))) DespawnCurrentMap(); - GUILayout.EndHorizontal(); - - GUILayout.Space(15); - - GUILayout.Label(L("ROOM TELEPORTS (IN-GAME)", "ТЕЛЕПОРТЫ ПО КОМНАТАМ (В ИГРЕ)"), headerStyle); - if (ShipStatus.Instance != null && PlayerControl.LocalPlayer != null) - { - mapsScrollPos = GUILayout.BeginScrollView(mapsScrollPos, GUILayout.Height(160)); - var locations = GetTeleportLocations(); - int columns = 3; - int count = 0; - - GUILayout.BeginHorizontal(); - foreach (var loc in locations) - { - if (GUILayout.Button(loc.Key, btnStyle, GUILayout.Width(135), GUILayout.Height(30))) - { - TeleportTo(loc.Value); - ShowNotification($"[TELEPORT] {L("Moved to:", "Перемещен в:")} {loc.Key}"); - } - - count++; - if (count % columns == 0) - { - GUILayout.EndHorizontal(); - GUILayout.BeginHorizontal(); - } - } - GUILayout.EndHorizontal(); - GUILayout.EndScrollView(); - } - else - { - GUILayout.Label($"{L("Teleports are only available when you are on a map.", "Телепорты доступны только когда вы находитесь на карте.")}", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); - } - - GUILayout.EndVertical(); - } - - private void DrawChatSettingsTab() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label(L("CHAT SETTINGS & LOGS", "НАСТРОЙКИ ЧАТА И ЛОГИ"), headerStyle); - GUILayout.Space(10); - - string hexColor = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); - - GUILayout.BeginHorizontal(); - - GUILayout.BeginVertical(GUILayout.Width(300)); - GUILayout.Label($"{L("LOCAL FEATURES", "ЛОКАЛЬНЫЕ ФУНКЦИИ")}", toggleLabelStyle); - GUILayout.Space(6); - alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 280); - GUILayout.Space(2); - readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 280); - GUILayout.Space(4); - DrawGhostChatColorControl(280f); - GUILayout.Space(2); - enableExtendedChat = DrawToggle(enableExtendedChat, L("Extended Chat (120 chars)", "Длинный чат (120 симв.)"), 280); - GUILayout.Space(2); - enableFastChat = DrawToggle(enableFastChat, L("Fast Chat (3.1 to 2.1", "Быстрый чат (c 3.1 до 2.1)"), 280); - GUILayout.Space(2); - allowLinksAndSymbols = DrawToggle(allowLinksAndSymbols, L("Unlock Extra Characters", "Разрешить все символы"), 280); - GUILayout.Space(2); - enableSpellCheck = DrawToggle(enableSpellCheck, L("Spell Check (Basic)", "Проверка орфографии (Базовая)"), 280); - GUILayout.EndVertical(); - - GUILayout.BeginVertical(GUILayout.ExpandWidth(true)); - GUILayout.Label($"{L("UTILITY OPTIONS", "УТИЛИТЫ")}", toggleLabelStyle); - GUILayout.Space(6); - enableChatHistory = DrawToggle(enableChatHistory, L("Chat History (Up/Down)", "История чата (Стрелочки)"), 280); - GUILayout.Space(2); - GUILayout.BeginHorizontal(); - GUILayout.Label($"{L("History size:", "Размер истории:")} {chatHistoryLimit}", new GUIStyle(toggleLabelStyle) { richText = true }, GUILayout.Width(130)); - chatHistoryLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(chatHistoryLimit, 5f, 80f, sliderStyle, sliderThumbStyle, GUILayout.Width(145)), 5, 80); - TrimChatHistoryToLimit(); - GUILayout.EndHorizontal(); - GUILayout.Space(2); - enableClipboard = DrawToggle(enableClipboard, L("Clipboard (Ctrl+C/V)", "Буфер обмена (Ctrl+C/V)"), 280); - GUILayout.Space(2); - enableChatLog = DrawToggle(enableChatLog, L("Save Chat Log to File", "Сохранять лог чата в файл"), 280); - GUILayout.Space(2); - enableChatDarkMode = DrawToggle(enableChatDarkMode, L("Dark Chat Theme", "Темная тема чата"), 280); - if (enableChatDarkMode && GUILayout.Button(L("Turn Off Dark Chat", "Выключить темный чат"), btnStyle, GUILayout.Width(180), GUILayout.Height(24))) - { - enableChatDarkMode = false; - SaveConfig(); - } - - GUILayout.Space(8); - - GUILayout.Label($"{L("HOST LOBBY OPTIONS", "НАСТРОЙКИ ХОСТА")}", toggleLabelStyle); - GUILayout.Space(6); - enableColorCommand = DrawToggle(enableColorCommand, L("Enable /color command", "Разрешить команду /color"), 280); - GUILayout.Space(2); - blockFortegreenChat = DrawToggle(blockFortegreenChat, L("Block Fortegreen Chat", "Запрет чата Fortegreen"), 280); - GUILayout.Space(2); - blockRainbowChat = DrawToggle(blockRainbowChat, L("Block Rainbow Chat", "Запрет радужного чата"), 280); - GUILayout.EndVertical(); - - GUILayout.EndHorizontal(); - - GUILayout.Space(12); - - GUILayout.Label($"{L("CHAT SENDER", "ОТПРАВКА ЧАТА")}", toggleLabelStyle); - GUILayout.Space(6); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Space(6); - - GUIStyle macFieldStyle = new GUIStyle(GUI.skin.textField) - { - fontSize = 12, - alignment = TextAnchor.MiddleLeft - }; - macFieldStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); - macFieldStyle.padding = new RectOffset(); - macFieldStyle.padding.left = 12; - macFieldStyle.padding.right = 12; - macFieldStyle.padding.top = 8; - macFieldStyle.padding.bottom = 8; - macFieldStyle.margin = new RectOffset(); - macFieldStyle.margin.left = 4; - macFieldStyle.margin.right = 4; - macFieldStyle.margin.top = 4; - macFieldStyle.margin.bottom = 4; - - Rect chatInputRect = GUILayoutUtility.GetRect(10f, 34f, GUILayout.ExpandWidth(true), GUILayout.Height(34)); - GUI.Box(chatInputRect, string.Empty, macFieldStyle); - - string drawText = string.IsNullOrEmpty(customChatMessage) - ? L("Type a message...", "Введите сообщение...") - : customChatMessage; - - if (customChatInputFocused && (Time.unscaledTime % 1f) < 0.5f) - drawText += "|"; - - GUIStyle chatInputTextStyle = new GUIStyle(GUI.skin.label) - { - alignment = TextAnchor.MiddleLeft, - clipping = TextClipping.Clip, - richText = false, - fontSize = 12 - }; - chatInputTextStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); - - Rect textRect = new Rect(chatInputRect.x + 12f, chatInputRect.y + 4f, chatInputRect.width - 24f, chatInputRect.height - 8f); - GUI.Label(textRect, drawText, chatInputTextStyle); - - Event e = Event.current; - if (e != null) - { - if (e.type == EventType.MouseDown) - { - customChatInputFocused = chatInputRect.Contains(e.mousePosition); - if (customChatInputFocused) e.Use(); - } - else if (customChatInputFocused && e.type == EventType.KeyDown) - { - if (HandleClipboardShortcut(e, ref customChatMessage, 120)) - { - } - else if (e.keyCode == KeyCode.Backspace) - { - if (!string.IsNullOrEmpty(customChatMessage)) - customChatMessage = customChatMessage.Substring(0, customChatMessage.Length - 1); - e.Use(); - } - else if (e.keyCode == KeyCode.Escape) - { - customChatInputFocused = false; - e.Use(); - } - else if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) - { - TrySendCustomChatMessage(customChatMessage); - e.Use(); - } - else if (!char.IsControl(e.character)) - { - if (customChatMessage == null) customChatMessage = string.Empty; - if (customChatMessage.Length < 120) - customChatMessage += e.character; - e.Use(); - } - } - } - - GUILayout.Space(10); - - GUILayout.BeginHorizontal(GUILayout.Height(30)); - if (GUILayout.Button(L("Send Chat", "Отправить"), btnStyle, GUILayout.Width(150), GUILayout.Height(30))) - TrySendCustomChatMessage(customChatMessage); - - GUILayout.Space(10); - string spamBtnText = customChatSpamEnabled ? L("Spam: ON", "Спам: ВКЛ") : L("Spam: OFF", "Спам: ВЫКЛ"); - if (GUILayout.Button(spamBtnText, customChatSpamEnabled ? activeTabStyle : btnStyle, GUILayout.Width(150), GUILayout.Height(30))) - customChatSpamEnabled = !customChatSpamEnabled; - - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.Space(12); - - GUILayout.BeginHorizontal(GUILayout.Height(24)); - GUILayout.Label($"{L("Delay:", "Задержка:")} {Mathf.Round(customChatSpamDelay * 10f) / 10f}s", new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(122)); - customChatSpamDelay = GUILayout.HorizontalSlider(customChatSpamDelay, 0.5f, 10f, sliderStyle, sliderThumbStyle, GUILayout.Width(300)); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - GUILayout.EndVertical(); - - GUILayout.Space(10); - - GUILayout.Label($"{L("COMMANDS & INFO", "КОМАНДЫ И ИНФОРМАЦИЯ")}", toggleLabelStyle); - GUILayout.Space(4); - - GUILayout.Label($"{L("Whisper:", "Шепот:")} /w, /pm, /msg [Name/ID/Color] [Text]", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 12 }); - GUILayout.Label($"{L("Sends a private message to a player on your screen only.", "Отправляет личное сообщение выбранному игроку (видит только он и вы).")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); - - GUILayout.Space(6); - - GUILayout.Label($"Log Info: {L("ChatLog.txt clears every 3 game restarts.", "Файл ChatLog.txt очищается каждые 3 запуска игры.")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); - - GUILayout.EndVertical(); - } - - private void TrySendCustomChatMessage(string rawText) - { - if (string.IsNullOrWhiteSpace(rawText)) return; - if (PlayerControl.LocalPlayer == null) return; - - try - { - PlayerControl.LocalPlayer.RpcSendChat(rawText.Trim()); - } - catch { } - } - - private static readonly HashSet BasicSpellDictionary = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "hello","hi","gg","wp","yes","no","ok","pls","please","thanks","thx","go","come","start","skip","vote","report","body","kill","who","where","why", - "привет","да","нет","ок","пж","пожалуйста","спасибо","го","старт","скип","голос","репорт","труп","килл","кто","где","почему","лол" - }; - - private static void TrySpellCheckNotify(string text) - { - if (!enableSpellCheck || string.IsNullOrWhiteSpace(text)) return; - if (text.StartsWith("/") || text.StartsWith("!")) return; - - try - { - var words = Regex.Matches(text.ToLower(), @"[a-zа-яё]{3,}"); - List suspicious = new List(); - foreach (Match m in words) - { - string w = m.Value; - if (w.Length < 3) continue; - if (BasicSpellDictionary.Contains(w)) continue; - if (suspicious.Contains(w)) continue; - suspicious.Add(w); - if (suspicious.Count >= 4) break; - } - - if (suspicious.Count > 0) - { - string joined = string.Join(", ", suspicious); - ShowNotification($"[SPELL] Проверь слова: {joined}"); - } - } - catch { } - } - - private static void UpsertPlayerHistory(PlayerControl pc) - { - try - { - if (pc == null || pc.Data == null || pc.Data.Disconnected) return; - string name = string.IsNullOrEmpty(pc.Data.PlayerName) ? "Unknown" : pc.Data.PlayerName; - string fc = GetDisplayedFriendCode(pc.Data); - string puid = "Unknown"; - string platform = "Unknown"; - string customPlatform = ""; - int level = 1; - - try - { - uint rawLevel = pc.Data.PlayerLevel; - if (rawLevel != uint.MaxValue && rawLevel < 10000) level = (int)rawLevel + 1; - } - catch { } - - try - { - var client = AmongUsClient.Instance?.GetClientFromCharacter(pc); - if (client != null) - { - platform = GetPlatform(client); - customPlatform = GetCustomPlatformName(client); - puid = GetClientPuid(client); - } - } - catch { } - - string key = $"{fc}|{puid}|{name}"; - var item = playerHistoryEntries.FirstOrDefault(x => $"{x.FriendCode}|{x.Puid}|{x.Name}" == key); - bool changed = false; - if (item == null) - { - item = new PlayerHistoryEntry - { - Name = name, - FriendCode = fc, - Puid = puid, - Platform = platform, - CustomPlatform = customPlatform, - Level = level, - FirstSeenUtc = DateTime.UtcNow, - LastSeenUtc = DateTime.UtcNow, - IsOnline = true - }; - playerHistoryEntries.Add(item); - changed = true; - } - else - { - changed = item.Name != name || - item.FriendCode != fc || - item.Puid != puid || - item.Platform != platform || - item.CustomPlatform != customPlatform || - item.Level != level || - !item.IsOnline || - item.LeftUtc.HasValue; - item.Name = name; - item.FriendCode = fc; - item.Puid = puid; - item.Platform = platform; - item.CustomPlatform = customPlatform; - item.Level = level; - item.LastSeenUtc = DateTime.UtcNow; - item.LeftUtc = null; - item.IsOnline = true; - } - playerHistoryKeysById[pc.PlayerId] = key; - if (changed) WritePlayerHistoryFile(); - } - catch { } - } - - private static string GetCustomPlatformName(ClientData client) - { - try - { - string value = client?.PlatformData?.PlatformName; - if (string.IsNullOrWhiteSpace(value)) return ""; - value = Regex.Replace(value, "<.*?>", string.Empty).Trim(); - if (string.IsNullOrWhiteSpace(value)) return ""; - - string platform = GetPlatform(client); - if (value.Equals(platform, StringComparison.OrdinalIgnoreCase)) return ""; - if (value.Equals(client.PlatformData.Platform.ToString(), StringComparison.OrdinalIgnoreCase)) return ""; - return value; - } - catch { return ""; } - } - - public static string GetClientPuid(ClientData client) - { - if (client == null) return "Unknown"; - - try - { - string direct = client.ProductUserId; - if (!string.IsNullOrWhiteSpace(direct)) return direct.Trim(); - } - catch { } - - string[] memberNames = { "ProductUserId", "productUserId", "Puid", "PUID", "puid", "EosId", "EOSId", "ProductId", "PlayerId" }; - foreach (string memberName in memberNames) - { - try - { - PropertyInfo prop = client.GetType().GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - object value = prop?.GetValue(client, null); - if (value != null && !string.IsNullOrWhiteSpace(value.ToString())) return value.ToString().Trim(); - } - catch { } - - try - { - FieldInfo field = client.GetType().GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - object value = field?.GetValue(client); - if (value != null && !string.IsNullOrWhiteSpace(value.ToString())) return value.ToString().Trim(); - } - catch { } - } - - return "Unknown"; - } - - private static string FormatPlatformHistory(PlayerHistoryEntry entry) - { - if (entry == null) return "Unknown"; - return string.IsNullOrWhiteSpace(entry.CustomPlatform) - ? entry.Platform - : $"{entry.Platform} + custom: {entry.CustomPlatform}"; - } - - private static string PlayerHistoryFilePath() - { - string folder = string.IsNullOrWhiteSpace(Plugin.ElysiumFolder) - ? System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu") - : Plugin.ElysiumFolder; - return System.IO.Path.Combine(folder, "ElysiumPlayerHistory.txt"); - } - - private static void MarkPlayerHistoryLeft(byte playerId) - { - try - { - if (!playerHistoryKeysById.TryGetValue(playerId, out string key)) return; - var item = playerHistoryEntries.FirstOrDefault(x => $"{x.FriendCode}|{x.Puid}|{x.Name}" == key); - if (item == null || !item.IsOnline) return; - - item.IsOnline = false; - item.LeftUtc = DateTime.UtcNow; - item.LastSeenUtc = item.LeftUtc.Value; - WritePlayerHistoryFile(); - } - catch { } - } - - public static void RecordPlayerRpc(PlayerControl pc, byte callId) - { - try - { - if (VanillaRpcIds.Contains(callId)) return; - if (pc == null || pc.Data == null) return; - UpsertPlayerHistory(pc); - - if (!playerHistoryKeysById.TryGetValue(pc.PlayerId, out string key)) return; - var item = playerHistoryEntries.FirstOrDefault(x => $"{x.FriendCode}|{x.Puid}|{x.Name}" == key); - if (item == null) return; - - if (!item.RpcCalls.Contains(callId)) - { - item.RpcCalls.Add(callId); - item.RpcCalls.Sort(); - WritePlayerHistoryFile(); - } - } - catch { } - } - - private static string FormatRpcHistory(PlayerHistoryEntry entry) - { - if (entry == null || entry.RpcCalls == null || entry.RpcCalls.Count == 0) return "нет"; - byte[] customRpcCalls = entry.RpcCalls.Where(x => !VanillaRpcIds.Contains(x)).Distinct().OrderBy(x => x).ToArray(); - if (customRpcCalls.Length == 0) return "нет"; - return string.Join(", ", customRpcCalls.Select(x => x.ToString()).ToArray()); - } - - private static void WritePlayerHistoryFile() - { - try - { - string path = PlayerHistoryFilePath(); - System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)); - - List lines = new List - { - "ElysiumModMenu Player History", - $"Updated UTC: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}", - "" - }; - - foreach (var e in playerHistoryEntries.OrderByDescending(x => x.LastSeenUtc)) - { - string left = e.LeftUtc.HasValue ? e.LeftUtc.Value.ToString("yyyy-MM-dd HH:mm:ss") : "online"; - lines.Add($"Nick: {e.Name}"); - lines.Add($"Level: {e.Level}"); - lines.Add($"FriendCode: {e.FriendCode}"); - lines.Add($"PUID: {e.Puid}"); - lines.Add($"Joined UTC: {e.FirstSeenUtc:yyyy-MM-dd HH:mm:ss}"); - lines.Add($"Left UTC: {left}"); - lines.Add($"Platform: {FormatPlatformHistory(e)}"); - lines.Add($"RPC calls: {FormatRpcHistory(e)}"); - lines.Add(new string('-', 48)); - } - - System.IO.File.WriteAllLines(path, lines.ToArray(), Encoding.UTF8); - } - catch { } - } - - private void TryHostOnlyKillAuraTick() - { - if (!killAuraHostOnly) - { - killAuraTimer = 0f; - return; - } - - if (AmongUsClient.Instance == null) return; - if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) return; - if (PlayerControl.LocalPlayer.Data.IsDead) return; - if (!RoleManager.IsImpostorRole(PlayerControl.LocalPlayer.Data.RoleType)) return; - if (MeetingHud.Instance != null) return; - if (PlayerControl.LocalPlayer.inVent || PlayerControl.LocalPlayer.onLadder) return; - if (!noKillCooldownHostOnly && GetRemainingKillCooldown(PlayerControl.LocalPlayer.PlayerId) > 0.05f) return; - - killAuraTimer += Time.deltaTime; - if (killAuraTimer < 0.05f) return; - - if (PlayerControl.AllPlayerControls == null) return; - - PlayerControl nearestTarget = null; - float nearestDistance = float.MaxValue; - Vector3 localPos = PlayerControl.LocalPlayer.transform.position; - Vector2 localPos2D = new Vector2(localPos.x, localPos.y); - - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null) continue; - if (pc.Data.Disconnected || pc.Data.IsDead) continue; - if (pc.inVent || pc.onLadder) continue; - - Vector3 targetPos = pc.transform.position; - float dist = Vector2.Distance(localPos2D, new Vector2(targetPos.x, targetPos.y)); - if (dist <= 2.2f && dist < nearestDistance) - { - nearestDistance = dist; - nearestTarget = pc; - } - } - - if (nearestTarget == null) return; - - try - { - PlayerControl.LocalPlayer.CmdCheckMurder(nearestTarget); - PlayerControl.LocalPlayer.RpcMurderPlayer(nearestTarget, true); - - if (AmongUsClient.Instance.AmHost) - PlayerControl.LocalPlayer.SetKillTimer(noKillCooldownHostOnly ? 0f : GetConfiguredKillCooldown()); - - killAuraTimer = 0f; - } - catch { } - } - - private void DrawAntiCheatTab() - { - float antiCheatColumnWidth = (windowRect.width - 220f) / 2f; - if (antiCheatColumnWidth < 250f) antiCheatColumnWidth = 250f; - - GUILayout.BeginHorizontal(); - - GUILayout.BeginVertical(boxStyle, GUILayout.Width(antiCheatColumnWidth)); - - GUILayout.Label(L("PUNISHMENT SYSTEM", "СИСТЕМА НАКАЗАНИЙ"), headerStyle); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - GUILayout.Label(L("Mode:", "Режим:"), toggleLabelStyle, GUILayout.Width(60)); - - GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) } }; - - if (GUILayout.Button("<", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) - { - punishmentMode--; - if (punishmentMode < 0) punishmentMode = punishmentNames.Length - 1; - } - - GUILayout.Label(punishmentNames[punishmentMode], middleLabelStyle, GUILayout.ExpandWidth(true), GUILayout.Height(25)); - - if (GUILayout.Button(">", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) - { - punishmentMode++; - if (punishmentMode >= punishmentNames.Length) punishmentMode = 0; - } - GUILayout.EndHorizontal(); - - string modeDesc = punishmentMode switch - { - 0 => "Null: Пакеты блокируются без действий.", - 1 => "Warn: Блокировка + Уведомление на экран.", - 2 => "Kick: Игрок будет исключен из лобби.", - 3 => "Ban: Игрок будет забанен (Host Only).", - _ => "" - }; - GUILayout.Label(modeDesc, new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); - - GUILayout.Space(12); - GUILayout.Label(L("RPC PROTECTIONS", "ЗАЩИТА RPC"), headerStyle); - - blockSpoofRPC = DrawToggle(blockSpoofRPC, L("Block Spoof RPC", "Блокировать spoof RPC"), 250); - GUILayout.Space(5); - blockSabotageRPC = DrawToggle(blockSabotageRPC, L("Block Sabotage & Meetings", "Блокировать саботажи и митинги"), 250); - GUILayout.Space(5); - blockGameRpcInLobby = DrawToggle(blockGameRpcInLobby, L("Block Game RPC in Lobby", "Блокировать игровые RPC в лобби"), 250); - GUILayout.Space(5); - - autoBanPlatformSpoof = DrawToggle(autoBanPlatformSpoof, L("Auto-Ban Platform Spoof (Host)", "Авто-бан Platform Spoof (Хост)"), 250); - GUILayout.Space(5); - banCustomPlatformsFromTxt = DrawToggle(banCustomPlatformsFromTxt, L("Ban Custom Platforms From TXT", "Бан кастом платформ из TXT"), 250); - GUILayout.Space(5); - - blockMeetingFloodRpc = DrawToggle(blockMeetingFloodRpc, L("Block Meeting RPC Flood", "Блокировать флуд RPC митинга"), 250); - GUILayout.Space(5); - blockChatFloodRpc = DrawToggle(blockChatFloodRpc, L("Block Chat RPC Flood", "Блокировать флуд RPC чата"), 250); - GUILayout.Space(5); - enablePasosLimit = DrawToggle(enablePasosLimit, L("RPC Anti-Cheat", "RPC Античит"), 250); - GUILayout.Space(5); - rpcSpamLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(rpcSpamLimit, 10f, 250f, sliderStyle, sliderThumbStyle, GUILayout.Width(250)), 10, 250); - GUILayout.Space(5); - enableLocalPasosBan = DrawToggle(enableLocalPasosBan, L("RPC Local Drop", "RPC локальный дроп"), 250); - GUILayout.Space(5); - enableHostPasosBan = DrawToggle(enableHostPasosBan, L("RPC Host Ban", "RPC бан на хосте"), 250); - GUILayout.Space(15); - GUILayout.Label(L("OTHER PROTECTIONS", "ПРОЧАЯ ЗАЩИТА"), headerStyle); - - disableVoteKicks = DrawToggle(disableVoteKicks, L("Disable Vote Kicks (Host)", "Запрет кика голосованием (Хост)"), 250); - GUILayout.Space(5); - - autoKickBugs = DrawToggle(autoKickBugs, L("Auto-Kick Fortegreen", "Авто-кик багнутых игроков"), 250); - if (autoKickBugs) - { - GUILayout.BeginHorizontal(); - autoKickTimer = GUILayout.HorizontalSlider(autoKickTimer, 1f, 15f, sliderStyle, sliderThumbStyle, GUILayout.Width(170)); - GUILayout.EndHorizontal(); - } - GUILayout.Space(5); - autoBanBrokenFriendCode = DrawToggle(autoBanBrokenFriendCode, L("Auto-Ban Broken FriendCode (Host)", "Авто-бан сломанного FriendCode (Хост)"), 250); - - GUILayout.EndVertical(); - GUILayout.Space(10); - - GUILayout.BeginVertical(boxStyle, GUILayout.Width(antiCheatColumnWidth), GUILayout.ExpandHeight(true)); - GUILayout.Label(L("BAN LIST", "БАН ЛИСТ"), headerStyle); - autoBanEnabled = DrawToggle(autoBanEnabled, L("Auto-Ban Blacklisted Players", "Авто-бан игроков из списка"), 250); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - string defaultBanText = L("Enter Friend Code", "Введите Friend Code"); - string banValue = string.IsNullOrEmpty(banInput) && !isEditingBan ? defaultBanText : banInput; - - if (DrawPseudoInputButton(banValue, isEditingBan, 25f, 46)) - { - isEditingBan = !isEditingBan; - isEditingGhostChatColor = false; - ResetAllBindWaits(); - } - - if (GUILayout.Button(L("ADD", "ДОБАВИТЬ"), btnStyle, GUILayout.Width(75f), GUILayout.Height(25f))) - { - if (!string.IsNullOrWhiteSpace(banInput)) - { - AddToBanList(banInput.Trim(), "Manual", "Unknown", "Manual ban"); - banInput = ""; - isEditingBan = false; - } - } - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - banListScroll = GUILayout.BeginScrollView(banListScroll); - - if (bannedEntries.Count == 0) - { - GUILayout.FlexibleSpace(); - GUILayout.Label($"{L("Ban list is empty.", "Бан лист пуст.")}", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); - GUILayout.FlexibleSpace(); - } - else - { - for (int i = 0; i < bannedEntries.Count; i++) - { - string entry = bannedEntries[i]; - if (string.IsNullOrWhiteSpace(entry)) continue; - - string[] parts = entry.Split('|'); - string disp = parts.Length >= 3 ? $"{parts[2]} ({parts[0]})" : entry; - - GUILayout.BeginHorizontal(boxStyle); - GUILayout.Label(disp, new GUIStyle(GUI.skin.label) { fontSize = 12 }, GUILayout.Width(185)); - GUILayout.FlexibleSpace(); - - GUIStyle redCrossStyle = new GUIStyle(btnStyle); - redCrossStyle.normal.textColor = new Color(1f, 0.3f, 0.3f); - - if (GUILayout.Button("X", redCrossStyle, GUILayout.Width(25), GUILayout.Height(22))) - { - RemoveFromBanList(entry); - break; - } - GUILayout.EndHorizontal(); - } - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - - GUILayout.EndHorizontal(); - } - - public static class ElysiumAnticheat - { - public static void Flag(PlayerControl player, string reason) - { - if (player == null || player.Data == null || player == PlayerControl.LocalPlayer) return; - - string pName = player.Data.PlayerName ?? "Unknown"; - - int mode = ElysiumModMenuGUI.punishmentMode; - - if (mode >= 1) - { - ElysiumModMenuGUI.ShowNotification($"[ANTICHEAT] {pName}: {reason}"); - } - - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - if (mode == 2) - { - AmongUsClient.Instance.KickPlayer(player.OwnerId, false); - } - else if (mode == 3) - { - string fc = string.IsNullOrEmpty(player.Data.FriendCode) ? "Unknown" : player.Data.FriendCode; - string puid = "Unknown"; - try - { - var client = AmongUsClient.Instance.GetClientFromCharacter(player); - if (client != null) puid = GetClientPuid(client); - } - catch { } - - ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Anticheat: {reason}"); - - AmongUsClient.Instance.KickPlayer(player.OwnerId, true); - } - } - } - } - - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] - public static class Anticheat_PlayerControl_RPC - { - private static readonly Dictionary> chatRpcTimes = new Dictionary>(); - private static readonly Dictionary> meetingRpcTimes = new Dictionary>(); - private static readonly HashSet lobbyGameRpcs = new HashSet - { - (byte)RpcCalls.MurderPlayer, - (byte)RpcCalls.ReportDeadBody, - (byte)RpcCalls.StartMeeting, - (byte)RpcCalls.EnterVent, - (byte)RpcCalls.ExitVent, - (byte)RpcCalls.Shapeshift, - (byte)RpcCalls.ProtectPlayer - }; - - private static bool IsFlooded(Dictionary> map, byte playerId, int maxCalls, float windowSeconds) - { - float now = Time.unscaledTime; - if (!map.TryGetValue(playerId, out Queue times)) - { - times = new Queue(); - map[playerId] = times; - } - - times.Enqueue(now); - while (times.Count > 0 && now - times.Peek() > windowSeconds) - times.Dequeue(); - - return times.Count > maxCalls; - } - - public static bool Prefix(PlayerControl __instance, byte callId, Hazel.MessageReader reader) - { - if (__instance != null && __instance != PlayerControl.LocalPlayer && __instance.Data != null && ElysiumModMenuGUI.enablePasosLimit) - { - int clientId = Shield_PasosLimit_Patch.GetKickClientId(__instance, -1); - if (Shield_PasosLimit_Patch.RecordDrop(clientId, __instance, $"PlayerControl RPC {callId} spam")) - return false; - } - - if (!ElysiumModMenuGUI.blockSpoofRPC && - !ElysiumModMenuGUI.blockSabotageRPC && - !ElysiumModMenuGUI.blockGameRpcInLobby && - !ElysiumModMenuGUI.blockChatFloodRpc && - !ElysiumModMenuGUI.blockMeetingFloodRpc) return true; - if (__instance == null || __instance == PlayerControl.LocalPlayer || __instance.Data == null) return true; - - int oldPos = reader.Position; - bool isCheat = false; - string cheatReason = ""; - - try - { - if (ElysiumModMenuGUI.blockGameRpcInLobby && - AmongUsClient.Instance != null && - !AmongUsClient.Instance.IsGameStarted && - lobbyGameRpcs.Contains(callId)) - { - isCheat = true; - cheatReason = $"Game RPC in lobby ({((RpcCalls)callId)})"; - } - - if (!isCheat && ElysiumModMenuGUI.blockChatFloodRpc && - (callId == (byte)RpcCalls.SendChat || callId == (byte)RpcCalls.SendQuickChat)) - { - if (IsFlooded(chatRpcTimes, __instance.PlayerId, ElysiumModMenuGUI.chatRpcLimit, ElysiumModMenuGUI.chatRpcWindow)) - { - isCheat = true; - cheatReason = "Chat RPC flood"; - } - } - - if (!isCheat && ElysiumModMenuGUI.blockMeetingFloodRpc && - (callId == (byte)RpcCalls.StartMeeting || callId == (byte)RpcCalls.ReportDeadBody)) - { - if (IsFlooded(meetingRpcTimes, __instance.PlayerId, ElysiumModMenuGUI.meetingRpcLimit, ElysiumModMenuGUI.meetingRpcWindow)) - { - isCheat = true; - cheatReason = "Meeting RPC flood"; - } - } - - if (!isCheat && ElysiumModMenuGUI.blockSpoofRPC) - { - if (callId == (byte)RpcCalls.SetColor) - { - uint netId = reader.ReadUInt32(); - byte color = reader.ReadByte(); - if (color >= Palette.PlayerColors.Length) { isCheat = true; cheatReason = $"Invalid Color ID ({color})"; } - } - else if (callId == (byte)RpcCalls.SetName || callId == (byte)RpcCalls.CheckName) - { - uint netId = callId == (byte)RpcCalls.SetName ? reader.ReadUInt32() : 0; - string reqName = reader.ReadString(); - if (reqName.Length > 12) { isCheat = true; cheatReason = "Name length too long"; } - if (reqName.Contains("<")) { isCheat = true; cheatReason = "HTML Tags in name"; } - } - else if (callId == (byte)RpcCalls.SetScanner) - { - bool scanning = reader.ReadBoolean(); - if (scanning && RoleManager.IsImpostorRole(__instance.Data.RoleType)) - { isCheat = true; cheatReason = "Scanner activated as Impostor"; } - } - else if (callId == (byte)RpcCalls.PlayAnimation) - { - byte anim = reader.ReadByte(); - if (RoleManager.IsImpostorRole(__instance.Data.RoleType)) - { isCheat = true; cheatReason = "Task Animation as Impostor"; } - } - else if (callId == (byte)RpcCalls.EnterVent || callId == (byte)RpcCalls.ExitVent) - { - if (!__instance.Data.IsDead && __instance.Data.Role != null && !__instance.Data.Role.CanVent) - { isCheat = true; cheatReason = "Vent without vent ability"; } - - if (GameManager.Instance != null && GameManager.Instance.IsHideAndSeek() && RoleManager.IsImpostorRole(__instance.Data.RoleType)) - { isCheat = true; cheatReason = "Venting as Seeker in H&S"; } - } - } - - if (!isCheat && ElysiumModMenuGUI.blockSabotageRPC) - { - if (callId == (byte)RpcCalls.ReportDeadBody) - { - if (GameManager.Instance != null && GameManager.Instance.IsHideAndSeek()) - { isCheat = true; cheatReason = "Reported body in H&S"; } - } - else if (callId == (byte)RpcCalls.SetStartCounter) - { - reader.ReadPackedInt32(); - sbyte counter = reader.ReadSByte(); - - if (__instance.OwnerId != AmongUsClient.Instance.HostId && counter != -1) - { isCheat = true; cheatReason = "Start counter changed by non-host"; } - } - } - } - catch { } - - reader.Position = oldPos; - - if (isCheat) - { - ElysiumAnticheat.Flag(__instance, cheatReason); - return false; - } - - return true; - } - } - - [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.HandleRpc))] - public static class Anticheat_ShipStatus_RPC - { - public static bool Prefix(ShipStatus __instance, byte callId, Hazel.MessageReader reader) - { - if (!ElysiumModMenuGUI.blockSabotageRPC) return true; - - int oldPos = reader.Position; - bool isCheat = false; - string cheatReason = ""; - PlayerControl sender = null; - - try - { - if (callId == (byte)RpcCalls.UpdateSystem) - { - SystemTypes system = (SystemTypes)reader.ReadByte(); - sender = reader.ReadNetObject(); - - if (sender != null && !sender.AmOwner) - { - if (system == SystemTypes.Sabotage) - { - SystemTypes sabSystem = (SystemTypes)reader.ReadByte(); - if (sender.Data != null && !RoleManager.IsImpostorRole(sender.Data.RoleType)) - { isCheat = true; cheatReason = "Triggered Sabotage as Crewmate"; } - } - } - } - else if (callId == (byte)RpcCalls.CloseDoorsOfType) - { - if (GameManager.Instance != null && GameManager.Instance.IsHideAndSeek()) - { isCheat = true; cheatReason = "Closed doors in H&S"; } - } - } - catch { } - - reader.Position = oldPos; - - if (isCheat && sender != null && sender != PlayerControl.LocalPlayer) - { - ElysiumAnticheat.Flag(sender, cheatReason); - return false; - } - - return true; - } - } - public static bool autoChatEveryone = false; - public static bool pendingAutoMeeting = false; - - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckColor))] - public static class AllowDuplicateColors_CheckColor_Patch - { - private static bool applyingDuplicateColor; - - public static bool Prefix(PlayerControl __instance, byte bodyColor) - { - if (applyingDuplicateColor || !ElysiumModMenuGUI.allowDuplicateColors || - __instance == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || - bodyColor == byte.MaxValue) - return true; - - try - { - applyingDuplicateColor = true; - __instance.RpcSetColor(bodyColor); - return false; - } - catch { return true; } - finally { applyingDuplicateColor = false; } - } - } - - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Start))] - public static class Anticheat_Platform_Check - { - public static void Postfix(PlayerControl __instance) - { - if ((!ElysiumModMenuGUI.blockSpoofRPC && !ElysiumModMenuGUI.autoBanPlatformSpoof && !ElysiumModMenuGUI.banCustomPlatformsFromTxt) || - __instance == null || __instance == PlayerControl.LocalPlayer) return; - - try - { - var clientData = AmongUsClient.Instance.GetClientFromCharacter(__instance); - if (clientData == null || clientData.PlatformData == null) return; - - if (ElysiumModMenuGUI.banCustomPlatformsFromTxt && - MatchesPlatformBanTxt(clientData, out string customPlatformName, out string token)) - { - HostBanForPlatform(__instance, $"Custom platform TXT match '{token}' ({customPlatformName})"); - return; - } - - var platform = clientData.PlatformData; - string pName = platform.PlatformName; - ulong xuid = platform.XboxPlatformId; - ulong psid = platform.PsnPlatformId; - - bool isValid = true; - - switch (platform.Platform) - { - case Platforms.StandaloneEpicPC: - case Platforms.StandaloneSteamPC: - case Platforms.StandaloneMac: - case Platforms.StandaloneItch: - case Platforms.IPhone: - case Platforms.Android: - isValid = (pName == "TESTNAME" && xuid == 0 && psid == 0); - break; - case Platforms.StandaloneWin10: - isValid = (pName == "TESTNAME" && xuid != 0 && psid == 0); - break; - case Platforms.Xbox: - isValid = (pName != "TESTNAME" && pName.Length >= 3 && xuid != 0 && psid == 0); - break; - case Platforms.Playstation: - isValid = (pName != "TESTNAME" && xuid == 0 && psid != 0); - break; - case Platforms.Switch: - isValid = (pName != "TESTNAME" && xuid == 0 && psid == 0); - break; - } - - if (!isValid) - { - string reason = $"Platform Spoof detected ({platform.Platform})"; - if (ElysiumModMenuGUI.autoBanPlatformSpoof) - HostBanForPlatform(__instance, reason); - else if (ElysiumModMenuGUI.blockSpoofRPC) - ElysiumAnticheat.Flag(__instance, reason); - } - } - catch { } - } - } - public static class ElysiumAutoLobbyReturn - { - private const float AutoReturnDelaySeconds = 3f; - private const float AutoReturnRetrySeconds = 0.4f; - private const int AutoReturnMaxAttempts = 40; - - private static int trackedEndGameId; - private static int exhaustedEndGameId; - private static int attempt; - private static float nextAttemptAt; - private static bool pending; - - public static void UpdateLogic() - { - if (!ShouldAutoReturn()) - { - ResetState(); - return; - } - if (LobbyBehaviour.Instance != null) - { - ResetState(); - return; - } - - EndGameManager val = UnityEngine.Object.FindObjectOfType(); - if (val != null) - { - int instanceID = val.gameObject.GetInstanceID(); - if (trackedEndGameId != instanceID) - { - trackedEndGameId = instanceID; - exhaustedEndGameId = 0; - attempt = 0; - nextAttemptAt = Time.unscaledTime + AutoReturnDelaySeconds; - pending = true; - } - } - else if (trackedEndGameId == 0) return; - - if (!pending || exhaustedEndGameId == trackedEndGameId || Time.unscaledTime < nextAttemptAt) - return; - - bool flag = false; - if (val != null) - { - flag = TryInvokeEndGameAction(val); - flag = TryClickEndGameButtons(val) || flag; - } - flag = TryClickGlobalReturnButtons() || flag; - - if (LobbyBehaviour.Instance != null) - { - ResetState(); - return; - } - - attempt++; - if (attempt >= AutoReturnMaxAttempts) - pending = false; - else - nextAttemptAt = Time.unscaledTime + AutoReturnRetrySeconds; - } - - public static void ResetState() - { - trackedEndGameId = 0; - exhaustedEndGameId = 0; - attempt = 0; - nextAttemptAt = 0f; - pending = false; - } - - private static bool ShouldAutoReturn() - { - return ElysiumModMenuGUI.AutoReturnLobbyAfterMatch || ElysiumAutoHostService.ShouldReturnAfterMatch; - } - - private static bool TryInvokeEndGameAction(EndGameManager manager) - { - if (manager == null) return false; - string[] methods = new string[] { "Continue", "NextGame", "PlayAgain" }; - for (int i = 0; i < methods.Length; i++) - { - System.Reflection.MethodInfo methodInfo = FindMethodNoWarn(manager.GetType(), methods[i], Type.EmptyTypes); - if (methodInfo != null) - { - try { methodInfo.Invoke(manager, null); return true; } - catch { } - } - } - return false; - } - - private static System.Reflection.MethodInfo FindMethodNoWarn(Type type, string name, Type[] parameters) - { - if (type == null || string.IsNullOrWhiteSpace(name)) return null; - Type[] types = parameters ?? Type.EmptyTypes; - Type t = type; - while (t != null) - { - System.Reflection.MethodInfo method = t.GetMethod(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic, null, types, null); - if (method != null) return method; - t = t.BaseType; - } - return null; - } - - private static bool TryClickEndGameButtons(EndGameManager manager) - { - if (manager == null) return false; - if (TryClickPassiveButtons(manager.GetComponentsInChildren(true), true)) - return true; - return TryClickUnityButtons(manager.GetComponentsInChildren(true), true); - } - - private static bool TryClickGlobalReturnButtons() - { - if (TryClickPassiveButtons(UnityEngine.Object.FindObjectsOfType(), true)) - return true; - return TryClickUnityButtons(UnityEngine.Object.FindObjectsOfType(), true); - } - - private static bool TryClickPassiveButtons(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase buttons, bool onlyActive) - { - if (buttons == null) return false; - foreach (PassiveButton btn in buttons) - { - if (btn == null) continue; - if (onlyActive && (!btn.gameObject.activeInHierarchy || !btn.isActiveAndEnabled)) - continue; - if (!IsLobbyReturnButton(btn.name, btn.GetComponentsInChildren(true))) - continue; - try - { - if (btn.OnClick != null) - { - btn.OnClick.Invoke(); - return true; - } - } - catch { } - } - return false; - } - - private static bool TryClickUnityButtons(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase buttons, bool onlyActive) - { - if (buttons == null) return false; - foreach (UnityEngine.UI.Button btn in buttons) - { - if (btn == null) continue; - if (onlyActive && (!btn.gameObject.activeInHierarchy || !btn.isActiveAndEnabled || !btn.interactable)) - continue; - if (!IsLobbyReturnButton(btn.name, btn.GetComponentsInChildren(true))) - continue; - try - { - if (btn.onClick != null) - { - btn.onClick.Invoke(); - return true; - } - } - catch { } - } - return false; - } - - private static bool IsLobbyReturnButton(string objectName, Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase texts) - { - string input = (objectName ?? string.Empty).ToLowerInvariant(); - if (ContainsAny(input, "exit", "quit", "menu", "back", "leave", "вых", "выйт", "назад")) - return false; - if (ContainsAny(input, "continue", "nextgame", "playagain", "returntolobby", "tolobby", "lobby", "again", "продолж", "занов", "снов", "лобби", "играть", "вернут")) - return true; - if (texts == null) return false; - foreach (TMPro.TMP_Text txt in texts) - { - if (txt == null) continue; - string stripped = StripRichText(txt.text).ToLowerInvariant(); - if (ContainsAny(stripped, "exit", "quit", "menu", "back", "leave", "вых", "выйт", "назад")) - return false; - if (ContainsAny(stripped, "continue", "next game", "play again", "return to lobby", "lobby", "again", "продолж", "занов", "снов", "лобби", "играть", "вернут")) - return true; - } - return false; - } - - private static bool ContainsAny(string input, params string[] tokens) - { - if (string.IsNullOrEmpty(input)) return false; - foreach (string token in tokens) - if (!string.IsNullOrWhiteSpace(token) && input.Contains(token)) - return true; - return false; - } - - private static string StripRichText(string input) - { - if (string.IsNullOrEmpty(input)) return string.Empty; - char[] chars = new char[input.Length]; - int length = 0; - bool inTag = false; - foreach (char c in input) - { - switch (c) - { - case '<': inTag = true; continue; - case '>': inTag = false; continue; - } - if (!inTag) chars[length++] = c; - } - return new string(chars, 0, length); - } - } - - public static class ElysiumAutoHostService - { - public sealed class AutoHostStatusSnapshot - { - public bool Enabled; - public bool IsHost; - public bool IsLobby; - public bool IsInGame; - public string State = string.Empty; - public string LastReason = string.Empty; - public int ConnectedPlayers; - public int ReadyPlayers; - public int RequiredPlayers; - public float CountdownRemainingSeconds; - public float BackoffRemainingSeconds; - public float LobbyAgeSeconds; - public float LobbyLifeRemainingSeconds = -1f; - public bool WaitingForLoadedPlayers; - public bool AutoReturnAfterMatch; - public bool ForceLastMinute; - public string StartMode = string.Empty; - public float EffectiveStartDelaySeconds; - public float WarmupRemainingSeconds; - public float LoadGraceRemainingSeconds; - public bool FastStartActive; - public bool ForceStartActive; - } - - private enum AutoHostState - { - Disabled, Idle, Warmup, WaitingPlayers, WaitingLoad, - Countdown, Starting, InGame, Returning, Backoff, - } - - private const float TickIntervalSeconds = 0.2f; - private const float StartRequestGraceSeconds = 7f; - private const float LobbyLifetimeSeconds = 600f; - private const float LastMinuteStartSeconds = 60f; - private const float NotificationCooldownSeconds = 0.75f; - - private static AutoHostState state = AutoHostState.Disabled; - private static string lastReason = "disabled"; - private static float nextTickAt; - private static float countdownStartedAt = -1f; - private static float activeCountdownDelay = -1f; - private static float backoffUntil = -1f; - private static float lastStartIssuedAt = -1f; - private static float lobbyOpenedAt = -1f; - private static float loadWaitStartedAt = -1f; - private static float lastNotificationAt = -1f; - private static int lobbyGameId = -1; - private static int lastCountdownNotice = -1; - - public static void Tick() - { - float now = Time.unscaledTime; - if (now < nextTickAt) return; - nextTickAt = now + TickIntervalSeconds; - - if (!IsEnabled) - { - ResetLobbyFlow(true); - SetState(AutoHostState.Disabled, "Выключен"); - return; - } - - InnerNetClient client = TryGetClient(); - if (client == null) - { - ResetLobbyFlow(false); - SetState(AutoHostState.Idle, "Клиент недоступен"); - return; - } - - if (!client.AmHost) - { - ResetLobbyFlow(false); - SetState(AutoHostState.Idle, "Ожидаю хост-контекст"); - return; - } - - if (IsEndGameScreen()) - { - ResetLobbyFlow(false); - SetState(ShouldReturnAfterMatch ? AutoHostState.Returning : AutoHostState.InGame, - ShouldReturnAfterMatch ? "Возврат в лобби" : "Матч завершен"); - return; - } - - if (IsInMatch()) - { - ResetLobbyFlow(true); - SetState(AutoHostState.InGame, "Матч идет"); - return; - } - - if (LobbyBehaviour.Instance == null) - { - ResetLobbyFlow(false); - lobbyOpenedAt = -1f; - lobbyGameId = -1; - SetState(AutoHostState.Idle, "Вне лобби"); - return; - } - - TrackLobby(client, now); - TickHostedLobby(client, now); - } - - public static AutoHostStatusSnapshot GetStatusSnapshot() - { - AutoHostStatusSnapshot snapshot = new AutoHostStatusSnapshot - { - Enabled = IsEnabled, - State = FormatState(state), - LastReason = lastReason ?? string.Empty, - RequiredPlayers = RequiredPlayers, - CountdownRemainingSeconds = CountdownRemaining, - BackoffRemainingSeconds = BackoffRemaining, - LobbyAgeSeconds = lobbyOpenedAt > 0f ? Mathf.Max(0f, Time.unscaledTime - lobbyOpenedAt) : 0f, - LobbyLifeRemainingSeconds = LobbyLifeRemaining, - AutoReturnAfterMatch = ShouldReturnAfterMatch, - ForceLastMinute = ForceLastMinuteEnabled, - StartMode = ElysiumModMenuGUI.AutoHostInstantStart ? "Мгновенный" : "Обычный", - EffectiveStartDelaySeconds = EffectiveStartDelay(0), - WarmupRemainingSeconds = WarmupRemaining, - LoadGraceRemainingSeconds = LoadGraceRemaining, - }; - InnerNetClient client = TryGetClient(); - if (client != null) - { - snapshot.IsHost = client.AmHost; - snapshot.IsLobby = LobbyBehaviour.Instance != null; - snapshot.IsInGame = IsInMatch(); - snapshot.ConnectedPlayers = CountLobbyPlayers(client, out int readyPlayers, out _); - snapshot.ReadyPlayers = readyPlayers; - snapshot.WaitingForLoadedPlayers = snapshot.ConnectedPlayers > snapshot.ReadyPlayers; - snapshot.FastStartActive = IsFastStartActive(snapshot.ConnectedPlayers); - snapshot.ForceStartActive = ShouldForceStart(snapshot.ConnectedPlayers, out _); - snapshot.EffectiveStartDelaySeconds = EffectiveStartDelay(snapshot.ConnectedPlayers); - } - return snapshot; - } - - public static void ResetTransientState() - { - nextTickAt = 0f; - ResetLobbyFlow(true); - SetState(IsEnabled ? AutoHostState.Idle : AutoHostState.Disabled, IsEnabled ? "Сброшен" : "Выключен"); - } - - public static string TryStartNow() - { - if (!IsEnabled) return "Автохост выключен."; - InnerNetClient client = TryGetClient(); - if (client == null || !client.AmHost) return "Ручной старт доступен только хосту."; - if (LobbyBehaviour.Instance == null) return "Ручной старт доступен только в лобби."; - GameStartManager manager = TryGetGameStartManager(); - if (manager == null) return "Кнопка старта не найдена."; - - if (!TryConfiguredStart(manager)) - { - EnterBackoff("Ручной старт отклонен"); - return "Старт не сработал."; - } - lastStartIssuedAt = Time.unscaledTime; - countdownStartedAt = -1f; - activeCountdownDelay = -1f; - backoffUntil = -1f; - SetState(AutoHostState.Starting, "Ручной старт"); - Notify("Автохост", "Матч запускается вручную."); - return "Старт отправлен."; - } - - private static void TickHostedLobby(InnerNetClient client, float now) - { - int connectedPlayers = CountLobbyPlayers(client, out int readyPlayers, out string loadingName); - bool forceStart = ShouldForceStart(connectedPlayers, out string forceReason); - float warmupRemaining = WarmupRemaining; - - if (!forceStart && warmupRemaining > 0.05f) - { - countdownStartedAt = -1f; - activeCountdownDelay = -1f; - lastStartIssuedAt = -1f; - lastCountdownNotice = -1; - SetState(AutoHostState.Warmup, $"Прогрев лобби {Mathf.CeilToInt(warmupRemaining)}с"); - return; - } - - bool waitingForLoad = ElysiumModMenuGUI.AutoHostWaitLoadedPlayers && connectedPlayers > readyPlayers; - if (waitingForLoad && !forceStart && !CanBypassLoadWait(now, readyPlayers, connectedPlayers, loadingName)) - { - countdownStartedAt = -1f; - activeCountdownDelay = -1f; - lastStartIssuedAt = -1f; - lastCountdownNotice = -1; - SetState(AutoHostState.WaitingLoad, $"Ожидаю прогрузку {readyPlayers}/{connectedPlayers}: {loadingName}"); - return; - } - if (!waitingForLoad) loadWaitStartedAt = -1f; - - if (lastStartIssuedAt > 0f) - { - if (now - lastStartIssuedAt < StartRequestGraceSeconds) - { - SetState(AutoHostState.Starting, "Старт отправлен"); - return; - } - lastStartIssuedAt = -1f; - EnterBackoff("Старт не подтвердился"); - return; - } - - if (backoffUntil > now) - { - SetState(AutoHostState.Backoff, "Пауза после попытки"); - return; - } - - int requiredPlayers = RequiredPlayers; - bool enoughPlayers = ElysiumModMenuGUI.AutoHostWaitLoadedPlayers ? readyPlayers >= requiredPlayers : connectedPlayers >= requiredPlayers; - bool continueBelowMin = !ElysiumModMenuGUI.AutoHostCancelBelowMin && countdownStartedAt >= 0f && connectedPlayers >= 2; - - if (!forceStart && !enoughPlayers && !continueBelowMin) - { - if (countdownStartedAt >= 0f) - Notify("Автохост", "Отсчет отменен: игроков стало меньше минимума."); - countdownStartedAt = -1f; - activeCountdownDelay = -1f; - lastCountdownNotice = -1; - SetState(AutoHostState.WaitingPlayers, $"Игроки {connectedPlayers}/{requiredPlayers}"); - return; - } - - float delay = EffectiveStartDelay(connectedPlayers); - if (!forceStart && countdownStartedAt < 0f) - { - countdownStartedAt = now; - activeCountdownDelay = delay; - lastCountdownNotice = -1; - SetState(AutoHostState.Countdown, IsFastStartActive(connectedPlayers) ? "Быстрый старт" : "Минимум игроков набран"); - Notify("Автохост", $"Старт через {Mathf.CeilToInt(delay)} с."); - } - - if (!forceStart && now - countdownStartedAt < delay) - { - AnnounceCountdown(delay - (now - countdownStartedAt)); - SetState(AutoHostState.Countdown, "Отсчет"); - return; - } - - GameStartManager manager = TryGetGameStartManager(); - if (manager == null) - { - EnterBackoff("Кнопка старта не найдена"); - return; - } - if (!TryConfiguredStart(manager)) - { - EnterBackoff(forceStart ? "Форс-старт отклонен" : "Старт отклонен"); - return; - } - - countdownStartedAt = -1f; - activeCountdownDelay = -1f; - backoffUntil = -1f; - lastStartIssuedAt = now; - lastCountdownNotice = -1; - SetState(AutoHostState.Starting, forceStart ? forceReason : "Старт матча"); - Notify("Автохост", forceStart ? forceReason : "Минимум набран, запускаю матч."); - } - - private static void TrackLobby(InnerNetClient client, float now) - { - int gameId; - try { gameId = client.GameId; } catch { gameId = 0; } - if (lobbyOpenedAt >= 0f && lobbyGameId == gameId) return; - lobbyOpenedAt = now; - lobbyGameId = gameId; - ResetLobbyFlow(true); - SetState(AutoHostState.WaitingPlayers, "Новое лобби"); - } - - private static void AnnounceCountdown(float remaining) - { - int whole = Mathf.CeilToInt(Mathf.Max(0f, remaining)); - if (whole == lastCountdownNotice) return; - if (whole == 60 || whole == 30 || whole == 15 || whole == 10 || whole == 5 || whole == 3 || whole == 2 || whole == 1) - { - lastCountdownNotice = whole; - Notify("Автохост", $"Старт через {whole} с."); - } - } - - private static bool TryConfiguredStart(GameStartManager manager) - { - if (manager == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || LobbyBehaviour.Instance == null) - return false; - try - { - manager.MinPlayers = 1; - if (ElysiumModMenuGUI.AutoHostInstantStart) - { - manager.startState = GameStartManager.StartingStates.Countdown; - manager.countDownTimer = 0f; - return true; - } - manager.BeginGame(); - return true; - } - catch { return false; } - } - - private static void EnterBackoff(string reason) - { - countdownStartedAt = -1f; - activeCountdownDelay = -1f; - lastStartIssuedAt = -1f; - loadWaitStartedAt = -1f; - lastCountdownNotice = -1; - backoffUntil = Time.unscaledTime + BackoffSeconds; - SetState(AutoHostState.Backoff, reason); - Notify("Автохост: пауза", reason); - } - - private static void ResetLobbyFlow(bool clearBackoff) - { - countdownStartedAt = -1f; - lastStartIssuedAt = -1f; - lastCountdownNotice = -1; - if (clearBackoff) backoffUntil = -1f; - } - - private static void SetState(AutoHostState nextState, string reason) - { - if (!string.IsNullOrWhiteSpace(reason)) lastReason = reason.Trim(); - state = nextState; - } - - private static int CountLobbyPlayers(InnerNetClient client, out int readyPlayers, out string loadingName) - { - readyPlayers = 0; - loadingName = "игрок"; - if (client == null || client.allClients == null) return 0; - - int connected = 0; - try - { - var cursor = client.allClients.GetEnumerator(); - while (cursor.MoveNext()) - { - ClientData data = cursor.Current; - if (data == null || data.Id < 0) continue; - if (IsDisconnected(data)) continue; - connected++; - if (IsReady(data)) readyPlayers++; - else loadingName = CleanName(data.PlayerName); - } - } - catch { return CountReadyPlayerControls(out readyPlayers); } - return connected; - } - - private static int CountReadyPlayerControls(out int readyPlayers) - { - readyPlayers = 0; - try - { - if (PlayerControl.AllPlayerControls == null) return 0; - int count = 0; - var cursor = PlayerControl.AllPlayerControls.GetEnumerator(); - while (cursor.MoveNext()) - { - PlayerControl player = cursor.Current; - if (player == null || player.Data == null || player.Data.Disconnected || player.PlayerId >= 100) continue; - count++; - readyPlayers++; - } - return count; - } - catch { return 0; } - } - - private static bool IsReady(ClientData data) - { - try - { - PlayerControl character = data.Character; - return character != null && character.Data != null && !character.Data.Disconnected && character.PlayerId < 100; - } - catch { return false; } - } - - private static bool IsDisconnected(ClientData data) - { - try { return data.Character != null && data.Character.Data != null && data.Character.Data.Disconnected; } - catch { return false; } - } - - private static GameStartManager TryGetGameStartManager() - { - try { if (DestroyableSingleton.InstanceExists) return DestroyableSingleton.Instance; } catch { } - try { return UnityEngine.Object.FindObjectOfType(); } catch { return null; } - } - - private static InnerNetClient TryGetClient() - { - try { return AmongUsClient.Instance == null ? null : (InnerNetClient)AmongUsClient.Instance; } catch { return null; } - } - - private static bool CanBypassLoadWait(float now, int readyPlayers, int connectedPlayers, string loadingName) - { - if (readyPlayers < RequiredPlayers) { loadWaitStartedAt = -1f; return false; } - int grace = Mathf.Clamp((int)ElysiumModMenuGUI.AutoHostLoadGraceSeconds, 0, 90); - if (grace <= 0) { loadWaitStartedAt = -1f; return false; } - if (loadWaitStartedAt < 0f) loadWaitStartedAt = now; - if (now - loadWaitStartedAt < grace) - { - SetState(AutoHostState.WaitingLoad, $"Жду прогрузку {readyPlayers}/{connectedPlayers}: {loadingName}"); - return false; - } - SetState(AutoHostState.Countdown, "Прогрузка задержалась, старт по готовым"); - return true; - } - - private static bool ShouldForceStart(int connectedPlayers, out string reason) - { - int minPlayers = ForceMinPlayers; - if (ForceLastMinuteEnabled && connectedPlayers >= minPlayers && LobbyLifeRemaining >= 0f && LobbyLifeRemaining <= LastMinuteStartSeconds) - { - reason = "Форс-старт: лобби скоро закроется"; - return true; - } - int forceAfterMinutes = Mathf.Clamp(ElysiumModMenuGUI.AutoHostForceAfterMinutes, 0, 10); - if (forceAfterMinutes > 0 && connectedPlayers >= minPlayers && lobbyOpenedAt > 0f && Time.unscaledTime - lobbyOpenedAt >= forceAfterMinutes * 60f) - { - reason = $"Форс-старт: ожидание {forceAfterMinutes} мин"; - return true; - } - reason = string.Empty; - return false; - } - - private static bool IsFastStartActive(int connectedPlayers) - { - int threshold = Mathf.Clamp(ElysiumModMenuGUI.AutoHostFastStartPlayers, 0, 15); - return threshold > 0 && connectedPlayers >= threshold; - } - - private static float EffectiveStartDelay(int connectedPlayers) - { - float delay = StartDelaySeconds; - if (IsFastStartActive(connectedPlayers)) - delay = Mathf.Min(delay, Mathf.Clamp(ElysiumModMenuGUI.AutoHostFastStartDelaySeconds, 0, 60)); - return delay; - } - - private static bool IsInMatch() => ShipStatus.Instance != null && LobbyBehaviour.Instance == null && !IsEndGameScreen(); - - private static bool IsEndGameScreen() - { - try { return UnityEngine.Object.FindObjectOfType() != null; } catch { return false; } - } - - private static void Notify(string title, string detail) - { - if (!ElysiumModMenuGUI.AutoHostNotifications) return; - float now = Time.unscaledTime; - if (lastNotificationAt > 0f && now - lastNotificationAt < NotificationCooldownSeconds) return; - lastNotificationAt = now; - ElysiumModMenuGUI.ShowNotification($"[{title}] {detail}"); - } - - private static string FormatState(AutoHostState value) - { - return value switch - { - AutoHostState.Disabled => L("Disabled", "Выключен"), - AutoHostState.Idle => L("Idle", "Ожидание"), - AutoHostState.Warmup => L("Warmup", "Прогрев"), - AutoHostState.WaitingPlayers => L("Waiting for players", "Ждет игроков"), - AutoHostState.WaitingLoad => L("Waiting for load", "Ждет прогрузку"), - AutoHostState.Countdown => L("Countdown", "Отсчет"), - AutoHostState.Starting => L("Starting", "Запуск"), - AutoHostState.InGame => L("In Game", "В игре"), - AutoHostState.Returning => L("Returning", "Возврат"), - AutoHostState.Backoff => L("Backoff", "Пауза"), - _ => value.ToString(), - }; - } - - private static string CleanName(string value) - { - if (string.IsNullOrWhiteSpace(value)) return "игрок"; - string clean = value.Replace("\r", " ").Replace("\n", " ").Trim(); - return clean.Length <= 18 ? clean : clean.Substring(0, 17) + "..."; - } - - public static bool IsEnabled => ElysiumModMenuGUI.AutoHostEnabled; - public static bool ShouldReturnAfterMatch => IsEnabled && ElysiumModMenuGUI.AutoReturnLobbyAfterMatch; - private static bool ForceLastMinuteEnabled => ElysiumModMenuGUI.AutoHostForceLastMinute; - private static int RequiredPlayers => Mathf.Clamp(ElysiumModMenuGUI.AutoHostMinPlayers, 1, 15); - private static int ForceMinPlayers => Mathf.Clamp(ElysiumModMenuGUI.AutoHostForceMinPlayers, 1, 15); - private static float StartDelaySeconds => Mathf.Clamp(ElysiumModMenuGUI.AutoHostStartDelaySeconds, 0f, 180f); - private static float BackoffSeconds => Mathf.Clamp(ElysiumModMenuGUI.AutoHostBackoffSeconds, 2f, 60f); - private static float CountdownRemaining => countdownStartedAt < 0f ? 0f : Mathf.Clamp((activeCountdownDelay >= 0f ? activeCountdownDelay : StartDelaySeconds) - (Time.unscaledTime - countdownStartedAt), 0f, StartDelaySeconds); - private static float BackoffRemaining => backoffUntil < 0f ? 0f : Mathf.Clamp(backoffUntil - Time.unscaledTime, 0f, BackoffSeconds); - private static float LobbyLifeRemaining => lobbyOpenedAt < 0f ? -1f : Mathf.Clamp(LobbyLifetimeSeconds - (Time.unscaledTime - lobbyOpenedAt), 0f, LobbyLifetimeSeconds); - private static float WarmupRemaining => lobbyOpenedAt < 0f ? 0f : Mathf.Clamp(ElysiumModMenuGUI.AutoHostWarmupSeconds - (Time.unscaledTime - lobbyOpenedAt), 0f, 120f); - private static float LoadGraceRemaining => loadWaitStartedAt < 0f || ElysiumModMenuGUI.AutoHostLoadGraceSeconds <= 0 ? 0f : Mathf.Clamp(ElysiumModMenuGUI.AutoHostLoadGraceSeconds - (Time.unscaledTime - loadWaitStartedAt), 0f, 90f); - } - private int currentVisualsSubTab = 0; - private string[] visualsSubTabs = { "IN-GAME" }; - private int currentSelfSubTab = 0; - private string[] selfSubTabs = { "SPOOF", "MOVEMENT", "ROLES", "CHAT" }; - private string[] selfOtherTabs = { "MOVEMENT", "ROLES", "CHAT" }; - public static bool fakeStartCounterTroll = false; - public static bool fakeStartCounterCustom = false; - public static string fakeStartInput = "69"; - public static bool isEditingFakeStart = false; - public static float customStartTimer = -1f; - - public static bool localRainbow = false; - public static List rainbowPlayers = new List(); - public static float colorTimer = 0f; - public static byte currentColorId = 0; - private Vector2 playerListScrollPos = Vector2.zero; - private Vector2 playerActionScrollPos = Vector2.zero; - private byte selectedAntiCheatPlayerId = 255; - - public static string spoofLevelString = "100"; - public static string customNameInput = "хыхых"; - public static string spoofFriendCodeInput = "crewmate01"; - public static string localFriendCodeInput = "Steam#Local"; - public static string ghostChatColorHex = "#D7B8FF"; - public static bool isEditingLevel = false; - public static bool isEditingName = false; - public static bool isEditingFriendCode = false; - public static bool isEditingLocalFriendCode = false; - public static bool isEditingGhostChatColor = false; - private static bool discordLaunchStatusSent = false; - private static bool discordInvalidWebhookNotified = false; - private static float discordLaunchStatusNextTryAt = 0f; - private static readonly string relaySessionId = Guid.NewGuid().ToString("N").Substring(0, 12); - private static readonly Dictionary watchedLogLineCounts = new Dictionary(); - private static readonly DateTime logMonitorStartedUtc = DateTime.UtcNow; - private static readonly object anomalyLogMonitorLock = new object(); - private static System.Threading.Timer anomalyLogMonitorTimer; - private static string anomalyReportDetailsCache = $"sessionId={relaySessionId}\nclientId=Unknown\nnetworkMode=Unknown\nhost=Unknown\nplatform=Unknown\ninGame=Unknown"; - private static float logMonitorNextScanAt = 0f; - private static float logBurstWindowStartedAt = -1f; - private static float logBurstCooldownUntil = 0f; - private static int logBurstLineCount = 0; - private static bool anomalyLogWatchNotified = false; - private const int LogBurstLineThreshold = 15; - private const int InitialLogTailLineLimit = 120; - private const float LogBurstWindowSeconds = 5f; - private const float LogBurstScanIntervalSeconds = 1f; - private const float LogBurstAlertCooldownSeconds = 60f; - public static bool enableLocalNameSpoof = false; - public static bool enableLocalFriendCodeSpoof = false; - public static bool enableFriendCodeSpoof = false; - public static bool enablePlatformSpoof = true; - public static bool enableAnomalyLogReports = true; - public static bool showEspFriendCode = true; - public static bool allowDuplicateColors = false; - public static bool autoGhostAfterStart = false; - public static bool autoBanPlatformSpoof = false; - public static bool banCustomPlatformsFromTxt = false; - public static int fpsLimit = 60; - public static int chatHistoryLimit = 20; - public static int currentPlatformIndex = 1; - private static float localNameRefreshTimer = 0f; - private static float localFriendCodeRefreshTimer = 0f; - private static float platformBanScanTimer = 0f; - private static int lastAppliedFpsLimit = -1; - private static bool autoGhostAppliedThisGame = false; - private static bool wasGameStartedForAutoGhost = false; - private static string originalLocalFriendCode = null; - private static string originalLocalName = null; - private static float friendEspIgnoreNextLoadAt = 0f; - private static readonly HashSet friendEspIgnoreTokens = new HashSet(StringComparer.OrdinalIgnoreCase); - private static string platformBanListPath = ""; - private static float platformBanListNextLoadAt = 0f; - private static readonly HashSet customPlatformBanTokens = new HashSet(StringComparer.OrdinalIgnoreCase); - private static readonly HashSet platformSpoofPunishedOwners = new HashSet(); - private const int FavoriteOutfitSlotCount = 4; - private static readonly string[] favoriteOutfitSlots = new string[FavoriteOutfitSlotCount]; - private float brokenFcScanTimer = 0f; - private static readonly HashSet brokenFcPunishedOwners = new HashSet(); - - public static string[] platformNames = { - "Epic", "Steam", "Mac", "Microsoft", "Itch", "iOS", - "Android", "Switch", "Xbox", "PlayStation", "Starlight" - }; - - public static Platforms[] platformValues = { - (Platforms)1, - (Platforms)2, - (Platforms)3, - (Platforms)4, - (Platforms)5, - (Platforms)6, - (Platforms)7, - (Platforms)8, - (Platforms)9, - (Platforms)10, - (Platforms)112 - }; - - public static bool unlockFeatures = true; - - - - public class ElysiumNotification - { - public string title; - public string message; - public float ttl; - public float lifetime; - public bool HasExpired => lifetime > ttl; - - public ElysiumNotification(string title, string message, float ttl) - { - this.title = title; - this.message = message; - this.ttl = ttl; - this.lifetime = 0f; - } - } - public static List bannedEntries = new List(); - public static string banListPath = ""; - private Vector2 banListScroll = Vector2.zero; - public static bool autoBanEnabled = true; - public static string banInput = ""; - public static bool isEditingBan = false; - - public static void LoadBanList() - { - try - { - banListPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ElysiumModMenuBanList.txt"); - if (!System.IO.File.Exists(banListPath)) - { - System.IO.File.Create(banListPath).Dispose(); - } - bannedEntries = new List(System.IO.File.ReadAllLines(banListPath)); - } - catch { } - } - - public static void AddToBanList(string friendCode, string puid, string name, string reason) - { - try - { - if (string.IsNullOrEmpty(friendCode)) return; - - bool alreadyBanned = false; - string fcLower = friendCode.Trim().ToLower(); - - foreach (var e in bannedEntries) - { - string[] parts = e.Split('|'); - if (parts.Length > 0 && parts[0].Trim().ToLower() == fcLower) - { - alreadyBanned = true; - break; - } - } - - if (!alreadyBanned) - { - string date = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); - string entry = $"{friendCode}|{puid}|{name}|{date}|{reason}"; - bannedEntries.Add(entry); - System.IO.File.AppendAllText(banListPath, entry + Environment.NewLine); - } - } - catch { } - } - - public static void RemoveFromBanList(string entry) - { - try - { - bannedEntries.Remove(entry); - System.IO.File.WriteAllLines(banListPath, bannedEntries.ToArray()); - } - catch { } - } - - public static bool killReach = false, killAnyone = false; - public static bool endlessSsDuration = false, noVitalsCooldown = false; - public static bool endlessBattery = false, endlessVentTime = false, noVentCooldown = false, noMapCooldowns = false; - public static bool reactorSab = false, oxygenSab = false, commsSab = false, elecSab = false; - public static bool autoOpenDoors = false; - public static bool moonWalk = false; - public static bool SeePlayersInVent = false; - public static bool seeGhosts = false; - public static bool seeRoles = false; - public static bool showPlayerInfo = false; - public static bool revealMeetingRoles = false; - public static bool showTracers = false; - public static bool fullBright = false; - public static bool seeProtections = false; - public static bool seeKillCooldown = false; - public static bool extendedLobby = false; - public static bool DarkModeEnabled = true; - public static bool enableChatDarkMode = true; - public static float customLightRadius = 5f; - private static Dictionary lastKillTimestamps = new Dictionary(); - - public static bool alwaysChat = false; - public static bool readGhostChat = false; - public static bool enableSpellCheck = false; - - public static bool neverEndGame = false; - public static void ShowNotification(string text) - { - string title = "ElysiumModMenu"; - string msg = text; - - if (text.Contains("[") && text.Contains("]")) - { - int start = text.IndexOf("["); - int end = text.IndexOf("]"); - if (end > start) - { - string rawTitle = text.Substring(start + 1, end - start - 1); - title = System.Text.RegularExpressions.Regex.Replace(rawTitle, "<.*?>", string.Empty); - msg = System.Text.RegularExpressions.Regex.Replace(msg, @"(]+>)?\[.*?\]()?\s*", ""); - } - } - SendNotification(title, msg.Trim(), 3.5f); - } - - public static void SendNotification(string title, string message, float ttl = 3.5f) - { - if (!EnableCustomNotifs) return; - screenNotifications.Add(new ElysiumNotification(title, message, ttl)); - } - - - - public static HashSet forcedImpostors = new HashSet(); - public static Dictionary forcedPreGameRoles = new Dictionary(); - public static bool enablePreGameRoleForce = false; - private Vector2 preRolesListScrollPos = Vector2.zero; - private Vector2 preRolesActionScrollPos = Vector2.zero; - private byte selectedPreRoleId = 255; - public static List lockedPlayersList = new List(); - public static bool LogAllRPCs = true; - public static bool blockRainbowChat = true; - public static bool blockFortegreenChat = true; - - public static bool EnableCustomNotifs = true; - public static Vector2 notificationBoxSize = new Vector2(260f, 65f); - public static List screenNotifications = new List(); - - - private bool stylesInited = false; - private GUIStyle windowStyle, btnStyle, activeTabStyle, headerStyle, boxStyle; - private GUIStyle sidebarStyle, sidebarBtnStyle, activeSidebarBtnStyle, titleStyle; - private GUIStyle toggleOnStyle, toggleOffStyle, toggleLabelStyle, safeLineStyle; - private GUIStyle sliderStyle, sliderThumbStyle, subTabStyle, activeSubTabStyle; - public GUIStyle inputBlockStyle; - private Texture2D texWindowBg, texBoxBg, texBtnBg, texAccent, texSidebarBg; - private Texture2D texToggleOff, texToggleOn, texSliderBg, texSliderThumb, texInputBg, texColorBtn, texScrollThumb; - private void DrawHostOnlyTab() - { - GUILayout.BeginHorizontal(); - for (int i = 0; i < hostOnlySubTabs.Length; i++) - { - if (GUILayout.Button(hostOnlySubTabs[i], currentHostOnlySubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) - { - currentHostOnlySubTab = i; - scrollPosition = Vector2.zero; - } - } - GUILayout.EndHorizontal(); - GUILayout.Space(8); - - if (currentHostOnlySubTab == 0) DrawLobbyControls(); - else if (currentHostOnlySubTab == 1) DrawPlayersRoles(); - else if (currentHostOnlySubTab == 2) DrawAntiCheatTab(); - else if (currentHostOnlySubTab == 3) DrawAutoHostTab(); - } - - - private void DrawVisualsInGame() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label(L("VISIBILITY", "ВИДИМОСТЬ"), headerStyle); - - GUILayout.BeginHorizontal(); - seeGhosts = DrawToggle(seeGhosts, L("See Ghosts", "Видеть призраков"), 210); - seeRoles = DrawToggle(seeRoles, L("See Roles", "Видеть роли"), 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - showPlayerInfo = DrawToggle(showPlayerInfo, L("Show Player Info (ESP)", "Инфо об игроке (ESP)"), 210); - revealMeetingRoles = DrawToggle(revealMeetingRoles, L("Reveal Roles (Meeting)", "Показывать роли на собрании"), 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - showEspFriendCode = DrawToggle(showEspFriendCode, L("Show FC In ESP", "FriendCode в ESP"), 210); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - removePenalty = DrawToggle(removePenalty, L("No Disconnect Penalty", "Нет штрафа за выход"), 210); - alwaysShowLobbyTimer = DrawToggle(alwaysShowLobbyTimer, L("Always Show Lobby Timer", "Всегда показывать таймер лобби"), 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - showTracers = DrawToggle(showTracers, L("Show Tracers", "Показывать линии (Tracer)"), 210); - fullBright = DrawToggle(fullBright, L("Full Bright (No Shadows)", "Полная яркость (Нет теней)"), 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 210); - readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - freecam = DrawToggle(freecam, L("Freecam (WASD)", "Свободная камера (WASD)"), 210); - cameraZoom = DrawToggle(cameraZoom, L("Camera Zoom (Scroll)", "Зум камеры (Колесико)"), 210); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - RevealVotesEnabled = DrawToggle(RevealVotesEnabled, L("Reveal Votes (Meeting)", "Показывать голоса (Собрание)"), 210); - SeePlayersInVent = DrawToggle(SeePlayersInVent, L("See Players In Vents", "Видеть игроков в люках"), 210); - GUILayout.EndHorizontal(); - - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - seeProtections = DrawToggle(seeProtections, L("See Protections", "Видеть щиты"), 210); - seeKillCooldown = DrawToggle(seeKillCooldown, L("See Kill Cooldown", "Видеть килл-кд"), 210); - GUILayout.EndHorizontal(); - - GUILayout.EndVertical(); - } - - - public static bool enableLocalPetSpamDrop = true; - public static bool enableHostPetSpamBan = false; - - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadMessage))] - public static class Shield_PasosLimit_Patch - { - private const byte DataGameDataTag = 1; - private const byte RpcGameDataTag = 2; - private const byte DroppedGameDataTag = 0; - private const float PasosNotifyCooldown = 2f; - private const float RpcSpamWindow = 1f; - private static readonly Dictionary> rpcSpamTrackers = new Dictionary>(); - private static readonly HashSet pasosBlockedClientIds = new HashSet(); - private static readonly HashSet pasosHostBannedClientIds = new HashSet(); - private static float lastPasosNotify; - private static int currentPasosClientId = -1; - - public static void BeginMessageContext(int clientId) - { - currentPasosClientId = IsValidClientId(clientId) ? clientId : -1; - } - - public static bool IsClientBlocked(int clientId) - { - return ElysiumModMenuGUI.enableLocalPasosBan && IsValidClientId(clientId) && pasosBlockedClientIds.Contains(clientId); - } - - public static bool IsPlayerBlocked(PlayerControl player) - { - return player != null && IsClientBlocked(GetKickClientId(player, -1)); - } - - public static bool IsEmptyGameDataReader(MessageReader reader) - { - if (!ElysiumModMenuGUI.enablePasosLimit || reader == null) return false; - - try - { - return reader.Length <= 0 || (reader.Position <= 0 && reader.BytesRemaining <= 0); - } - catch { } - - return false; - } - - public static bool IsValidClientId(int clientId) - { - return clientId >= 0 && clientId < 256; - } - - public static bool RecordDrop(int clientId = -1, PlayerControl player = null, string reason = "RPC spam") - { - float now = UnityEngine.Time.time; - int resolvedClientId = IsValidClientId(clientId) ? clientId : GetKickClientId(player, -1); - if (!IsValidClientId(resolvedClientId)) - resolvedClientId = currentPasosClientId; - if (!IsValidClientId(resolvedClientId)) - resolvedClientId = ResolveSingleRemoteClientId(); - if (!IsValidClientId(resolvedClientId)) - return false; - - int clientDropCount = TrackRpcSpam(resolvedClientId, now); - bool overLimit = clientDropCount >= ElysiumModMenuGUI.rpcSpamLimit; - if (overLimit) - { - BlockPasosClient(resolvedClientId, player, clientDropCount, reason); - if (now - lastPasosNotify > PasosNotifyCooldown) - lastPasosNotify = now; - } - - return overLimit || (ElysiumModMenuGUI.enableLocalPasosBan && pasosBlockedClientIds.Contains(resolvedClientId)); - } - - private static int TrackRpcSpam(int clientId, float now) - { - if (!IsValidClientId(clientId)) return 0; - - if (!rpcSpamTrackers.TryGetValue(clientId, out Queue drops)) - { - drops = new Queue(); - rpcSpamTrackers[clientId] = drops; - } - - while (drops.Count > 0 && drops.Peek() < now - RpcSpamWindow) - drops.Dequeue(); - - drops.Enqueue(now); - return drops.Count; - } - - private static void BlockPasosClient(int clientId, PlayerControl player, int packetCount, string reason) - { - try - { - if (!IsValidClientId(clientId) || (AmongUsClient.Instance != null && clientId == AmongUsClient.Instance.ClientId)) return; - - if (player == null) - player = FindPlayerByClientId(clientId); - - string pName = player?.Data?.PlayerName ?? $"Client {clientId}"; - int banClientId = GetKickClientId(player, clientId); - string fc = string.IsNullOrEmpty(player?.Data?.FriendCode) ? "Unknown" : player.Data.FriendCode; - string puid = IsValidClientId(banClientId) ? banClientId.ToString() : clientId.ToString(); - - try - { - if (player != null && AmongUsClient.Instance != null) - { - var client = AmongUsClient.Instance.GetClientFromCharacter(player); - if (client != null) puid = GetClientPuid(client); - } - } - catch { } - - if (ElysiumModMenuGUI.enableLocalPasosBan && pasosBlockedClientIds.Add(clientId)) - { - ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Local RPC drop: {reason} after {packetCount} packets/s"); - } - - if (!ElysiumModMenuGUI.enableHostPasosBan || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; - if (!pasosHostBannedClientIds.Add(clientId)) return; - if (!IsValidClientId(banClientId)) return; - - ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Host RPC auto-ban: {reason} after {packetCount} packets/s"); - AmongUsClient.Instance.KickPlayer(banClientId, true); - } - catch { } - } - - public static int GetKickClientId(PlayerControl player, int fallbackClientId) - { - try - { - if (player != null) - { - int ownerId = (int)player.OwnerId; - if (IsValidClientId(ownerId)) return ownerId; - - if (player.Data != null && IsValidClientId(player.Data.ClientId)) - return player.Data.ClientId; - } - } - catch { } - - return IsValidClientId(fallbackClientId) ? fallbackClientId : -1; - } - - public static PlayerControl FindPlayerByClientId(int clientId) - { - try - { - if (PlayerControl.AllPlayerControls == null) return null; - - foreach (PlayerControl pc in PlayerControl.AllPlayerControls) - { - if (pc == null) continue; - if ((int)pc.OwnerId == clientId) return pc; - - try - { - if (pc.Data != null && pc.Data.ClientId == clientId) return pc; - } - catch { } - } - } - catch { } - - return null; - } - - public static int ResolveClientId(object source) - { - return ResolveClientId(source, 0); - } - - private static int ResolveClientId(object source, int depth) - { - if (source == null || depth > 2 || source is MessageReader || source is SendOption) return -1; - - try - { - if (source is PlayerControl pc) - return GetKickClientId(pc, -1); - - int direct = ConvertNumericClientId(source); - if (direct >= 0) return direct; - - Type type = source.GetType(); - foreach (string name in new[] { "ClientId", "OwnerId", "Id", "clientId", "ownerId", "id" }) - { - PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - direct = ConvertNumericClientId(property?.GetValue(source)); - if (direct >= 0) return direct; - - FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - direct = ConvertNumericClientId(field?.GetValue(source)); - if (direct >= 0) return direct; - } - - foreach (string name in new[] { "Character", "Object", "Player", "Data", "character", "player" }) - { - PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - direct = ResolveClientId(property?.GetValue(source), depth + 1); - if (direct >= 0) return direct; - - FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - direct = ResolveClientId(field?.GetValue(source), depth + 1); - if (direct >= 0) return direct; - } - - string typeName = type.FullName ?? type.Name; - if (typeName.IndexOf("Player", StringComparison.OrdinalIgnoreCase) >= 0 || - typeName.IndexOf("Client", StringComparison.OrdinalIgnoreCase) >= 0) - { - foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (property.GetIndexParameters().Length > 0) continue; - - try - { - direct = ConvertNumericClientId(property.GetValue(source)); - if (direct >= 0) return direct; - } - catch { } - } - - foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - try - { - direct = ConvertNumericClientId(field.GetValue(source)); - if (direct >= 0) return direct; - } - catch { } - } - } - } - catch { } - - return -1; - } - - private static int ResolveSingleRemoteClientId() - { - try - { - if (PlayerControl.AllPlayerControls == null) return -1; - - int found = -1; - int count = 0; - foreach (PlayerControl pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; - - int ownerId = (int)pc.OwnerId; - if (!IsValidClientId(ownerId)) continue; - - found = ownerId; - count++; - if (count > 1) return -1; - } - - return count == 1 ? found : -1; - } - catch { } - - return -1; - } - - private static int ConvertNumericClientId(object value) - { - if (value == null) return -1; - - try - { - TypeCode code = Type.GetTypeCode(value.GetType()); - switch (code) - { - case TypeCode.Byte: - case TypeCode.SByte: - case TypeCode.Int16: - case TypeCode.UInt16: - case TypeCode.Int32: - case TypeCode.UInt32: - case TypeCode.Int64: - case TypeCode.UInt64: - long id = Convert.ToInt64(value); - return id >= 0 && id < 256 ? (int)id : -1; - } - } - catch { } - - return -1; - } - - public static void Postfix(MessageReader __result) - { - DropSuspiciousGameDataMessage(__result); - } - - public static void DropSuspiciousGameDataMessage(MessageReader reader) - { - if (!ElysiumModMenuGUI.enablePasosLimit || reader == null) return; - - try - { - if (reader.BytesRemaining > 0 || reader.Length > 0) return; - - string reason = null; - if (reader.Tag == RpcGameDataTag) - reason = "empty StartMessage RPC"; - else if (reader.Tag == DataGameDataTag) - reason = "empty SDF/data message"; - else if (IsBadGameDataTag(reader.Tag)) - reason = $"bad data tag {reader.Tag}"; - - if (reason == null) return; - - reader.Tag = DroppedGameDataTag; - reader.Position = reader.Length; - - RecordDrop(-1, null, reason); - } - catch { } - } - - private static bool IsBadGameDataTag(byte tag) - { - switch (tag) - { - case DroppedGameDataTag: - case DataGameDataTag: - case RpcGameDataTag: - case 4: - case 5: - case 6: - case 7: - case 8: - return false; - default: - return true; - } - } - } - - public static class Shield_PasosLimit_GameDataGuard - { - private static readonly Dictionary coroutineReaderProperties = new Dictionary(); - - public static bool ShouldDrop(object[] args) - { - if (!ElysiumModMenuGUI.enablePasosLimit) return false; - - try - { - MessageReader reader = FindReader(args); - - if (!Shield_PasosLimit_Patch.IsEmptyGameDataReader(reader)) - return false; - - Shield_PasosLimit_Patch.RecordDrop(); - return true; - } - catch { } - - return false; - } - - public static bool ShouldDropCoroutine(object instance) - { - if (!ElysiumModMenuGUI.enablePasosLimit) return false; - - try - { - MessageReader reader = GetCoroutineReader(instance); - - if (!Shield_PasosLimit_Patch.IsEmptyGameDataReader(reader)) - return false; - - Shield_PasosLimit_Patch.RecordDrop(); - return true; - } - catch { } - - return false; - } - - private static MessageReader FindReader(object[] args) - { - if (args == null) return null; - - foreach (object arg in args) - { - if (arg is MessageReader reader) - return reader; - } - - return null; - } - - private static MessageReader GetCoroutineReader(object instance) - { - if (instance == null) return null; - - try - { - Type type = instance.GetType(); - if (!coroutineReaderProperties.TryGetValue(type, out PropertyInfo property)) - { - property = type.GetProperty("reader", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - coroutineReaderProperties[type] = property; - } - - return property?.GetValue(instance) as MessageReader; - } - catch { } - - return null; - } - - private static System.Collections.IEnumerator EmptyGameDataCoroutine() - { - yield break; - } - - public static Il2CppSystem.Collections.IEnumerator EmptyGameDataCoroutineIl2Cpp() - { - return EmptyGameDataCoroutine().WrapToIl2Cpp(); - } - } - - [HarmonyPatch] - public static class Shield_PasosLimit_HandleGameData_Patch - { - public static IEnumerable TargetMethods() - { - foreach (MethodInfo method in typeof(InnerNetClient).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (method.Name == "HandleGameData" && method.ReturnType == typeof(void) && - method.GetParameters().Any(p => p.ParameterType == typeof(MessageReader))) - { - yield return method; - } - } - } - - public static bool Prefix(object[] __args) - { - return !Shield_PasosLimit_GameDataGuard.ShouldDrop(__args); - } - } - - [HarmonyPatch] - public static class Shield_PasosLimit_HandleGameDataInner_Patch - { - public static IEnumerable TargetMethods() - { - foreach (MethodInfo method in typeof(InnerNetClient).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (method.Name == "HandleGameDataInner" && - method.GetParameters().Any(p => p.ParameterType == typeof(MessageReader))) - { - yield return method; - } - } - } - - public static bool Prefix(object[] __args, ref Il2CppSystem.Collections.IEnumerator __result) - { - if (!Shield_PasosLimit_GameDataGuard.ShouldDrop(__args)) - return true; - - __result = Shield_PasosLimit_GameDataGuard.EmptyGameDataCoroutineIl2Cpp(); - return false; - } - } - - [HarmonyPatch] - public static class Shield_PasosLimit_GameDataCoroutine_Patch - { - public static IEnumerable TargetMethods() - { - foreach (Type nestedType in typeof(InnerNetClient).GetNestedTypes(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (nestedType.Name.IndexOf("HandleGameDataInner", StringComparison.OrdinalIgnoreCase) < 0) continue; - - MethodInfo moveNext = nestedType.GetMethod("MoveNext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (moveNext != null && moveNext.ReturnType == typeof(bool)) - yield return moveNext; - } - } - - public static bool Prefix(object __instance, ref bool __result) - { - if (!Shield_PasosLimit_GameDataGuard.ShouldDropCoroutine(__instance)) - return true; - - __result = false; - return false; - } - } - - [HarmonyPatch] - public static class Shield_PasosLimit_HandleMessageContext_Patch - { - public static IEnumerable TargetMethods() - { - foreach (MethodInfo method in typeof(InnerNetClient).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (method.Name != "HandleMessage") continue; - if (method.GetParameters().Any(p => p.ParameterType == typeof(MessageReader))) - yield return method; - } - } - - public static bool Prefix(object[] __args) - { - if (!ElysiumModMenuGUI.enablePasosLimit) return true; - - try - { - int clientId = ExtractClientId(__args); - Shield_PasosLimit_Patch.BeginMessageContext(clientId); - } - catch { } - - return true; - } - - public static int ExtractClientId(object[] args) - { - if (args == null) return -1; - - foreach (object arg in args) - { - int clientId = ExtractClientId(arg); - if (clientId >= 0) return clientId; - } - - return -1; - } - - private static int ExtractClientId(object source) - { - return Shield_PasosLimit_Patch.ResolveClientId(source); - } - } - - [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] - public static class Shield_PetSpam_Patch - { - public static bool Prefix(PlayerPhysics __instance, byte callId, Hazel.MessageReader reader) - { - if (!ElysiumModMenuGUI.enablePasosLimit) return true; - - if (callId == 49 || callId == 50) - { - try - { - if (__instance == null || __instance.myPlayer == null) return true; - - if (__instance.myPlayer == PlayerControl.LocalPlayer) return true; - - if (Shield_PasosLimit_Patch.IsPlayerBlocked(__instance.myPlayer)) - return false; - - int clientId = Shield_PasosLimit_Patch.GetKickClientId(__instance.myPlayer, -1); - if (Shield_PasosLimit_Patch.RecordDrop(clientId, __instance.myPlayer, "pet RPC spam")) - return false; - } - catch { } - } - - return true; - } - } - public static int GetColorIdByName(string name) - { - string[] names = { "red", "blue", "green", "pink", "orange", "yellow", "black", "white", "purple", "brown", "cyan", "lime", "maroon", "rose", "banana", "gray", "tan", "coral", "fortegreen" }; - for (int i = 0; i < names.Length; i++) - if (names[i] == name.ToLower().Trim()) return i; - return -1; - } - private IEnumerator AttemptShapeshiftFrame(PlayerControl target, PlayerControl morphInto) - { - if (target == null || morphInto == null || PlayerControl.LocalPlayer == null || AmongUsClient.Instance == null) yield break; - - bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); - - if (target.Data.RoleType != RoleTypes.Shapeshifter && hasAnticheat) - { - RoleTypes currentRole = target.Data.RoleType; - target.RpcSetRole(RoleTypes.Shapeshifter, true); - - yield return new WaitForSeconds(0.5f); - - target.RpcShapeshift(morphInto, true); - - yield return new WaitForSeconds(0.5f); - - target.RpcSetRole(currentRole, true); - } - else - { - target.RpcShapeshift(morphInto, true); - } - ShowNotification($"[MORPH] {target.Data.PlayerName} превращен в {morphInto.Data.PlayerName}!"); - } - - private IEnumerator MassMorphCoroutine() - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) yield break; - - bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); - - Dictionary originalRoles = new Dictionary(); - - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null && pc.Data != null && !pc.Data.Disconnected) - { - originalRoles[pc.PlayerId] = pc.Data.RoleType; - - if (hasAnticheat && pc.Data.RoleType != RoleTypes.Shapeshifter) - { - pc.RpcSetRole(RoleTypes.Shapeshifter, true); - } - } - } - - if (hasAnticheat) yield return new UnityEngine.WaitForSeconds(0.5f); - - PlayerControl targetToMorphInto = null; - if (selectedMorphTargetId != 255) - { - targetToMorphInto = GameData.Instance.GetPlayerById(selectedMorphTargetId)?.Object; - } - - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null && pc.Data != null && !pc.Data.Disconnected) - { - PlayerControl morphTarget = targetToMorphInto != null ? targetToMorphInto : pc; - pc.RpcShapeshift(morphTarget, true); - } - } - - - if (hasAnticheat) yield return new UnityEngine.WaitForSeconds(0.5f); - - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null && pc.Data != null && !pc.Data.Disconnected) - { - if (hasAnticheat && originalRoles.ContainsKey(pc.PlayerId)) - { - pc.RpcSetRole(originalRoles[pc.PlayerId], true); - } - } - } - - string notifText = targetToMorphInto != null ? targetToMorphInto.Data.PlayerName : "Egg"; - ShowNotification($"[MASS MORPH] {notifText}"); - } - - - private void ForceMeetingAsPlayer(PlayerControl target) - { - if (target == null || AmongUsClient.Instance == null) return; - if (!AmongUsClient.Instance.AmHost) return; - - try - { - MeetingRoomManager.Instance.AssignSelf(target, null); - target.RpcStartMeeting(null); - DestroyableSingleton.Instance.OpenMeetingRoom(target); - } - catch { } - } - - private void KillAll() - { - if (PlayerControl.LocalPlayer == null || PlayerControl.AllPlayerControls == null) return; - Vector3 op = PlayerControl.LocalPlayer.transform.position; - foreach (var t in PlayerControl.AllPlayerControls) - { - if (t != null && t != PlayerControl.LocalPlayer && !t.Data.IsDead && !t.Data.Disconnected) - { - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(t.transform.position); - PlayerControl.LocalPlayer.CmdCheckMurder(t); - PlayerControl.LocalPlayer.RpcMurderPlayer(t, true); - } - } - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(op); - } - - private void KickAll() - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) - { - foreach (var pc in PlayerControl.AllPlayerControls) - if (pc != null && pc != PlayerControl.LocalPlayer && !pc.Data.Disconnected) - AmongUsClient.Instance.KickPlayer((int)pc.OwnerId, false); - } - } - - private void DespawnLobby() - { - try - { - if (LobbyBehaviour.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - LobbyBehaviour.Instance.Cast().Despawn(); - } - } - catch { } - } - - private void SpawnLobby() - { - try - { - if (GameStartManager.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - LobbyBehaviour newLobby = UnityEngine.Object.Instantiate(GameStartManager.Instance.LobbyPrefab); - AmongUsClient.Instance.Spawn(newLobby.Cast(), -2, SpawnFlags.None); - } - } - catch { } - } - - - - public static void UnlockCosmetics() - { - if (HatManager.Instance == null) return; - try - { - foreach (var h in HatManager.Instance.allHats) h.Free = true; - foreach (var s in HatManager.Instance.allSkins) s.Free = true; - foreach (var v in HatManager.Instance.allVisors) v.Free = true; - foreach (var p in HatManager.Instance.allPets) p.Free = true; - foreach (var n in HatManager.Instance.allNamePlates) n.Free = true; - } - catch { } - } - - public static void ChangeNameGlobalHost(PlayerControl target, string newName) - { - if (target == null) return; - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; - try - { - target.RpcSetName(newName); - var netObj = GameData.Instance.GetComponent(); - if (netObj != null) netObj.SetDirtyBit(1U << (int)target.PlayerId); - } - catch { } - } - - private static void ApplyLocalNameSelf(string newName, bool notify = true) - { - try - { - PlayerControl local = PlayerControl.LocalPlayer; - if (local == null) - { - if (notify) ShowNotification("[LOCAL NAME] Local player not found."); - return; - } - - string renderName = BuildLocalNameRenderText(newName); - if (originalLocalName == null) - { - originalLocalName = local.CurrentOutfit != null && !string.IsNullOrWhiteSpace(local.CurrentOutfit.PlayerName) - ? local.CurrentOutfit.PlayerName - : local.Data?.PlayerName; - } - - if (local.cosmetics != null) - local.cosmetics.SetName(renderName); - - TrySetPlayerNameObject(local.Data, renderName); - if (local.Data != null) - { - TrySetPlayerNameObject(local.Data.DefaultOutfit, renderName); - TrySetPlayerNameObject(local.CurrentOutfit, renderName); - } - - if (notify) - ShowNotification($"[LOCAL NAME] {L("Applied locally:", "Локально применен:")} {newName}"); - } - catch { } - } - - private static void RestoreLocalNameSelf() - { - try - { - PlayerControl local = PlayerControl.LocalPlayer; - if (local == null || local.cosmetics == null) return; - - string baseName = !string.IsNullOrWhiteSpace(originalLocalName) - ? originalLocalName - : (local.Data?.PlayerName ?? local.CurrentOutfit?.PlayerName); - if (!string.IsNullOrWhiteSpace(baseName)) - { - local.cosmetics.SetName(baseName); - TrySetPlayerNameObject(local.Data, baseName); - if (local.Data != null) - { - TrySetPlayerNameObject(local.Data.DefaultOutfit, baseName); - TrySetPlayerNameObject(local.CurrentOutfit, baseName); - } - } - - originalLocalName = null; - } - catch { } - } - - private static void ApplyLocalFriendCodeSelf(string fakeFriendCode, bool notify = true) - { - try - { - PlayerControl local = PlayerControl.LocalPlayer; - if (local == null || local.Data == null) - { - if (notify) ShowNotification("[LOCAL FC] Local player data not found."); - return; - } - - fakeFriendCode ??= string.Empty; - string current = local.Data.FriendCode ?? string.Empty; - if (originalLocalFriendCode == null && current != fakeFriendCode) - originalLocalFriendCode = current; - - TrySetStringMember(local.Data, "FriendCode", fakeFriendCode); - - if (notify) - ShowNotification($"[LOCAL FC] {L("Applied locally:", "Локально применен:")} {fakeFriendCode}"); - } - catch { } - } - - private static void RestoreLocalFriendCodeSelf() - { - try - { - if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null || originalLocalFriendCode == null) return; - TrySetStringMember(PlayerControl.LocalPlayer.Data, "FriendCode", originalLocalFriendCode); - originalLocalFriendCode = null; - } - catch { } - } - - private static void TrySetPlayerNameObject(object target, string newName) - { - TrySetStringMember(target, "PlayerName", newName); - } - - private static void TrySetStringMember(object target, string memberName, string value) - { - if (target == null || string.IsNullOrEmpty(memberName)) return; - - const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - Type type = target.GetType(); - - try - { - PropertyInfo property = type.GetProperty(memberName, flags); - if (property != null && property.CanWrite) - { - property.SetValue(target, value, null); - return; - } - } - catch { } - - try - { - FieldInfo field = type.GetField(memberName, flags); - if (field != null) field.SetValue(target, value); - } - catch { } - } - - private static void TryInvokeStringMethod(object target, string methodName, string value) - { - if (target == null) return; - - try - { - MethodInfo method = target.GetType().GetMethod( - methodName, - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - null, - new[] { typeof(string) }, - null); - - if (method != null) - method.Invoke(target, new object[] { value }); - } - catch { } - } - - public static bool showWatermark = true; - public static bool whiteMenuTheme = false; - - private static void SaveBool(string key, bool value) - { - PlayerPrefs.SetInt(key, value ? 1 : 0); - } - - private static bool LoadBool(string key, bool defaultValue) - { - return PlayerPrefs.HasKey(key) ? PlayerPrefs.GetInt(key) == 1 : defaultValue; - } - - private static int LoadInt(string key, int defaultValue) - { - return PlayerPrefs.HasKey(key) ? PlayerPrefs.GetInt(key) : defaultValue; - } - - private static float LoadFloat(string key, float defaultValue) - { - return PlayerPrefs.HasKey(key) ? PlayerPrefs.GetFloat(key) : defaultValue; - } - - private void SaveConfig() - { - try - { - PlayerPrefs.SetInt("M_BndMagnet", (int)bindMagnetCursor); - Plugin.SpoofedLevel.Value = spoofLevelString; - Plugin.EnableFriendCodeSpoofConfig.Value = enableFriendCodeSpoof; - Plugin.SpoofFriendCodeConfig.Value = spoofFriendCodeInput; - Plugin.EnablePlatformSpoof.Value = enablePlatformSpoof; - Plugin.AutoBanBrokenFriendCodeConfig.Value = autoBanBrokenFriendCode; - Plugin.PlatformIndex.Value = currentPlatformIndex; - Plugin.ShowWatermarkConfig.Value = showWatermark; - Plugin.UnlockCosmeticsConfig.Value = unlockCosmetics; - Plugin.MoreLobbyInfoConfig.Value = moreLobbyInfo; - Plugin.EnableChatDarkModeConfig.Value = enableChatDarkMode; - Plugin.GhostChatColorConfig.Value = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); - Plugin.EnableAnomalyLogReportsConfig.Value = enableAnomalyLogReports; - Plugin.ShowEspFriendCodeConfig.Value = showEspFriendCode; - Plugin.RpcSpoofDelayConfig.Value = rpcSpoofDelay; - Plugin.MenuColorIndexConfig.Value = currentMenuColorIndex; - Plugin.RgbMenuModeConfig.Value = rgbMenuMode; - if (menuToggleKey == KeyCode.None) menuToggleKey = KeyCode.Insert; - Plugin.MenuKeybind.Value = menuToggleKey; - PlayerPrefs.SetInt("M_MenuToggleKey", (int)menuToggleKey); - SaveBool("M_WhiteTheme", whiteMenuTheme); - PlayerPrefs.SetInt("M_MenuLanguageIndex", currentMenuLanguageIndex); - PlayerPrefs.SetInt("M_FpsLimit", fpsLimit); - SaveBool("M_EnableBackground", enableBackground); - SaveBool("M_EnableCustomNotifs", EnableCustomNotifs); - SaveBool("M_LogAllRPCs", LogAllRPCs); - PlayerPrefs.SetInt("M_SelectedSpoofMenuIndex", selectedSpoofMenuIndex); - PlayerPrefs.SetFloat("M_MenuWindowX", windowRect.x); - PlayerPrefs.SetFloat("M_MenuWindowY", windowRect.y); - PlayerPrefs.SetFloat("M_MenuWindowW", windowRect.width); - PlayerPrefs.SetFloat("M_MenuWindowH", windowRect.height); - PlayerPrefs.SetInt("M_CurrentTab", currentTab); - PlayerPrefs.SetInt("M_TargetTab", targetTabIndex); - PlayerPrefs.SetInt("M_CurrentGeneralSubTab", currentGeneralSubTab); - PlayerPrefs.SetInt("M_CurrentGeneralInfoSubTab", currentGeneralInfoSubTab); - PlayerPrefs.SetInt("M_CurrentSelfSubTab", currentSelfSubTab); - PlayerPrefs.SetInt("M_CurrentVisualsSubTab", currentVisualsSubTab); - PlayerPrefs.SetInt("M_CurrentPlayersSubTab", currentPlayersSubTab); - PlayerPrefs.SetInt("M_CurrentHostOnlySubTab", currentHostOnlySubTab); - PlayerPrefs.SetInt("M_CurrentAutoHostSubTab", currentAutoHostSubTab); - PlayerPrefs.SetInt("M_BndMMorph", (int)bindMassMorph); - PlayerPrefs.SetInt("M_BndSpawn", (int)bindSpawnLobby); - PlayerPrefs.SetInt("M_BndDespawn", (int)bindDespawnLobby); - PlayerPrefs.SetInt("M_BndCloseMtg", (int)bindCloseMeeting); - PlayerPrefs.SetInt("M_BndInstaStart", (int)bindInstaStart); - PlayerPrefs.SetInt("M_BndEndCrew", (int)bindEndCrew); - PlayerPrefs.SetInt("M_BndEndImp", (int)bindEndImp); - PlayerPrefs.SetInt("M_BndEndImpDC", (int)bindEndImpDC); - PlayerPrefs.SetInt("M_BndEndHnsDC", (int)bindEndHnsDC); - PlayerPrefs.SetInt("M_BndToggleTracers", (int)bindToggleTracers); - PlayerPrefs.SetInt("M_BndToggleNoClip", (int)bindToggleNoClip); - PlayerPrefs.SetInt("M_BndToggleFreecam", (int)bindToggleFreecam); - PlayerPrefs.SetInt("M_BndToggleCameraZoom", (int)bindToggleCameraZoom); - PlayerPrefs.SetInt("M_BndKillAll", (int)bindKillAll); - PlayerPrefs.SetInt("M_BndCallMeeting", (int)bindCallMeeting); - PlayerPrefs.SetInt("M_BndTogglePlayerInfo", (int)bindTogglePlayerInfo); - PlayerPrefs.SetInt("M_BndToggleSeeRoles", (int)bindToggleSeeRoles); - PlayerPrefs.SetInt("M_BndToggleSeeGhosts", (int)bindToggleSeeGhosts); - PlayerPrefs.SetInt("M_BndToggleFullBright", (int)bindToggleFullBright); - PlayerPrefs.SetInt("M_BndKickAll", (int)bindKickAll); - PlayerPrefs.SetInt("M_BndFixSabotages", (int)bindFixSabotages); - PlayerPrefs.SetInt("M_BndSetAllGhost", (int)bindSetAllGhost); - PlayerPrefs.SetInt("M_BndSetAllGhostImp", (int)bindSetAllGhostImp); - SaveBool("M_AutoKickBugs", autoKickBugs); - PlayerPrefs.SetFloat("M_AutoKickTimer", autoKickTimer); - SaveBool("M_DisableVoteKicks", disableVoteKicks); - SaveBool("M_LocalNameSpoof", enableLocalNameSpoof); - SaveBool("M_LocalFakeFCEnabled", enableLocalFriendCodeSpoof); - PlayerPrefs.SetString("M_LocalFakeFC", localFriendCodeInput); - - SaveBool("M_ShowPlayerInfo", showPlayerInfo); - SaveBool("M_SeeGhosts", seeGhosts); - SaveBool("M_SeeRoles", seeRoles); - SaveBool("M_RevealMeetingRoles", revealMeetingRoles); - SaveBool("M_ShowTracers", showTracers); - SaveBool("M_FullBright", fullBright); - SaveBool("M_SeeProtections", seeProtections); - SaveBool("M_SeeKillCooldown", seeKillCooldown); - SaveBool("M_ExtendedLobby", extendedLobby); - SaveBool("M_MoreLobbyInfo", moreLobbyInfo); - SaveBool("M_AlwaysChat", alwaysChat); - SaveBool("M_ReadGhostChat", readGhostChat); - SaveBool("M_EnableExtendedChat", enableExtendedChat); - SaveBool("M_EnableFastChat", enableFastChat); - SaveBool("M_AllowLinksAndSymbols", allowLinksAndSymbols); - SaveBool("M_EnableChatHistory", enableChatHistory); - PlayerPrefs.SetInt("M_ChatHistoryLimit", chatHistoryLimit); - SaveBool("M_EnableClipboard", enableClipboard); - SaveBool("M_EnableChatLog", enableChatLog); - SaveBool("M_EnableColorCommand", enableColorCommand); - SaveBool("M_BlockRainbowChat", blockRainbowChat); - SaveBool("M_BlockFortegreenChat", blockFortegreenChat); - SaveBool("M_SpoofMenuEnabled", SpoofMenuEnabled); - SaveBool("M_NoClip", noClip); - SaveBool("M_TpToCursor", tpToCursor); - SaveBool("M_DragToCursor", dragToCursor); - SaveBool("M_AutoFollowCursor", autoFollowCursor); - SaveBool("M_Freecam", freecam); - SaveBool("M_CameraZoom", cameraZoom); - SaveBool("M_RevealVotes", RevealVotesEnabled); - SaveBool("M_NoTaskMode", noTaskMode); - SaveBool("M_NoMapCooldowns", noMapCooldowns); - SaveBool("M_NeverEndGame", neverEndGame); - SaveBool("M_RemovePenalty", removePenalty); - SaveBool("M_AlwaysShowLobbyTimer", alwaysShowLobbyTimer); - SaveBool("M_AutoBanEnabled", autoBanEnabled); - SaveBool("M_AllowDuplicateColors", allowDuplicateColors); - SaveBool("M_BlockSpoofRPC", blockSpoofRPC); - SaveBool("M_AutoBanPlatformSpoof", autoBanPlatformSpoof); - SaveBool("M_BanCustomPlatformsFromTxt", banCustomPlatformsFromTxt); - SaveBool("M_BlockSabotageRPC", blockSabotageRPC); - SaveBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); - SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); - SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); - SaveBool("M_PasosLimit", enablePasosLimit); - PlayerPrefs.SetInt("M_RpcSpamLimit", rpcSpamLimit); - SaveBool("M_AntiPasosLocalBan", enableLocalPasosBan); - SaveBool("M_AntiPasosHostBan", enableHostPasosBan); - SaveBool("M_AutoHostEnabled", AutoHostEnabled); - SaveBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); - SaveBool("M_AutoHostNotifications", AutoHostNotifications); - SaveBool("M_AutoHostForceLastMinute", AutoHostForceLastMinute); - SaveBool("M_AutoHostWaitLoadedPlayers", AutoHostWaitLoadedPlayers); - SaveBool("M_AutoHostCancelBelowMin", AutoHostCancelBelowMin); - SaveBool("M_AutoHostInstantStart", AutoHostInstantStart); - SaveBool("M_AutoGhostAfterStart", autoGhostAfterStart); - PlayerPrefs.SetInt("M_AutoHostMinPlayers", AutoHostMinPlayers); - PlayerPrefs.SetFloat("M_AutoHostStartDelaySeconds", AutoHostStartDelaySeconds); - PlayerPrefs.SetInt("M_AutoHostFastStartPlayers", AutoHostFastStartPlayers); - PlayerPrefs.SetFloat("M_AutoHostFastStartDelaySeconds", AutoHostFastStartDelaySeconds); - PlayerPrefs.SetFloat("M_WalkSpeed", walkSpeed); - PlayerPrefs.SetFloat("M_EngineSpeed", engineSpeed); - - Plugin.MenuConfig.Save(); - - PlayerPrefs.SetString("M_SpoofName", customNameInput); - for (int i = 0; i < favoriteOutfitSlots.Length; i++) - PlayerPrefs.SetString($"M_FavoriteOutfit_{i}", favoriteOutfitSlots[i] ?? string.Empty); - PlayerPrefs.Save(); - } - catch { } - } - private void DrawAutoHostTab() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label(L("AUTO HOST SYSTEM", "СИСТЕМА АВТО-ХОСТА"), headerStyle); - - var snapshot = ElysiumAutoHostService.GetStatusSnapshot(); - GUILayout.Label($"{L("Status:", "Статус:")} {snapshot.State}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 13 }); - GUILayout.Space(10); - - AutoHostEnabled = DrawToggle(AutoHostEnabled, L("Enable Auto Host", "Включить Авто-Хост"), 250); - GUILayout.Space(5); - AutoReturnLobbyAfterMatch = DrawToggle(AutoReturnLobbyAfterMatch, L("Auto Return To Lobby", "Авто-возврат в лобби"), 250); - GUILayout.Space(5); - AutoHostNotifications = DrawToggle(AutoHostNotifications, L("Show Notifications", "Показывать уведомления"), 250); - GUILayout.Space(5); - AutoHostWaitLoadedPlayers = DrawToggle(AutoHostWaitLoadedPlayers, L("Wait For Players To Load", "Ждать прогрузки игроков"), 250); - GUILayout.Space(5); - AutoHostCancelBelowMin = DrawToggle(AutoHostCancelBelowMin, L("Cancel Countdown If Player Leaves", "Отмена отсчета, если игрок вышел"), 250); - GUILayout.Space(5); - AutoHostInstantStart = DrawToggle(AutoHostInstantStart, L("Instant Start (No 5s Wait)", "Мгновенный старт (Без 5с)"), 250); - GUILayout.Space(5); - autoGhostAfterStart = DrawToggle(autoGhostAfterStart, L("Auto Ghost After Start", "Авто-призрак после старта"), 250); - GUILayout.Space(5); - AutoHostForceLastMinute = DrawToggle(AutoHostForceLastMinute, L("Force Start Last Minute", "Форс-старт на последней минуте"), 250); - - GUILayout.Space(15); - - string hexColor = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); - GUIStyle sliderLabelStyle = new GUIStyle(toggleLabelStyle) { richText = true }; - - GUILayout.BeginHorizontal(); - GUILayout.Label($"{L("Min Players:", "Мин. игроков:")} {AutoHostMinPlayers}", sliderLabelStyle, GUILayout.Width(175)); - AutoHostMinPlayers = (int)GUILayout.HorizontalSlider(AutoHostMinPlayers, 1f, 15f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - GUILayout.Label($"{L("Start Delay:", "Задержка старта:")} {Mathf.Round(AutoHostStartDelaySeconds)}s", sliderLabelStyle, GUILayout.Width(175)); - AutoHostStartDelaySeconds = GUILayout.HorizontalSlider(AutoHostStartDelaySeconds, 0f, 180f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - GUILayout.Label($"{L("Fast Start Players:", "Игроков для фаст-старта:")} {AutoHostFastStartPlayers}", sliderLabelStyle, GUILayout.Width(175)); - AutoHostFastStartPlayers = (int)GUILayout.HorizontalSlider(AutoHostFastStartPlayers, 0f, 15f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - GUILayout.Label($"{L("Fast Start Delay:", "Задержка фаст-старта:")} {Mathf.Round(AutoHostFastStartDelaySeconds)}s", sliderLabelStyle, GUILayout.Width(175)); - AutoHostFastStartDelaySeconds = GUILayout.HorizontalSlider(AutoHostFastStartDelaySeconds, 0f, 60f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); - GUILayout.EndHorizontal(); - - GUILayout.EndVertical(); - } - private void LoadConfig() - { - try - { - spoofLevelString = Plugin.SpoofedLevel.Value; - enableFriendCodeSpoof = Plugin.EnableFriendCodeSpoofConfig.Value; - spoofFriendCodeInput = Plugin.SpoofFriendCodeConfig.Value; - enablePlatformSpoof = Plugin.EnablePlatformSpoof.Value; - autoBanBrokenFriendCode = Plugin.AutoBanBrokenFriendCodeConfig.Value; - currentPlatformIndex = Plugin.PlatformIndex.Value; - showWatermark = Plugin.ShowWatermarkConfig.Value; - unlockCosmetics = Plugin.UnlockCosmeticsConfig.Value; - moreLobbyInfo = Plugin.MoreLobbyInfoConfig.Value; - enableChatDarkMode = Plugin.EnableChatDarkModeConfig.Value; - ghostChatColorHex = SanitizeHexColor(Plugin.GhostChatColorConfig.Value, "#D7B8FF"); - enableAnomalyLogReports = Plugin.EnableAnomalyLogReportsConfig.Value; - showEspFriendCode = Plugin.ShowEspFriendCodeConfig.Value; - rpcSpoofDelay = Plugin.RpcSpoofDelayConfig.Value; - currentMenuColorIndex = Plugin.MenuColorIndexConfig.Value; - rgbMenuMode = Plugin.RgbMenuModeConfig.Value; - whiteMenuTheme = LoadBool("M_WhiteTheme", whiteMenuTheme); - currentMenuLanguageIndex = Mathf.Clamp(LoadInt("M_MenuLanguageIndex", currentMenuLanguageIndex), 0, menuLanguageNames.Length - 1); - fpsLimit = Mathf.Clamp(LoadInt("M_FpsLimit", fpsLimit), 60, 240); - ApplyFpsLimit(); - autoKickBugs = LoadBool("M_AutoKickBugs", autoKickBugs); - if (PlayerPrefs.HasKey("M_AutoKickTimer")) autoKickTimer = PlayerPrefs.GetFloat("M_AutoKickTimer"); - disableVoteKicks = LoadBool("M_DisableVoteKicks", disableVoteKicks); - enableLocalNameSpoof = LoadBool("M_LocalNameSpoof", enableLocalNameSpoof); - enableLocalFriendCodeSpoof = LoadBool("M_LocalFakeFCEnabled", enableLocalFriendCodeSpoof); - if (PlayerPrefs.HasKey("M_LocalFakeFC")) localFriendCodeInput = PlayerPrefs.GetString("M_LocalFakeFC"); - if (PlayerPrefs.HasKey("M_BndMagnet")) bindMagnetCursor = (KeyCode)PlayerPrefs.GetInt("M_BndMagnet"); - menuToggleKey = Plugin.MenuKeybind.Value == KeyCode.None ? KeyCode.Insert : Plugin.MenuKeybind.Value; - if (PlayerPrefs.HasKey("M_MenuToggleKey")) menuToggleKey = (KeyCode)PlayerPrefs.GetInt("M_MenuToggleKey"); - if (menuToggleKey == KeyCode.None) menuToggleKey = KeyCode.Insert; - if (PlayerPrefs.HasKey("M_BndMMorph")) bindMassMorph = (KeyCode)PlayerPrefs.GetInt("M_BndMMorph"); - if (PlayerPrefs.HasKey("M_BndSpawn")) bindSpawnLobby = (KeyCode)PlayerPrefs.GetInt("M_BndSpawn"); - if (PlayerPrefs.HasKey("M_BndDespawn")) bindDespawnLobby = (KeyCode)PlayerPrefs.GetInt("M_BndDespawn"); - if (PlayerPrefs.HasKey("M_BndCloseMtg")) bindCloseMeeting = (KeyCode)PlayerPrefs.GetInt("M_BndCloseMtg"); - if (PlayerPrefs.HasKey("M_BndInstaStart")) bindInstaStart = (KeyCode)PlayerPrefs.GetInt("M_BndInstaStart"); - if (PlayerPrefs.HasKey("M_BndEndCrew")) bindEndCrew = (KeyCode)PlayerPrefs.GetInt("M_BndEndCrew"); - if (PlayerPrefs.HasKey("M_BndEndImp")) bindEndImp = (KeyCode)PlayerPrefs.GetInt("M_BndEndImp"); - if (PlayerPrefs.HasKey("M_BndEndImpDC")) bindEndImpDC = (KeyCode)PlayerPrefs.GetInt("M_BndEndImpDC"); - if (PlayerPrefs.HasKey("M_BndEndHnsDC")) bindEndHnsDC = (KeyCode)PlayerPrefs.GetInt("M_BndEndHnsDC"); - if (PlayerPrefs.HasKey("M_BndToggleTracers")) bindToggleTracers = (KeyCode)PlayerPrefs.GetInt("M_BndToggleTracers"); - if (PlayerPrefs.HasKey("M_BndToggleNoClip")) bindToggleNoClip = (KeyCode)PlayerPrefs.GetInt("M_BndToggleNoClip"); - if (PlayerPrefs.HasKey("M_BndToggleFreecam")) bindToggleFreecam = (KeyCode)PlayerPrefs.GetInt("M_BndToggleFreecam"); - if (PlayerPrefs.HasKey("M_BndToggleCameraZoom")) bindToggleCameraZoom = (KeyCode)PlayerPrefs.GetInt("M_BndToggleCameraZoom"); - if (PlayerPrefs.HasKey("M_BndKillAll")) bindKillAll = (KeyCode)PlayerPrefs.GetInt("M_BndKillAll"); - if (PlayerPrefs.HasKey("M_BndCallMeeting")) bindCallMeeting = (KeyCode)PlayerPrefs.GetInt("M_BndCallMeeting"); - if (PlayerPrefs.HasKey("M_BndTogglePlayerInfo")) bindTogglePlayerInfo = (KeyCode)PlayerPrefs.GetInt("M_BndTogglePlayerInfo"); - if (PlayerPrefs.HasKey("M_BndToggleSeeRoles")) bindToggleSeeRoles = (KeyCode)PlayerPrefs.GetInt("M_BndToggleSeeRoles"); - if (PlayerPrefs.HasKey("M_BndToggleSeeGhosts")) bindToggleSeeGhosts = (KeyCode)PlayerPrefs.GetInt("M_BndToggleSeeGhosts"); - if (PlayerPrefs.HasKey("M_BndToggleFullBright")) bindToggleFullBright = (KeyCode)PlayerPrefs.GetInt("M_BndToggleFullBright"); - if (PlayerPrefs.HasKey("M_BndKickAll")) bindKickAll = (KeyCode)PlayerPrefs.GetInt("M_BndKickAll"); - if (PlayerPrefs.HasKey("M_BndFixSabotages")) bindFixSabotages = (KeyCode)PlayerPrefs.GetInt("M_BndFixSabotages"); - if (PlayerPrefs.HasKey("M_BndSetAllGhost")) bindSetAllGhost = (KeyCode)PlayerPrefs.GetInt("M_BndSetAllGhost"); - if (PlayerPrefs.HasKey("M_BndSetAllGhostImp")) bindSetAllGhostImp = (KeyCode)PlayerPrefs.GetInt("M_BndSetAllGhostImp"); - - if (!rgbMenuMode && currentMenuColorIndex >= 0 && currentMenuColorIndex < menuColors.Length) - { - currentAccentColor = menuColors[currentMenuColorIndex]; - } - - showPlayerInfo = LoadBool("M_ShowPlayerInfo", showPlayerInfo); - seeGhosts = LoadBool("M_SeeGhosts", seeGhosts); - seeRoles = LoadBool("M_SeeRoles", seeRoles); - revealMeetingRoles = LoadBool("M_RevealMeetingRoles", revealMeetingRoles); - showTracers = LoadBool("M_ShowTracers", showTracers); - fullBright = LoadBool("M_FullBright", fullBright); - seeProtections = LoadBool("M_SeeProtections", seeProtections); - seeKillCooldown = LoadBool("M_SeeKillCooldown", seeKillCooldown); - extendedLobby = LoadBool("M_ExtendedLobby", extendedLobby); - moreLobbyInfo = LoadBool("M_MoreLobbyInfo", moreLobbyInfo); - alwaysChat = LoadBool("M_AlwaysChat", alwaysChat); - readGhostChat = LoadBool("M_ReadGhostChat", readGhostChat); - enableExtendedChat = LoadBool("M_EnableExtendedChat", enableExtendedChat); - enableFastChat = LoadBool("M_EnableFastChat", enableFastChat); - allowLinksAndSymbols = LoadBool("M_AllowLinksAndSymbols", allowLinksAndSymbols); - enableChatHistory = LoadBool("M_EnableChatHistory", enableChatHistory); - chatHistoryLimit = Mathf.Clamp(LoadInt("M_ChatHistoryLimit", chatHistoryLimit), 5, 80); - enableClipboard = LoadBool("M_EnableClipboard", enableClipboard); - enableChatLog = LoadBool("M_EnableChatLog", enableChatLog); - enableColorCommand = LoadBool("M_EnableColorCommand", enableColorCommand); - blockRainbowChat = LoadBool("M_BlockRainbowChat", blockRainbowChat); - blockFortegreenChat = LoadBool("M_BlockFortegreenChat", blockFortegreenChat); - SpoofMenuEnabled = LoadBool("M_SpoofMenuEnabled", SpoofMenuEnabled); - noClip = LoadBool("M_NoClip", noClip); - tpToCursor = LoadBool("M_TpToCursor", tpToCursor); - dragToCursor = LoadBool("M_DragToCursor", dragToCursor); - autoFollowCursor = LoadBool("M_AutoFollowCursor", autoFollowCursor); - freecam = LoadBool("M_Freecam", freecam); - cameraZoom = LoadBool("M_CameraZoom", cameraZoom); - RevealVotesEnabled = LoadBool("M_RevealVotes", RevealVotesEnabled); - noTaskMode = LoadBool("M_NoTaskMode", noTaskMode); - noMapCooldowns = LoadBool("M_NoMapCooldowns", noMapCooldowns); - neverEndGame = LoadBool("M_NeverEndGame", neverEndGame); - removePenalty = LoadBool("M_RemovePenalty", removePenalty); - alwaysShowLobbyTimer = LoadBool("M_AlwaysShowLobbyTimer", alwaysShowLobbyTimer); - autoBanEnabled = LoadBool("M_AutoBanEnabled", autoBanEnabled); - allowDuplicateColors = LoadBool("M_AllowDuplicateColors", allowDuplicateColors); - blockSpoofRPC = LoadBool("M_BlockSpoofRPC", blockSpoofRPC); - autoBanPlatformSpoof = LoadBool("M_AutoBanPlatformSpoof", autoBanPlatformSpoof); - banCustomPlatformsFromTxt = LoadBool("M_BanCustomPlatformsFromTxt", banCustomPlatformsFromTxt); - blockSabotageRPC = LoadBool("M_BlockSabotageRPC", blockSabotageRPC); - blockGameRpcInLobby = LoadBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); - blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); - blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); - enablePasosLimit = LoadBool("M_PasosLimit", enablePasosLimit); - if (PlayerPrefs.HasKey("M_RpcSpamLimit")) rpcSpamLimit = Mathf.Clamp(PlayerPrefs.GetInt("M_RpcSpamLimit"), 10, 250); - enableLocalPasosBan = LoadBool("M_AntiPasosLocalBan", enableLocalPasosBan); - enableHostPasosBan = LoadBool("M_AntiPasosHostBan", enableHostPasosBan); - AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); - AutoReturnLobbyAfterMatch = LoadBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); - AutoHostNotifications = LoadBool("M_AutoHostNotifications", AutoHostNotifications); - AutoHostForceLastMinute = LoadBool("M_AutoHostForceLastMinute", AutoHostForceLastMinute); - AutoHostWaitLoadedPlayers = LoadBool("M_AutoHostWaitLoadedPlayers", AutoHostWaitLoadedPlayers); - AutoHostCancelBelowMin = LoadBool("M_AutoHostCancelBelowMin", AutoHostCancelBelowMin); - AutoHostInstantStart = LoadBool("M_AutoHostInstantStart", AutoHostInstantStart); - autoGhostAfterStart = LoadBool("M_AutoGhostAfterStart", autoGhostAfterStart); - if (PlayerPrefs.HasKey("M_AutoHostMinPlayers")) AutoHostMinPlayers = PlayerPrefs.GetInt("M_AutoHostMinPlayers"); - if (PlayerPrefs.HasKey("M_AutoHostStartDelaySeconds")) AutoHostStartDelaySeconds = PlayerPrefs.GetFloat("M_AutoHostStartDelaySeconds"); - if (PlayerPrefs.HasKey("M_AutoHostFastStartPlayers")) AutoHostFastStartPlayers = PlayerPrefs.GetInt("M_AutoHostFastStartPlayers"); - if (PlayerPrefs.HasKey("M_AutoHostFastStartDelaySeconds")) AutoHostFastStartDelaySeconds = PlayerPrefs.GetFloat("M_AutoHostFastStartDelaySeconds"); - if (PlayerPrefs.HasKey("M_WalkSpeed")) walkSpeed = PlayerPrefs.GetFloat("M_WalkSpeed"); - if (PlayerPrefs.HasKey("M_EngineSpeed")) engineSpeed = PlayerPrefs.GetFloat("M_EngineSpeed"); - for (int i = 0; i < favoriteOutfitSlots.Length; i++) - favoriteOutfitSlots[i] = PlayerPrefs.GetString($"M_FavoriteOutfit_{i}", string.Empty); - enableBackground = LoadBool("M_EnableBackground", enableBackground); - EnableCustomNotifs = LoadBool("M_EnableCustomNotifs", EnableCustomNotifs); - LogAllRPCs = LoadBool("M_LogAllRPCs", LogAllRPCs); - selectedSpoofMenuIndex = Mathf.Clamp(LoadInt("M_SelectedSpoofMenuIndex", selectedSpoofMenuIndex), 0, spoofMenuNames.Length - 1); - windowRect = new Rect( - LoadFloat("M_MenuWindowX", windowRect.x), - LoadFloat("M_MenuWindowY", windowRect.y), - Mathf.Clamp(LoadFloat("M_MenuWindowW", windowRect.width), 640f, 1400f), - Mathf.Clamp(LoadFloat("M_MenuWindowH", windowRect.height), 420f, 900f)); - currentTab = Mathf.Clamp(LoadInt("M_CurrentTab", currentTab), 0, tabNames.Length - 1); - targetTabIndex = Mathf.Clamp(LoadInt("M_TargetTab", currentTab), 0, tabNames.Length - 1); - currentGeneralSubTab = Mathf.Clamp(LoadInt("M_CurrentGeneralSubTab", currentGeneralSubTab), 0, generalSubTabs.Length - 1); - currentGeneralInfoSubTab = Mathf.Clamp(LoadInt("M_CurrentGeneralInfoSubTab", currentGeneralInfoSubTab), 0, generalInfoSubTabs.Length - 1); - currentSelfSubTab = Mathf.Clamp(LoadInt("M_CurrentSelfSubTab", currentSelfSubTab), 0, selfSubTabs.Length); - currentVisualsSubTab = Mathf.Clamp(LoadInt("M_CurrentVisualsSubTab", currentVisualsSubTab), 0, visualsSubTabs.Length - 1); - currentPlayersSubTab = Mathf.Clamp(LoadInt("M_CurrentPlayersSubTab", currentPlayersSubTab), 0, playersSubTabs.Length - 1); - currentHostOnlySubTab = Mathf.Clamp(LoadInt("M_CurrentHostOnlySubTab", currentHostOnlySubTab), 0, hostOnlySubTabs.Length - 1); - currentAutoHostSubTab = Mathf.Clamp(LoadInt("M_CurrentAutoHostSubTab", currentAutoHostSubTab), 0, autoHostSubTabs.Length - 1); - tabTransitionProgress = 1f; - SyncKeybindDictionary(); - if (PlayerPrefs.HasKey("M_SpoofName")) customNameInput = PlayerPrefs.GetString("M_SpoofName"); - } - catch { } - } - - private static void ApplyFpsLimit() - { - try - { - fpsLimit = Mathf.Clamp(fpsLimit, 60, 240); - if (lastAppliedFpsLimit == fpsLimit) return; - Application.targetFrameRate = fpsLimit; - QualitySettings.vSyncCount = 0; - lastAppliedFpsLimit = fpsLimit; - } - catch { } - } - - private static void TrimChatHistoryToLimit() - { - try - { - chatHistoryLimit = Mathf.Clamp(chatHistoryLimit, 5, 80); - while (ChatHistory.sentMessages.Count > chatHistoryLimit) - ChatHistory.sentMessages.RemoveAt(0); - - ChatHistory.HistoryIndex = Mathf.Clamp(ChatHistory.HistoryIndex, 0, ChatHistory.sentMessages.Count); - } - catch { } - } - - private static void SyncKeybindDictionary() - { - try - { - keyBinds["Toggle Menu"] = menuToggleKey; - keyBinds["Magnet Cursor"] = bindMagnetCursor; - keyBinds["Mass Morph"] = bindMassMorph; - keyBinds["Spawn Lobby"] = bindSpawnLobby; - keyBinds["Despawn Lobby"] = bindDespawnLobby; - keyBinds["Close Meeting"] = bindCloseMeeting; - keyBinds["Insta Start"] = bindInstaStart; - keyBinds["End Crew"] = bindEndCrew; - keyBinds["End Imp"] = bindEndImp; - keyBinds["End Imp DC"] = bindEndImpDC; - keyBinds["End H&S DC"] = bindEndHnsDC; - keyBinds["Toggle Tracers"] = bindToggleTracers; - keyBinds["Toggle NoClip"] = bindToggleNoClip; - keyBinds["Toggle Freecam"] = bindToggleFreecam; - keyBinds["Toggle Camera Zoom"] = bindToggleCameraZoom; - keyBinds["Toggle Player Info"] = bindTogglePlayerInfo; - keyBinds["Toggle See Roles"] = bindToggleSeeRoles; - keyBinds["Toggle See Ghosts"] = bindToggleSeeGhosts; - keyBinds["Toggle Full Bright"] = bindToggleFullBright; - keyBinds["Kill All"] = bindKillAll; - keyBinds["Call Meeting"] = bindCallMeeting; - keyBinds["Kick All"] = bindKickAll; - keyBinds["Fix Sabotages"] = bindFixSabotages; - keyBinds["All Ghost"] = bindSetAllGhost; - keyBinds["All Ghost Imp"] = bindSetAllGhostImp; - } - catch { } - } - private Texture2D MakeRoundedTex(int size, Color col, float radius) - { - Texture2D result = new Texture2D(size, size, TextureFormat.RGBA32, false); - result.hideFlags = HideFlags.HideAndDontSave; - Color[] pix = new Color[size * size]; - float center = size / 2f; - for (int y = 0; y < size; y++) - { - for (int x = 0; x < size; x++) - { - float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); - float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); - float dist = Mathf.Sqrt(dx * dx + dy * dy); - float alpha = Mathf.Clamp01(radius - dist + 0.5f); - Color c = col; - c.a = col.a * alpha; - pix[y * size + x] = c; - } - } - result.SetPixels(pix); result.Apply(); - return result; - } - - private RectOffset CreateRectOffset(int left, int right, int top, int bottom) - { - return new RectOffset { left = left, right = right, top = top, bottom = bottom }; - } - - private void UpdateSwitchTex(Texture2D tex, bool isOn, Color accentColor) - { - int width = tex.width; int height = tex.height; - Color transparent = new Color(0, 0, 0, 0); - Color offBg = new Color(0.23f, 0.23f, 0.23f, 1f); - Color offKnob = new Color(0.6f, 0.6f, 0.6f, 1f); - Color bgColor = isOn ? accentColor : offBg; - Color knobColor = isOn ? Color.white : offKnob; - float r = height / 2f; - float cx1 = r; float cx2 = width - r; float cy = r; - float knobRadius = r - 2f; - float knobCx = isOn ? cx2 : cx1; - Color[] pixels = new Color[width * height]; - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - float dLeft = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx1, cy)); - float dRight = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx2, cy)); - float dRect = (x + 0.5f >= cx1 && x + 0.5f <= cx2) ? Mathf.Abs((y + 0.5f) - cy) : 9999f; - float distBg = Mathf.Min(dLeft, Mathf.Min(dRight, dRect)); - float alphaBg = Mathf.Clamp01(r - distBg + 0.5f); - float distKnob = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(knobCx, cy)); - float alphaKnob = Mathf.Clamp01(knobRadius - distKnob + 0.5f); - if (alphaBg > 0) - { - Color finalCol = Color.Lerp(bgColor, knobColor, alphaKnob); - finalCol.a = alphaBg; - pixels[y * width + x] = finalCol; - } - else pixels[y * width + x] = transparent; - } - } - tex.SetPixels(pixels); tex.Apply(); - } - - private static Color GetThemeAccentColor(Color source) - { - if (!whiteMenuTheme) return source; - - Color.RGBToHSV(source, out float h, out float s, out float v); - - if (s < 0.08f) - return new Color(0.34f, 0.34f, 0.34f, 1f); - - if (h <= 0.04f || h >= 0.96f) - return new Color(0.50f, 0.14f, 0.18f, 1f); - - if (h >= 0.11f && h <= 0.19f) - return new Color32(232, 194, 37, 255); - - float targetS = Mathf.Clamp(Mathf.Max(s, 0.55f), 0.55f, 0.95f); - float targetV = Mathf.Clamp(v * 0.62f, 0.26f, 0.72f); - Color mapped = Color.HSVToRGB(h, targetS, targetV); - mapped.a = 1f; - return mapped; - } - - private void UpdateAccentColor(Color color) - { - currentAccentColor = color; - Color effectiveColor = GetThemeAccentColor(color); - if (texAccent != null) - { - int size = texAccent.width; - Color[] pix = new Color[size * size]; - float center = size / 2f; - float radius = 6f; - for (int y = 0; y < size; y++) - { - for (int x = 0; x < size; x++) - { - float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); - float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); - float dist = Mathf.Sqrt(dx * dx + dy * dy); - float alpha = Mathf.Clamp01(radius - dist + 0.5f); - Color c = effectiveColor; c.a = alpha; - pix[y * size + x] = c; - } - } - texAccent.SetPixels(pix); texAccent.Apply(); - } - if (texSliderThumb != null) - { - int size = texSliderThumb.width; - Color[] pix = new Color[size * size]; - float center = size / 2f; - float radius = 10f; - for (int y = 0; y < size; y++) - { - for (int x = 0; x < size; x++) - { - float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); - float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); - float dist = Mathf.Sqrt(dx * dx + dy * dy); - float alpha = Mathf.Clamp01(radius - dist + 0.5f); - Color c = effectiveColor; c.a = alpha; - pix[y * size + x] = c; - } - } - texSliderThumb.SetPixels(pix); texSliderThumb.Apply(); - } - if (texScrollThumb != null) - { - int size = texScrollThumb.width; - Color[] pix = new Color[size * size]; - float center = size / 2f; - float radius = 4f; - for (int y = 0; y < size; y++) - { - for (int x = 0; x < size; x++) - { - float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); - float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); - float dist = Mathf.Sqrt(dx * dx + dy * dy); - float alpha = Mathf.Clamp01(radius - dist + 0.5f); - Color c = effectiveColor; c.a = alpha; - pix[y * size + x] = c; - } - } - texScrollThumb.SetPixels(pix); texScrollThumb.Apply(); - } - if (texToggleOn != null) UpdateSwitchTex(texToggleOn, true, effectiveColor); - if (windowStyle != null) windowStyle.normal.textColor = whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : color; - if (headerStyle != null) headerStyle.normal.textColor = whiteMenuTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : color; - if (activeSidebarBtnStyle != null) { activeSidebarBtnStyle.normal.textColor = effectiveColor; activeSidebarBtnStyle.hover.textColor = effectiveColor; } - if (activeTabStyle != null) activeTabStyle.normal.background = texAccent; - if (activeSubTabStyle != null) activeSubTabStyle.normal.background = texAccent; - if (btnStyle != null) btnStyle.active.background = texAccent; - if (inputBlockStyle != null) inputBlockStyle.normal.textColor = whiteMenuTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : color; - } - - private void InitStyles() - { - bool isLightTheme = whiteMenuTheme; - Color accent = GetThemeAccentColor(currentAccentColor); - Color darkBg = isLightTheme ? new Color(0.97f, 0.97f, 0.97f, 0.78f) : new Color(0.12f, 0.12f, 0.12f, 0.90f); - Color sidebarBg = new Color(0.0f, 0.0f, 0.0f, 0.0f); - Color boxBg = new Color(0f, 0f, 0f, 0f); - Color btnCol = isLightTheme ? new Color(0.90f, 0.90f, 0.90f, 0.74f) : new Color(0.23f, 0.23f, 0.23f, 1f); - Color sliderBgCol = isLightTheme ? new Color(0.78f, 0.78f, 0.78f, 0.68f) : new Color(0.08f, 0.08f, 0.08f, 1f); - Color textMain = isLightTheme ? new Color(0.18f, 0.18f, 0.18f, 1f) : new Color(0.78f, 0.78f, 0.78f, 1f); - Color textMuted = isLightTheme ? new Color(0.33f, 0.33f, 0.33f, 1f) : new Color(0.6f, 0.6f, 0.6f, 1f); - Color textHover = isLightTheme ? new Color(0.06f, 0.06f, 0.06f, 1f) : Color.white; - Color headerText = isLightTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : accent; - Color inputBgCol = isLightTheme ? new Color(1f, 1f, 1f, 0.86f) : new Color(0.08f, 0.08f, 0.08f, 0.85f); - - texWindowBg = MakeRoundedTex(64, darkBg, 12f); - texSidebarBg = MakeRoundedTex(64, sidebarBg, 0f); - texBoxBg = MakeRoundedTex(64, boxBg, 0f); - texBtnBg = MakeRoundedTex(64, btnCol, 6f); - texAccent = MakeRoundedTex(64, accent, 6f); - texSliderBg = MakeRoundedTex(64, sliderBgCol, 4f); - texSliderThumb = MakeRoundedTex(20, accent, 10f); - texInputBg = MakeRoundedTex(64, inputBgCol, 6f); - texColorBtn = MakeRoundedTex(64, Color.white, 12f); - - texToggleOff = new Texture2D(30, 16, TextureFormat.RGBA32, false); - texToggleOff.hideFlags = HideFlags.HideAndDontSave; - texToggleOn = new Texture2D(30, 16, TextureFormat.RGBA32, false); - texToggleOn.hideFlags = HideFlags.HideAndDontSave; - UpdateSwitchTex(texToggleOff, false, Color.white); - UpdateSwitchTex(texToggleOn, true, accent); - - safeLineStyle = new GUIStyle(); - safeLineStyle.normal.background = MakeRoundedTex(2, isLightTheme ? new Color(0.75f, 0.75f, 0.75f, 1f) : Color.white, 0f); - - windowStyle = new GUIStyle(); - windowStyle.normal.background = texWindowBg; - windowStyle.normal.textColor = accent; - windowStyle.fontStyle = FontStyle.Bold; - windowStyle.fontSize = 14; - windowStyle.padding = CreateRectOffset(0, 0, 0, 0); - windowStyle.border = CreateRectOffset(12, 12, 12, 12); - - boxStyle = new GUIStyle(); - boxStyle.normal.background = texBoxBg; - boxStyle.padding = CreateRectOffset(0, 0, 0, 0); - boxStyle.margin = CreateRectOffset(0, 0, 4, 8); - - btnStyle = new GUIStyle(GUI.skin.button); - btnStyle.normal.background = texBtnBg; - btnStyle.normal.textColor = textMain; - btnStyle.active.background = texAccent; - btnStyle.active.textColor = Color.black; - btnStyle.alignment = TextAnchor.MiddleCenter; - btnStyle.border = CreateRectOffset(6, 6, 6, 6); - btnStyle.fontSize = 12; - btnStyle.fontStyle = FontStyle.Bold; - - activeTabStyle = new GUIStyle(btnStyle); - activeTabStyle.normal.background = texAccent; - activeTabStyle.normal.textColor = Color.black; - - subTabStyle = new GUIStyle(btnStyle); - subTabStyle.padding = CreateRectOffset(6, 6, 2, 2); - activeSubTabStyle = new GUIStyle(activeTabStyle); - activeSubTabStyle.padding = CreateRectOffset(6, 6, 2, 2); - - inputBlockStyle = new GUIStyle(btnStyle); - inputBlockStyle.normal.background = texInputBg; - inputBlockStyle.hover.background = texInputBg; - inputBlockStyle.active.background = texAccent; - inputBlockStyle.normal.textColor = isLightTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : accent; - inputBlockStyle.alignment = TextAnchor.MiddleCenter; - inputBlockStyle.fontStyle = FontStyle.Bold; - - headerStyle = new GUIStyle(); - headerStyle.normal.background = texBtnBg; - headerStyle.normal.textColor = headerText; - headerStyle.fontStyle = FontStyle.Bold; - headerStyle.alignment = TextAnchor.MiddleLeft; - headerStyle.padding = CreateRectOffset(6, 6, 4, 4); - headerStyle.margin = CreateRectOffset(0, 0, 4, 4); - headerStyle.fontSize = 13; - - sidebarStyle = new GUIStyle(); - sidebarStyle.normal.background = texSidebarBg; - sidebarStyle.padding = CreateRectOffset(0, 0, 5, 0); - - sidebarBtnStyle = new GUIStyle(); - sidebarBtnStyle.normal.textColor = textMuted; - sidebarBtnStyle.hover.textColor = textHover; - sidebarBtnStyle.padding = CreateRectOffset(12, 0, 6, 6); - sidebarBtnStyle.alignment = TextAnchor.MiddleLeft; - sidebarBtnStyle.fontSize = 13; - sidebarBtnStyle.fontStyle = FontStyle.Bold; - - activeSidebarBtnStyle = new GUIStyle(sidebarBtnStyle); - activeSidebarBtnStyle.normal.textColor = accent; - activeSidebarBtnStyle.hover.textColor = accent; - - toggleOffStyle = new GUIStyle(); - toggleOffStyle.normal.background = texToggleOff; - toggleOnStyle = new GUIStyle(); - toggleOnStyle.normal.background = texToggleOn; - - toggleLabelStyle = new GUIStyle(); - toggleLabelStyle.normal.textColor = textMain; - toggleLabelStyle.alignment = TextAnchor.MiddleLeft; - toggleLabelStyle.padding = CreateRectOffset(4, 0, 0, 0); - toggleLabelStyle.fontSize = 12; - toggleLabelStyle.fontStyle = FontStyle.Bold; - - sliderStyle = new GUIStyle(); - sliderStyle.normal.background = texSliderBg; - sliderStyle.border = CreateRectOffset(6, 6, 6, 6); - sliderStyle.fixedHeight = 10f; - sliderStyle.margin = CreateRectOffset(0, 0, 8, 8); - - sliderThumbStyle = new GUIStyle(); - sliderThumbStyle.normal.background = texSliderThumb; - sliderThumbStyle.fixedWidth = 18f; - sliderThumbStyle.fixedHeight = 18f; - sliderThumbStyle.margin = CreateRectOffset(0, 0, -4, 0); - - titleStyle = new GUIStyle(); - titleStyle.normal.textColor = accent; - titleStyle.fontStyle = FontStyle.Bold; - titleStyle.fontSize = 14; - titleStyle.padding = CreateRectOffset(10, 0, 8, 0); - - Texture2D texScrollBg = MakeRoundedTex(8, new Color(0.1f, 0.1f, 0.1f, 0.2f), 4f); - texScrollThumb = MakeRoundedTex(8, accent, 4f); - - GUIStyle scrollBarStyle = new GUIStyle(GUI.skin.verticalScrollbar); - scrollBarStyle.normal.background = texScrollBg; - scrollBarStyle.fixedWidth = 8f; - scrollBarStyle.border = CreateRectOffset(0, 0, 4, 4); - scrollBarStyle.margin = CreateRectOffset(2, 2, 2, 2); - - GUIStyle scrollBarThumbStyle = new GUIStyle(GUI.skin.verticalScrollbarThumb); - scrollBarThumbStyle.normal.background = texScrollThumb; - scrollBarThumbStyle.hover.background = texScrollThumb; - scrollBarThumbStyle.active.background = texScrollThumb; - scrollBarThumbStyle.fixedWidth = 8f; - scrollBarThumbStyle.border = CreateRectOffset(0, 0, 4, 4); - - GUI.skin.verticalScrollbar = scrollBarStyle; - GUI.skin.verticalScrollbarThumb = scrollBarThumbStyle; - GUI.skin.horizontalScrollbar.normal.background = null; - GUI.skin.horizontalScrollbarThumb.normal.background = null; - GUI.skin.label.normal.textColor = textMain; - GUI.skin.box.normal.textColor = textMain; - - stylesInited = true; - } - public static bool autoCopyCodeAndLeave = false; - public static HashSet votedPlayerIds = new HashSet(); - - private void LoadBackgroundImage() - { - try - { - string bgPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "MenuBG.png"); - if (!System.IO.File.Exists(bgPath)) bgPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "MenuBG.jpg"); - if (System.IO.File.Exists(bgPath)) - { - byte[] fileData = System.IO.File.ReadAllBytes(bgPath); - Texture2D tempTex = new Texture2D(2, 2); - ImageConversion.LoadImage(tempTex, fileData); - customMenuBg = new Texture2D(tempTex.width, tempTex.height, TextureFormat.RGBA32, false); - customMenuBg.hideFlags = HideFlags.HideAndDontSave; - Color[] pix = tempTex.GetPixels(); - UnityEngine.Object.Destroy(tempTex); - int w = customMenuBg.width, h = customMenuBg.height; - float targetRadius = 12f, rx = targetRadius * (w / windowRect.width), ry = targetRadius * (h / windowRect.height); - for (int y = 0; y < h; y++) - for (int x = 0; x < w; x++) - { - float dx = 0f, dy = 0f; - if (x < rx) dx = rx - x; - else if (x > w - rx) dx = x - (w - rx); - if (y < ry) dy = ry - y; - else if (y > h - ry) dy = y - (h - ry); - if (dx > 0 && dy > 0) - { - float nx = dx / rx, ny = dy / ry; - float dist = Mathf.Sqrt(nx * nx + ny * ny); - if (dist > 1f) { Color c = pix[y * w + x]; c.a = 0f; pix[y * w + x] = c; } - else - { - float alphaMult = Mathf.Clamp01((1f - dist) * Mathf.Max(rx, ry)); - Color c = pix[y * w + x]; c.a *= alphaMult; pix[y * w + x] = c; - } - } - } - customMenuBg.SetPixels(pix); customMenuBg.Apply(); - } - else enableBackground = false; - } - catch { enableBackground = false; } - } - - public static string ApplyMenuShimmer(string text) - { - string result = ""; - Color baseColor = currentAccentColor, glowColor = Color.white; - for (int i = 0; i < text.Length; i++) - { - if (text[i] == ' ') { result += " "; continue; } - float wave = Mathf.Sin(Time.unscaledTime * 6f - (i * 0.4f)) * 0.5f + 0.5f; - Color c = Color.Lerp(baseColor, glowColor, wave); - result += $"{text[i]}"; - } - return result; - } - - private bool DrawToggle(bool value, string text, int width = 0) - { - GUILayout.BeginHorizontal(GUILayout.Width(width > 0 ? width : 200)); - - bool clickedBox = GUILayout.Button("", value ? toggleOnStyle : toggleOffStyle, GUILayout.Width(30), GUILayout.Height(16)); - - GUILayout.Space(6); - - bool clickedText = GUILayout.Button(text, toggleLabelStyle); - - GUILayout.EndHorizontal(); - - return (clickedBox || clickedText) ? !value : value; - } - - private bool DrawBindableButton(string label, string bindKey, float width) - { - bool clicked = false; - GUILayout.BeginVertical(GUILayout.Width(width)); - if (GUILayout.Button(label, btnStyle, GUILayout.Height(25), GUILayout.Width(width))) clicked = true; - string bindTxt = bindingAction == bindKey ? "Press Key..." : (keyBinds.ContainsKey(bindKey) ? $"[{keyBinds[bindKey]}]" : "[Bind Key]"); - GUIStyle bindStyle = new GUIStyle(btnStyle) { fontSize = 10, normal = { textColor = new Color(0.6f, 0.6f, 0.6f) } }; - if (bindingAction == bindKey) bindStyle.normal.textColor = GetThemeAccentColor(currentAccentColor); - if (GUILayout.Button(bindTxt, bindStyle, GUILayout.Height(15), GUILayout.Width(width))) bindingAction = bindKey; - GUILayout.EndVertical(); - return clicked; - } - - private bool DrawHostToggle(bool value, string text, float totalWidth = 250f) - { - GUILayout.BeginHorizontal(GUILayout.Width(totalWidth), GUILayout.Height(20)); - bool clickedBox = GUILayout.Button("", value ? toggleOnStyle : toggleOffStyle, GUILayout.Width(30), GUILayout.Height(16)); - GUILayout.Space(6); - bool clickedText = GUILayout.Button(text, toggleLabelStyle, GUILayout.Width(totalWidth - 36f), GUILayout.Height(16)); - GUILayout.EndHorizontal(); - return (clickedBox || clickedText) ? !value : value; - } - private void DrawBindsTab() - { - GUILayout.BeginVertical(boxStyle); - try - { - GUILayout.Label("CUSTOM KEYBINDS", headerStyle); - GUILayout.Label(L("Menu toggle is configurable. Right Shift stays disabled.", "Кнопку меню можно менять. Right Shift выключен."), safeLineStyle); - GUILayout.Space(10); - - DrawKeybindRow("Menu Toggle:", ref menuToggleKey, ref isWaitingForBind); - DrawKeybindRow("Magnet Cursor:", ref bindMagnetCursor, ref isWaitBindMagnetCursor); - DrawKeybindRow("Mass Morph:", ref bindMassMorph, ref isWaitBindMassMorph); - DrawKeybindRow("Spawn Lobby:", ref bindSpawnLobby, ref isWaitBindSpawnLobby); - DrawKeybindRow("Despawn Lobby:", ref bindDespawnLobby, ref isWaitBindDespawnLobby); - DrawKeybindRow("Close Meeting:", ref bindCloseMeeting, ref isWaitBindCloseMeeting); - DrawKeybindRow("Insta Start:", ref bindInstaStart, ref isWaitBindInstaStart); - DrawKeybindRow("End: Crewmate Win:", ref bindEndCrew, ref isWaitBindEndCrew); - DrawKeybindRow("End: Impostor Win:", ref bindEndImp, ref isWaitBindEndImp); - DrawKeybindRow("End: Imp Disconnect:", ref bindEndImpDC, ref isWaitBindEndImpDC); - DrawKeybindRow("End: H&S Disconnect:", ref bindEndHnsDC, ref isWaitBindEndHnsDC); - DrawKeybindRow("Toggle Tracers:", ref bindToggleTracers, ref isWaitBindToggleTracers); - DrawKeybindRow("Toggle NoClip:", ref bindToggleNoClip, ref isWaitBindToggleNoClip); - DrawKeybindRow("Toggle Freecam:", ref bindToggleFreecam, ref isWaitBindToggleFreecam); - DrawKeybindRow("Toggle Camera Zoom:", ref bindToggleCameraZoom, ref isWaitBindToggleCameraZoom); - DrawKeybindRow("Toggle Player Info:", ref bindTogglePlayerInfo, ref isWaitBindTogglePlayerInfo); - DrawKeybindRow("Toggle See Roles:", ref bindToggleSeeRoles, ref isWaitBindToggleSeeRoles); - DrawKeybindRow("Toggle See Ghosts:", ref bindToggleSeeGhosts, ref isWaitBindToggleSeeGhosts); - DrawKeybindRow("Toggle Full Bright:", ref bindToggleFullBright, ref isWaitBindToggleFullBright); - DrawKeybindRow("Kill All:", ref bindKillAll, ref isWaitBindKillAll); - DrawKeybindRow("Call Meeting:", ref bindCallMeeting, ref isWaitBindCallMeeting); - DrawKeybindRow("Kick All:", ref bindKickAll, ref isWaitBindKickAll); - DrawKeybindRow("Fix Sabotages:", ref bindFixSabotages, ref isWaitBindFixSabotages); - DrawKeybindRow("All -> Ghost:", ref bindSetAllGhost, ref isWaitBindSetAllGhost); - DrawKeybindRow("All -> Ghost Imp:", ref bindSetAllGhostImp, ref isWaitBindSetAllGhostImp); - } - finally { GUILayout.EndVertical(); } - } - - private void DrawKeybindRow(string label, ref KeyCode currentKey, ref bool isWaiting) - { - GUILayout.BeginHorizontal(); - GUILayout.Space(10); - GUIStyle alignedLabel = new GUIStyle(toggleLabelStyle) { alignment = TextAnchor.MiddleLeft, margin = CreateRectOffset(0, 0, 4, 0) }; - GUILayout.Label(label, alignedLabel, GUILayout.Width(220), GUILayout.Height(25)); - - string bindText = isWaiting ? "Press any key..." : (currentKey == KeyCode.None ? "NONE" : currentKey.ToString()); - if (GUILayout.Button(bindText, isWaiting ? activeTabStyle : btnStyle, GUILayout.Width(120), GUILayout.Height(25))) - { - ResetAllBindWaits(); - isWaiting = true; - } - - if (GUILayout.Button("Clear", btnStyle, GUILayout.Width(50), GUILayout.Height(25))) - { - currentKey = KeyCode.None; - isWaiting = false; - SaveConfig(); - } - - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - } - public static bool AnimShieldsEnabled = false; - public static bool AnimAsteroidsEnabled = false; - public static bool AnimCamsInUseEnabled = false; - public static bool IsScanning = false; - private void ResetAllBindWaits() - { - isWaitingForBind = false; - isWaitBindMassMorph = false; - isWaitBindSpawnLobby = false; - isWaitBindDespawnLobby = false; - isWaitBindCloseMeeting = false; - isWaitBindInstaStart = false; - isWaitBindEndCrew = false; - isWaitBindEndImp = false; - isWaitBindEndImpDC = false; - isWaitBindEndHnsDC = false; - isWaitBindMagnetCursor = false; - isWaitBindToggleTracers = false; - isWaitBindToggleNoClip = false; - isWaitBindToggleFreecam = false; - isWaitBindToggleCameraZoom = false; - isWaitBindKillAll = false; - isWaitBindCallMeeting = false; - isWaitBindTogglePlayerInfo = false; - isWaitBindToggleSeeRoles = false; - isWaitBindToggleSeeGhosts = false; - isWaitBindToggleFullBright = false; - isWaitBindKickAll = false; - isWaitBindFixSabotages = false; - isWaitBindSetAllGhost = false; - isWaitBindSetAllGhostImp = false; - } - - private void DrawGeneralTab() - { - GUILayout.BeginHorizontal(); - for (int i = 0; i < generalSubTabs.Length; i++) - { - if (GUILayout.Button(generalSubTabs[i], currentGeneralSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(22))) - { - currentGeneralSubTab = i; - scrollPosition = Vector2.zero; - } - } - GUILayout.EndHorizontal(); - GUILayout.Space(10); - - if (currentGeneralSubTab == 0) DrawGeneralInfoTab(); - else if (currentGeneralSubTab == 1) DrawBindsTab(); - } - - private bool DrawColoredActionButton(string text, Color color, float width, float height = 24f) - { - GUIStyle style = new GUIStyle(btnStyle); - Color themedColor = whiteMenuTheme ? GetThemeAccentColor(color) : color; - Color hoverColor = whiteMenuTheme - ? Color.Lerp(themedColor, Color.black, 0.18f) - : Color.Lerp(themedColor, Color.white, 0.22f); - - style.normal.textColor = themedColor; - style.hover.textColor = hoverColor; - style.focused.textColor = themedColor; - style.active.textColor = whiteMenuTheme ? Color.white : Color.black; - - return GUILayout.Button(text, style, GUILayout.Width(width), GUILayout.Height(height)); - } - - private bool DrawPseudoInputButton(string value, bool editing, float height = 28f, int maxChars = 52) - { - GUIStyle style = new GUIStyle(editing ? activeTabStyle : inputBlockStyle); - style.alignment = TextAnchor.MiddleLeft; - style.clipping = TextClipping.Clip; - style.wordWrap = false; - style.padding = CreateRectOffset(10, 10, 0, 0); - - Rect rect = GUILayoutUtility.GetRect(GUIContent.none, style, GUILayout.ExpandWidth(true), GUILayout.Height(height)); - return GUI.Button(rect, FormatInputPreview(value, editing, maxChars), style); - } - - private void DrawClippedHint(string text, float height = 13f) - { - GUIStyle style = new GUIStyle(toggleLabelStyle) - { - fontSize = 10, - clipping = TextClipping.Clip, - wordWrap = false, - alignment = TextAnchor.MiddleLeft - }; - - Rect rect = GUILayoutUtility.GetRect(GUIContent.none, style, GUILayout.ExpandWidth(true), GUILayout.Height(height)); - GUI.Label(rect, text, style); - } - - private void OpenExternalLink(string url, string label) - { - try - { - Application.OpenURL(url); - ShowNotification($"[LINK] {L("Opening", "Открываю")} {label}"); - } - catch - { - ShowNotification($"[LINK] {L("Failed to open link.", "Не удалось открыть ссылку.")}"); - } - } - - private void DrawGeneralInfoTab() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("ELYSIUM OVERVIEW", headerStyle); - GUILayout.Space(6); - - GUILayout.BeginHorizontal(); - for (int i = 0; i < generalInfoSubTabs.Length; i++) - { - if (GUILayout.Button(generalInfoSubTabs[i], currentGeneralInfoSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Width(108), GUILayout.Height(24))) - { - currentGeneralInfoSubTab = i; - } - } - GUILayout.EndHorizontal(); - GUILayout.Space(10); - - GUILayout.BeginHorizontal(); - GUILayout.Label(L("Menu language:", "Язык меню:"), toggleLabelStyle, GUILayout.Width(110)); - if (GUILayout.Button("<", btnStyle, GUILayout.Width(26), GUILayout.Height(24))) - { - currentMenuLanguageIndex--; - if (currentMenuLanguageIndex < 0) currentMenuLanguageIndex = menuLanguageNames.Length - 1; - SaveConfig(); - } - GUILayout.Label(menuLanguageNames[Mathf.Clamp(currentMenuLanguageIndex, 0, menuLanguageNames.Length - 1)], new GUIStyle(btnStyle) { normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, fontStyle = FontStyle.Bold }, GUILayout.Width(132), GUILayout.Height(24)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(26), GUILayout.Height(24))) - { - currentMenuLanguageIndex++; - if (currentMenuLanguageIndex >= menuLanguageNames.Length) currentMenuLanguageIndex = 0; - SaveConfig(); - } - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.Space(8); - - string accentHex = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); - string githubHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(26, 188, 156, 255)) : new Color32(26, 188, 156, 255)); - string goldHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(255, 187, 54, 255)) : new Color32(255, 187, 54, 255)); - string leadHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(255, 92, 122, 255)) : new Color32(255, 92, 122, 255)); - string devHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(38, 194, 129, 255)) : new Color32(38, 194, 129, 255)); - string contributorHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(109, 138, 255, 255)) : new Color32(109, 138, 255, 255)); - string dangerHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(231, 76, 60, 255)) : new Color32(231, 76, 60, 255)); - string safeHex = ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(57, 255, 20, 255)) : new Color32(57, 255, 20, 255)); - string versionText = "1.3.4"; - - GUIStyle textStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 12 }; - textStyle.normal.textColor = whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : new Color(0.85f, 0.85f, 0.85f, 1f); - - if (currentGeneralInfoSubTab == 0) - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label( - $"{L("Welcome to", "Добро пожаловать в")} ElysiumModMenu " + - $"v{versionText} {L("by", "от")} meowchelo!", - textStyle); - GUILayout.Space(4); - GUILayout.Label(L( - "ElysiumModMenu is a lightweight BepInEx IL2CPP utility for Among Us with lobby tools, visuals, spoofing and host-side controls.", - "ElysiumModMenu это легкий BepInEx IL2CPP мод для Among Us с инструментами для лобби, визуалом, спуфингом и хост-функциями."), textStyle); - GUILayout.Label(L( - "Use the buttons below to open the GitHub repository or jump straight to the latest public release.", - "Кнопки ниже открывают GitHub репозиторий и страницу с последним публичным релизом."), textStyle); - GUILayout.Space(6); - - GUILayout.BeginHorizontal(); - if (DrawColoredActionButton("GitHub", new Color32(26, 188, 156, 255), 110f)) - OpenExternalLink("https://github.com/meowchelo/ElysiumModMenu", "GitHub"); - GUILayout.Space(6); - if (DrawColoredActionButton("Check for Updates", new Color32(255, 187, 54, 255), 165f)) - OpenExternalLink("https://github.com/meowchelo/ElysiumModMenu/releases/latest", "Latest Release"); - GUILayout.EndHorizontal(); - - GUILayout.Space(8); - GUILayout.Label($"{L("Project", "Проект")}: meowchelo/ElysiumModMenu", textStyle); - GUILayout.Label($"{L("Main page", "Главная ссылка")}: https://github.com/meowchelo/ElysiumModMenu", textStyle); - GUILayout.Space(8); - GUILayout.Label($"{L("ElysiumModMenu is free and open-source software.", "ElysiumModMenu это бесплатный open-source проект.")}", textStyle); - GUILayout.Label($"{L("If you paid for this menu, demand a refund immediately.", "Если вы заплатили за это меню, требуйте возврат денег сразу.")}", textStyle); - GUILayout.Label($"{L("Make sure you are using the latest version from GitHub releases.", "Убедитесь, что используете последнюю версию из GitHub releases.")}", textStyle); - GUILayout.Space(8); - GUILayout.Label($"{L("Quick Hotkeys", "Быстрые клавиши")}", textStyle); - string menuKeyText = (menuToggleKey == KeyCode.None ? KeyCode.Insert : menuToggleKey).ToString(); - GUILayout.Label($"{L("Menu key", "Кнопка меню")}: {menuKeyText}", textStyle); - GUILayout.Label(L("Right Click: teleport to cursor", "ПКМ: телепорт к курсору"), textStyle); - GUILayout.Label(L("F9: magnet cursor", "F9: магнит курсора"), textStyle); - GUILayout.EndVertical(); - } - else - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label(L( - "ElysiumModMenu is an open-source project. Meet the people behind this build:", - "ElysiumModMenu это open-source проект. Ниже люди, которые стоят за этой сборкой:"), textStyle); - GUILayout.Space(8); - - GUILayout.Label($"LEAD DEVELOPER", textStyle); - GUILayout.Space(4); - if (DrawColoredActionButton("meowchelo", new Color32(255, 92, 122, 255), 150f)) - OpenExternalLink("https://github.com/meowchelo", "meowchelo"); - - GUILayout.Space(10); - GUILayout.Label($"DEVELOPERS", textStyle); - GUILayout.Space(4); - GUILayout.BeginHorizontal(); - if (DrawColoredActionButton("Carrot", new Color32(38, 194, 129, 255), 150f)) - OpenExternalLink("https://github.com/abobanamne", "Carrot"); - GUILayout.Space(6); - if (DrawColoredActionButton("wextikit", new Color32(109, 138, 255, 255), 150f)) - OpenExternalLink("https://github.com/wextikit", "wextikit"); - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - GUILayout.Label($"TESTERS", textStyle); - GUILayout.Space(4); - DrawColoredActionButton("Жена", new Color32(109, 138, 255, 255), 150f); - - GUILayout.Space(10); - GUILayout.Label($"{L("Repository", "Репозиторий")}", textStyle); - GUILayout.Label(L( - "The public source, releases and project updates are published on GitHub.", - "Публичный исходный код, релизы и обновления проекта публикуются на GitHub."), textStyle); - GUILayout.Space(4); - if (DrawColoredActionButton("Open ElysiumModMenu Repository", new Color32(26, 188, 156, 255), 220f)) - OpenExternalLink("https://github.com/meowchelo/ElysiumModMenu", "ElysiumModMenu Repository"); - - GUILayout.Space(10); - GUILayout.Label($"{L("Notes", "Примечание")}", textStyle); - GUILayout.Label(L( - "Thank you to everyone helping with ideas, testing and polishing the menu.", - "Спасибо всем, кто помогает идеями, тестами и полировкой меню."), textStyle); - GUILayout.EndVertical(); - } - - GUILayout.FlexibleSpace(); - GUILayout.EndVertical(); - } - [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] - public static class ChatLogger_Patch - { - public static void Prefix(PlayerControl sourcePlayer, ref string chatText) - { - if (!ElysiumModMenuGUI.enableChatLog || string.IsNullOrWhiteSpace(chatText)) return; - - try - { - string time = System.DateTime.Now.ToString("HH:mm:ss"); - - string name = "System/Unknown"; - string levelStr = "?"; - string fc = "Hidden"; - string puid = "Unknown"; - string platformStr = "Unknown"; - - if (sourcePlayer != null && sourcePlayer.Data != null) - { - name = sourcePlayer.Data.PlayerName; - - uint rawLevel = sourcePlayer.Data.PlayerLevel; - if (rawLevel != uint.MaxValue && rawLevel < 10000) levelStr = (rawLevel + 1).ToString(); - - fc = GetDisplayedFriendCode(sourcePlayer.Data, "Hidden"); - - var client = AmongUsClient.Instance?.GetClientFromCharacter(sourcePlayer); - if (client != null) - { - puid = GetClientPuid(client); - platformStr = ElysiumModMenuGUI.GetPlatform(client); - } - } - - string cleanText = System.Text.RegularExpressions.Regex.Replace(chatText, "<.*?>", string.Empty); - - string logLine = $"[{time}] [{name}] [Lv:{levelStr}] [FC:{fc}] [ID:{puid}] [{platformStr}] : {cleanText}\n"; - - string chatLogPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ChatLog.txt"); - System.IO.File.AppendAllText(chatLogPath, logLine); - } - catch { } - } - } - - - private void DrawSelfTab() - { - if (currentSelfSubTab == 0) currentSelfSubTab = 1; - - float selfColumnWidth = Mathf.Max(270f, (windowRect.width - 170f) * 0.5f); - - GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(GUILayout.Width(selfColumnWidth)); - DrawSelfSpoof(); - GUILayout.EndVertical(); - - GUILayout.Space(8); - - GUILayout.BeginVertical(boxStyle, GUILayout.Width(selfColumnWidth), GUILayout.ExpandHeight(false)); - GUILayout.Label("SELF TOOLS", headerStyle); - GUILayout.Space(4); - - GUILayout.BeginHorizontal(); - for (int i = 0; i < selfOtherTabs.Length; i++) - { - int tabIndex = i + 1; - if (GUILayout.Button(selfOtherTabs[i], currentSelfSubTab == tabIndex ? activeSubTabStyle : subTabStyle, GUILayout.Height(22))) - { - currentSelfSubTab = tabIndex; - scrollPosition = Vector2.zero; - } - } - GUILayout.EndHorizontal(); - GUILayout.Space(8); - - if (currentSelfSubTab == 1) DrawPlayerMovementCompact(); - else if (currentSelfSubTab == 2) DrawRolesCompact(); - else if (currentSelfSubTab == 3) DrawChatSettingsCompact(); - - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - } - - private void DrawPlayerMovementCompact() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("MOVEMENT & TELEPORT", headerStyle); - - GUILayout.BeginHorizontal(); - GUILayout.Label($"Engine: {Mathf.Round(engineSpeed)}x", toggleLabelStyle, GUILayout.Width(86)); - engineSpeed = GUILayout.HorizontalSlider(engineSpeed, 1f, 555f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); - if (GUILayout.Button("R", btnStyle, GUILayout.Width(28), GUILayout.Height(22))) engineSpeed = 1f; - GUILayout.EndHorizontal(); - - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - GUILayout.Label($"Walk: {Mathf.Round(walkSpeed)}x", toggleLabelStyle, GUILayout.Width(86)); - walkSpeed = GUILayout.HorizontalSlider(walkSpeed, 1f, 30f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); - if (GUILayout.Button("R", btnStyle, GUILayout.Width(28), GUILayout.Height(22))) walkSpeed = 1f; - GUILayout.EndHorizontal(); - - GUILayout.Space(8); - tpToCursor = DrawToggle(tpToCursor, "TP To Cursor", 230); - GUILayout.Space(3); - dragToCursor = DrawToggle(dragToCursor, "Drag To Cursor", 230); - GUILayout.Space(3); - autoFollowCursor = DrawToggle(autoFollowCursor, $"Magnet Cursor ({bindMagnetCursor})", 230); - GUILayout.Space(3); - noClip = DrawToggle(noClip, "True NoClip", 230); - - GUILayout.EndVertical(); - } - - private void DrawRolesCompact() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("ROLE TOOLS", headerStyle); - - GUIStyle roleMidStyle = new GUIStyle(btnStyle) - { - fontStyle = FontStyle.Bold, - normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, - alignment = TextAnchor.MiddleCenter - }; - - GUILayout.BeginHorizontal(); - if (GUILayout.Button("<", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) - { - fakeRoleIdx--; - if (fakeRoleIdx < 0) fakeRoleIdx = forceRoleOptions.Length - 1; - } - GUILayout.Label(forceRoleOptions[fakeRoleIdx].ToString(), roleMidStyle, GUILayout.ExpandWidth(true), GUILayout.Height(24)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) - { - fakeRoleIdx++; - if (fakeRoleIdx >= forceRoleOptions.Length) fakeRoleIdx = 0; - } - if (GUILayout.Button("Set", activeTabStyle, GUILayout.Width(42), GUILayout.Height(24))) - RoleManager.Instance?.SetRole(PlayerControl.LocalPlayer, forceRoleOptions[fakeRoleIdx]); - GUILayout.EndHorizontal(); - - GUILayout.Space(8); - GUILayout.Label("IMPOSTOR", headerStyle); - killReach = DrawToggle(killReach, "Kill Reach", 230); - GUILayout.Space(3); - killAnyone = DrawToggle(killAnyone, "Kill Anyone", 230); - GUILayout.Space(3); - killAuraHostOnly = DrawToggle(killAuraHostOnly, "Kill Aura", 230); - GUILayout.Space(3); - noKillCooldownHostOnly = DrawToggle(noKillCooldownHostOnly, "Kill Cooldown 0", 230); - GUILayout.Space(3); - spamReportBodies = DrawToggle(spamReportBodies, "Spam Report Bodies", 230); - - GUILayout.Space(8); - GUILayout.Label("SPECIAL ROLES", headerStyle); - NoShapeshiftAnim = DrawToggle(NoShapeshiftAnim, "No Ss Animation", 230); - GUILayout.Space(3); - endlessSsDuration = DrawToggle(endlessSsDuration, "Endless Ss Duration", 230); - GUILayout.Space(3); - EndlessTracking = DrawToggle(EndlessTracking, "Endless Tracking", 230); - GUILayout.Space(3); - NoTrackingCooldown = DrawToggle(NoTrackingCooldown, "No Track Cooldown", 230); - GUILayout.Space(3); - endlessVentTime = DrawToggle(endlessVentTime, "Endless Vent Time", 230); - GUILayout.Space(3); - noVentCooldown = DrawToggle(noVentCooldown, "No Vent Cooldown", 230); - GUILayout.Space(3); - noMapCooldowns = DrawToggle(noMapCooldowns, "No Map Cooldowns", 230); - GUILayout.Space(3); - endlessBattery = DrawToggle(endlessBattery, "Endless Battery", 230); - GUILayout.Space(3); - noVitalsCooldown = DrawToggle(noVitalsCooldown, "No Vitals Cooldown", 230); - GUILayout.Space(3); - UnlimitedInterrogateRange = DrawToggle(UnlimitedInterrogateRange, "Interrogate Reach", 230); - - GUILayout.EndVertical(); - } - - private void DrawChatSettingsCompact() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label(L("CHAT SETTINGS", "НАСТРОЙКИ ЧАТА"), headerStyle); - - alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 230); - GUILayout.Space(3); - readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 230); - GUILayout.Space(4); - DrawGhostChatColorControl(230f); - GUILayout.Space(3); - enableExtendedChat = DrawToggle(enableExtendedChat, L("Extended Chat", "Длинный чат"), 230); - GUILayout.Space(3); - enableFastChat = DrawToggle(enableFastChat, L("Fast Chat", "Быстрый чат"), 230); - GUILayout.Space(3); - allowLinksAndSymbols = DrawToggle(allowLinksAndSymbols, L("Unlock Extra Characters", "Все символы"), 230); - GUILayout.Space(3); - enableSpellCheck = DrawToggle(enableSpellCheck, L("Spell Check", "Проверка орфографии"), 230); - - GUILayout.Space(8); - GUILayout.Label(L("CHAT UTILITY", "УТИЛИТЫ ЧАТА"), headerStyle); - enableChatHistory = DrawToggle(enableChatHistory, L("Chat History", "История чата"), 230); - GUILayout.Space(3); - GUILayout.BeginHorizontal(); - GUILayout.Label($"{L("History:", "История:")} {chatHistoryLimit}", toggleLabelStyle, GUILayout.Width(92)); - chatHistoryLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(chatHistoryLimit, 5f, 80f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)), 5, 80); - TrimChatHistoryToLimit(); - GUILayout.EndHorizontal(); - GUILayout.Space(3); - enableClipboard = DrawToggle(enableClipboard, L("Clipboard", "Буфер обмена"), 230); - GUILayout.Space(3); - enableChatLog = DrawToggle(enableChatLog, L("Save Chat Log", "Сохранять лог чата"), 230); - GUILayout.Space(3); - enableChatDarkMode = DrawToggle(enableChatDarkMode, L("Dark Chat Theme", "Темная тема чата"), 230); - GUILayout.Space(3); - if (enableChatDarkMode && GUILayout.Button(L("Turn Off Dark Chat", "Выключить темный чат"), btnStyle, GUILayout.Height(24))) - { - enableChatDarkMode = false; - SaveConfig(); - } - GUILayout.Space(3); - enableColorCommand = DrawToggle(enableColorCommand, L("Enable /color", "Разрешить /color"), 230); - GUILayout.Space(3); - blockFortegreenChat = DrawToggle(blockFortegreenChat, L("Block Fortegreen", "Блок Fortegreen"), 230); - GUILayout.Space(3); - blockRainbowChat = DrawToggle(blockRainbowChat, L("Block Rainbow", "Блок Rainbow"), 230); - - GUILayout.Space(8); - GUILayout.Label(L("CHAT SENDER", "ОТПРАВКА ЧАТА"), headerStyle); - GUILayout.Space(4); - - GUIStyle fieldStyle = new GUIStyle(GUI.skin.textField) - { - fontSize = 12, - alignment = TextAnchor.MiddleLeft, - clipping = TextClipping.Clip - }; - fieldStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); - - Rect chatInputRect = GUILayoutUtility.GetRect(10f, 32f, GUILayout.ExpandWidth(true), GUILayout.Height(32)); - GUI.Box(chatInputRect, string.Empty, fieldStyle); - - string drawText = string.IsNullOrEmpty(customChatMessage) - ? L("Type a message...", "Введите сообщение...") - : customChatMessage; - if (customChatInputFocused && (Time.unscaledTime % 1f) < 0.5f) drawText += "|"; - - GUIStyle chatInputTextStyle = new GUIStyle(GUI.skin.label) - { - alignment = TextAnchor.MiddleLeft, - clipping = TextClipping.Clip, - richText = false, - fontSize = 12 - }; - chatInputTextStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); - GUI.Label(new Rect(chatInputRect.x + 9f, chatInputRect.y + 3f, chatInputRect.width - 18f, chatInputRect.height - 6f), drawText, chatInputTextStyle); - - Event e = Event.current; - if (e != null) - { - if (e.type == EventType.MouseDown) - { - customChatInputFocused = chatInputRect.Contains(e.mousePosition); - if (customChatInputFocused) e.Use(); - } - else if (customChatInputFocused && e.type == EventType.KeyDown) - { - if (HandleClipboardShortcut(e, ref customChatMessage, 120)) - { - } - else if (e.keyCode == KeyCode.Backspace) - { - if (!string.IsNullOrEmpty(customChatMessage)) - customChatMessage = customChatMessage.Substring(0, customChatMessage.Length - 1); - e.Use(); - } - else if (e.keyCode == KeyCode.Escape) - { - customChatInputFocused = false; - e.Use(); - } - else if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) - { - TrySendCustomChatMessage(customChatMessage); - e.Use(); - } - else if (!char.IsControl(e.character)) - { - if (customChatMessage == null) customChatMessage = string.Empty; - if (customChatMessage.Length < 120) customChatMessage += e.character; - e.Use(); - } - } - } - - GUILayout.Space(6); - GUILayout.BeginHorizontal(); - if (GUILayout.Button(L("Send", "Отправить"), btnStyle, GUILayout.Height(28))) - TrySendCustomChatMessage(customChatMessage); - GUILayout.Space(6); - string spamBtnText = customChatSpamEnabled ? L("Spam ON", "Спам ВКЛ") : L("Spam OFF", "Спам ВЫКЛ"); - if (GUILayout.Button(spamBtnText, customChatSpamEnabled ? activeTabStyle : btnStyle, GUILayout.Height(28))) - customChatSpamEnabled = !customChatSpamEnabled; - GUILayout.EndHorizontal(); - - GUILayout.Space(6); - GUILayout.BeginHorizontal(); - GUILayout.Label($"{L("Delay:", "Задержка:")} {Mathf.Round(customChatSpamDelay * 10f) / 10f}s", toggleLabelStyle, GUILayout.Width(92)); - customChatSpamDelay = GUILayout.HorizontalSlider(customChatSpamDelay, 0.5f, 10f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); - GUILayout.EndHorizontal(); - - GUILayout.EndVertical(); - } - - private void DrawGhostChatColorControl(float width) - { - GUILayout.BeginHorizontal(GUILayout.Width(width)); - GUILayout.Label(L("Ghost Chat:", "Ghost Chat:"), new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(74)); - if (DrawPseudoInputButton(ghostChatColorHex, isEditingGhostChatColor, 24f, 16)) - { - isEditingGhostChatColor = !isEditingGhostChatColor; - if (isEditingGhostChatColor) - { - ghostChatColorHex = FilterHexInput(ghostChatColorHex, 7); - } - isEditingName = false; - isEditingLevel = false; - isEditingFriendCode = false; - isEditingLocalFriendCode = false; - isEditingBan = false; - ResetAllBindWaits(); - } - if (GUILayout.Button(L("Apply", "OK"), btnStyle, GUILayout.Width(48), GUILayout.Height(24))) - { - isEditingGhostChatColor = false; - ghostChatColorHex = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); - SaveConfig(); - } - GUILayout.EndHorizontal(); - - string previewHex = GetGhostChatColorHex(); - GUILayout.Label($"{L("Preview ghost chat color", "Пример цвета чата призраков")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = false, clipping = TextClipping.Clip }, GUILayout.Width(width), GUILayout.Height(16f)); - } - - private void DrawPlayerMovement() - { - GUILayout.BeginVertical(boxStyle); - try - { - GUILayout.Label("MOVEMENT & TELEPORT", headerStyle); - - GUILayout.BeginHorizontal(); - try - { - GUILayout.Label($"Engine Speed: {Mathf.Round(engineSpeed)}x", GUILayout.Width(130)); - engineSpeed = GUILayout.HorizontalSlider(engineSpeed, 1f, 555f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); - GUILayout.Space(10); - if (GUILayout.Button("Reset", btnStyle, GUILayout.Width(50), GUILayout.Height(20))) engineSpeed = 1f; - } - finally { GUILayout.EndHorizontal(); } - - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - try - { - GUILayout.Label($"Walk Speed: {Mathf.Round(walkSpeed)}x", GUILayout.Width(130)); - walkSpeed = GUILayout.HorizontalSlider(walkSpeed, 1f, 30f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); - GUILayout.Space(10); - if (GUILayout.Button("Reset", btnStyle, GUILayout.Width(50), GUILayout.Height(20))) walkSpeed = 1f; - } - finally { GUILayout.EndHorizontal(); } - - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - try - { - tpToCursor = DrawToggle(tpToCursor, "TP To Cursor", 160); - dragToCursor = DrawToggle(dragToCursor, "Drag To Cursor", 160); - GUILayout.FlexibleSpace(); - } - finally { GUILayout.EndHorizontal(); } - - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - try - { - autoFollowCursor = DrawToggle(autoFollowCursor, $"Magnet Cursor ({bindMagnetCursor})", 160); - noClip = DrawToggle(noClip, "True NoClip", 160); - GUILayout.FlexibleSpace(); - } - finally { GUILayout.EndHorizontal(); } - } - finally { GUILayout.EndVertical(); } - } - private void SmartEndGame(string outcome) - { - if (GameManager.Instance == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; - - bool isHns = GameManager.Instance.IsHideAndSeek(); - int reasonCode = 0; - - switch (outcome) - { - case "CrewWin": reasonCode = isHns ? 7 : 0; break; - case "ImpWin": reasonCode = isHns ? 8 : 3; break; - case "ImpDisconnect": - case "HnsImpDisconnect": reasonCode = 5; break; - } - - bool tempBlock = neverEndGame; - neverEndGame = false; - GameManager.Instance.RpcEndGame((GameOverReason)reasonCode, false); - neverEndGame = tempBlock; - } - - private static string SanitizeSpoofFriendCode(string input) - { - if (string.IsNullOrWhiteSpace(input)) return ""; - - string clean = ""; - foreach (char c in input.ToLowerInvariant()) - { - if (char.IsWhiteSpace(c)) break; - if (char.IsLetterOrDigit(c)) clean += c; - if (clean.Length >= 10) break; - } - return clean; - } - - private static string SanitizeHexColor(string input, string fallback) - { - string value = (input ?? string.Empty).Trim(); - if (value.StartsWith("#")) value = value.Substring(1); - - string clean = ""; - foreach (char c in value) - { - if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) - { - clean += char.ToUpperInvariant(c); - if (clean.Length >= 6) break; - } - } - - return clean.Length == 6 ? "#" + clean : fallback; - } - - private static string FilterHexInput(string input, int maxChars) - { - string value = (input ?? string.Empty).Trim(); - string clean = ""; - bool hasHash = false; - - foreach (char c in value) - { - if (c == '#' && clean.Length == 0 && !hasHash) - { - hasHash = true; - clean = "#"; - continue; - } - - if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) - { - if (clean.Length == 0) clean = "#"; - clean += char.ToUpperInvariant(c); - if (clean.Length >= maxChars) break; - } - } - - return clean.Length == 0 ? "#" : clean; - } - - public static string GetGhostChatColorHex() - { - if (isEditingGhostChatColor) - { - return SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); - } - - ghostChatColorHex = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); - return ghostChatColorHex; - } - - private static string BuildLocalNameRenderText(string input) - { - string value = (input ?? string.Empty).Replace("\r\n", "\n").Replace('\r', '\n'); - if (string.IsNullOrWhiteSpace(value)) return string.Empty; - - string trimmed = value.TrimStart(); - if (trimmed.StartsWith("shimmer:", StringComparison.OrdinalIgnoreCase)) - return ApplyMenuShimmer(trimmed.Substring("shimmer:".Length).TrimStart()); - - Match hexPrefix = Regex.Match(trimmed, @"^#([0-9A-Fa-f]{6})(.*)$"); - if (hexPrefix.Success) - { - string payload = hexPrefix.Groups[2].Value.TrimStart(' ', ':', '|', '-', '>'); - if (!string.IsNullOrEmpty(payload)) - return $"{payload}"; - } - - return value; - } - - private static string GetDisplayedFriendCode(NetworkedPlayerInfo data, string emptyValue = "Hidden") - { - if (data == null) return emptyValue; - - string value = data.FriendCode; - if (enableLocalFriendCodeSpoof && - PlayerControl.LocalPlayer != null && - data.PlayerId == PlayerControl.LocalPlayer.PlayerId && - !string.IsNullOrEmpty(localFriendCodeInput)) - { - value = localFriendCodeInput; - } - - return string.IsNullOrEmpty(value) ? emptyValue : value; - } - - public static bool PrepareLocalFriendCodeForSerialize(NetworkedPlayerInfo data, out string restoreValue) - { - restoreValue = null; - try - { - if (!enableLocalFriendCodeSpoof || enableFriendCodeSpoof) return false; - if (data == null || PlayerControl.LocalPlayer == null || data.PlayerId != PlayerControl.LocalPlayer.PlayerId) return false; - - restoreValue = data.FriendCode; - TrySetStringMember(data, "FriendCode", originalLocalFriendCode ?? string.Empty); - return true; - } - catch - { - restoreValue = null; - return false; - } - } - - public static void RestoreLocalFriendCodeAfterSerialize(NetworkedPlayerInfo data, string restoreValue) - { - try - { - if (data == null || restoreValue == null) return; - TrySetStringMember(data, "FriendCode", restoreValue); - } - catch { } - } - - private static string FormatInputPreview(string value, bool editing, int maxChars = 52) - { - string preview = value ?? string.Empty; - if (preview.Length > maxChars) - preview = "..." + preview.Substring(preview.Length - (maxChars - 3)); - - if (editing) preview += "_"; - return string.IsNullOrEmpty(preview) ? " " : preview; - } - - private static bool HandleClipboardShortcut(Event e, ref string target, int maxLength = -1) - { - if (e == null || e.type != EventType.KeyDown) return false; - - bool ctrlOrCmd = e.control || e.command; - bool pasteAlt = e.shift && e.keyCode == KeyCode.Insert; - if (!ctrlOrCmd && !pasteAlt) return false; - - target ??= string.Empty; - - if (ctrlOrCmd && e.keyCode == KeyCode.C) - { - GUIUtility.systemCopyBuffer = target; - e.Use(); - return true; - } - - if (ctrlOrCmd && e.keyCode == KeyCode.X) - { - GUIUtility.systemCopyBuffer = target; - target = string.Empty; - e.Use(); - return true; - } - - if ((ctrlOrCmd && e.keyCode == KeyCode.V) || pasteAlt) - { - string paste = (GUIUtility.systemCopyBuffer ?? string.Empty).Replace("\r\n", "\n").Replace('\r', '\n'); - if (paste.Length > 0) - { - target += paste; - if (maxLength >= 0 && target.Length > maxLength) - target = target.Substring(0, maxLength); - } - e.Use(); - return true; - } - - return false; - } - - private static bool IsBrokenFriendCode(string friendCode) - { - if (string.IsNullOrWhiteSpace(friendCode)) return true; - if (friendCode.Contains(" ")) return true; - if (friendCode.Contains("<") || friendCode.Contains(">")) return true; - if (!friendCode.Contains("#")) return true; - - string[] parts = friendCode.Split('#'); - if (parts.Length != 2) return true; - if (string.IsNullOrWhiteSpace(parts[0]) || string.IsNullOrWhiteSpace(parts[1])) return true; - if (parts[0].Length < 3 || parts[0].Length > 16) return true; - if (parts[1].Length < 3 || parts[1].Length > 8) return true; - if (!parts[0].All(char.IsLetterOrDigit)) return true; - if (!parts[1].All(char.IsDigit)) return true; - - return false; - } - - private void TryAutoBanBrokenFriendCodeTick() - { - try - { - if (!autoBanBrokenFriendCode) - { - brokenFcScanTimer = 0f; - brokenFcPunishedOwners.Clear(); - return; - } - - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) - { - brokenFcScanTimer = 0f; - return; - } - - if (PlayerControl.AllPlayerControls.Count <= 1) - brokenFcPunishedOwners.Clear(); - - brokenFcScanTimer += Time.deltaTime; - if (brokenFcScanTimer < 0.8f) return; - brokenFcScanTimer = 0f; - - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; - - string fc = pc.Data.FriendCode ?? ""; - if (!IsBrokenFriendCode(fc)) continue; - - int owner = (int)pc.OwnerId; - if (brokenFcPunishedOwners.Contains(owner)) continue; - brokenFcPunishedOwners.Add(owner); - - string name = string.IsNullOrWhiteSpace(pc.Data.PlayerName) ? "Unknown" : pc.Data.PlayerName; - string puid = "Unknown"; - try - { - var client = AmongUsClient.Instance.GetClientFromCharacter(pc); - if (client != null) puid = GetClientPuid(client); - } - catch { } - - AddToBanList(string.IsNullOrWhiteSpace(fc) ? "Unknown" : fc, puid, name, "Broken FriendCode"); - AmongUsClient.Instance.KickPlayer(owner, true); - ShowNotification($"[ANTICHEAT] {name} banned: broken FC"); - } - } - catch { } - } - - private static void TryAutoGhostAfterStartTick() - { - try - { - bool gameStarted = AmongUsClient.Instance != null && AmongUsClient.Instance.IsGameStarted; - if (!gameStarted) - { - wasGameStartedForAutoGhost = false; - autoGhostAppliedThisGame = false; - return; - } - - if (!wasGameStartedForAutoGhost) - { - wasGameStartedForAutoGhost = true; - autoGhostAppliedThisGame = false; - } - - if (!autoGhostAfterStart || autoGhostAppliedThisGame || PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) - return; - - if (PlayerControl.LocalPlayer.Data.IsDead) - { - autoGhostAppliedThisGame = true; - return; - } - - MakePlayerGhost(PlayerControl.LocalPlayer, false, false); - autoGhostAppliedThisGame = true; - ShowNotification($"[AUTO HOST] {L("Auto ghost applied.", "Авто-призрак применен.")}"); - } - catch { } - } - - private static void EnsurePlatformBanListLoaded() - { - try - { - if (string.IsNullOrEmpty(platformBanListPath)) - platformBanListPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ElysiumPlatformBanList.txt"); - - if (!System.IO.File.Exists(platformBanListPath)) - System.IO.File.WriteAllText(platformBanListPath, "# One custom platform token per line. Matching PlatformName values are host-banned when enabled.\n# Example: github\n"); - - if (Time.unscaledTime < platformBanListNextLoadAt) return; - platformBanListNextLoadAt = Time.unscaledTime + 3f; - - customPlatformBanTokens.Clear(); - foreach (string rawLine in System.IO.File.ReadAllLines(platformBanListPath)) - { - string line = rawLine.Trim(); - if (line.Length == 0 || line.StartsWith("#")) continue; - customPlatformBanTokens.Add(line); - } - } - catch { } - } - - private static bool IsCustomPlatformName(ClientData client, out string platformName) - { - platformName = ""; - try - { - if (client == null || client.PlatformData == null) return false; - platformName = client.PlatformData.PlatformName ?? ""; - if (string.IsNullOrWhiteSpace(platformName)) return false; - - string enumName = client.PlatformData.Platform.ToString(); - if (platformName.Equals("TESTNAME", StringComparison.OrdinalIgnoreCase)) return false; - return !platformName.Equals(enumName, StringComparison.OrdinalIgnoreCase) && - !platformName.Equals(GetPlatform(client), StringComparison.OrdinalIgnoreCase); - } - catch { } - - return false; - } - - private static bool IsInvalidPlatformData(ClientData client, out string reason) - { - reason = ""; - try - { - if (client == null || client.PlatformData == null) return false; - - var platform = client.PlatformData; - string pName = platform.PlatformName ?? ""; - ulong xuid = platform.XboxPlatformId; - ulong psid = platform.PsnPlatformId; - bool isValid = true; - - switch (platform.Platform) - { - case Platforms.StandaloneEpicPC: - case Platforms.StandaloneSteamPC: - case Platforms.StandaloneMac: - case Platforms.StandaloneItch: - case Platforms.IPhone: - case Platforms.Android: - isValid = (pName == "TESTNAME" && xuid == 0 && psid == 0); - break; - case Platforms.StandaloneWin10: - isValid = (pName == "TESTNAME" && xuid != 0 && psid == 0); - break; - case Platforms.Xbox: - isValid = (pName != "TESTNAME" && pName.Length >= 3 && xuid != 0 && psid == 0); - break; - case Platforms.Playstation: - isValid = (pName != "TESTNAME" && xuid == 0 && psid != 0); - break; - case Platforms.Switch: - isValid = (pName != "TESTNAME" && xuid == 0 && psid == 0); - break; - } - - if (!isValid) - { - reason = $"Platform Spoof detected ({platform.Platform})"; - return true; - } - } - catch { } - - return false; - } - - private static bool MatchesPlatformBanTxt(ClientData client, out string platformName, out string matchedToken) - { - platformName = ""; - matchedToken = ""; - EnsurePlatformBanListLoaded(); - - if (!IsCustomPlatformName(client, out platformName) || customPlatformBanTokens.Count == 0) - return false; - - foreach (string token in customPlatformBanTokens) - { - if (platformName.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0) - { - matchedToken = token; - return true; - } - } - - return false; - } - - private static void HostBanForPlatform(PlayerControl player, string reason) - { - try - { - if (player == null || player == PlayerControl.LocalPlayer || player.Data == null || - AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) - return; - - int owner = (int)player.OwnerId; - if (platformSpoofPunishedOwners.Contains(owner)) return; - platformSpoofPunishedOwners.Add(owner); - - string name = string.IsNullOrWhiteSpace(player.Data.PlayerName) ? "Unknown" : player.Data.PlayerName; - string fc = string.IsNullOrWhiteSpace(player.Data.FriendCode) ? "Unknown" : player.Data.FriendCode; - string puid = "Unknown"; - try - { - var client = AmongUsClient.Instance.GetClientFromCharacter(player); - if (client != null) puid = GetClientPuid(client); - } - catch { } - - AddToBanList(fc, puid, name, reason); - AmongUsClient.Instance.KickPlayer(owner, true); - ShowNotification($"[PLATFORM BAN] {name}: {reason}"); - } - catch { } - } - - private static void TryAutoBanCustomPlatformsTick() - { - try - { - if ((!autoBanPlatformSpoof && !banCustomPlatformsFromTxt) || - AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) - { - platformBanScanTimer = 0f; - return; - } - - platformBanScanTimer += Time.deltaTime; - if (platformBanScanTimer < 1f) return; - platformBanScanTimer = 0f; - - foreach (PlayerControl pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; - - ClientData client = null; - try { client = AmongUsClient.Instance.GetClientFromCharacter(pc); } catch { } - if (client == null) continue; - - if (banCustomPlatformsFromTxt && MatchesPlatformBanTxt(client, out string platformName, out string token)) - { - HostBanForPlatform(pc, $"Custom platform TXT match '{token}' ({platformName})"); - continue; - } - - if (autoBanPlatformSpoof && IsInvalidPlatformData(client, out string reason)) - HostBanForPlatform(pc, reason); - } - } - catch { } - } - - private void DrawSelfSpoof() - { - GUILayout.BeginVertical(boxStyle); - GUIStyle greenHeader = new GUIStyle(headerStyle); - greenHeader.normal.textColor = GetThemeAccentColor(currentAccentColor); - GUILayout.Label("ACCOUNT SPOOFER", greenHeader); - - GUILayout.Space(4); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("LEVEL SPOOF", headerStyle); - GUILayout.BeginHorizontal(); - GUILayout.Label("Fake Level", btnStyle, GUILayout.Width(86), GUILayout.Height(28)); - if (DrawPseudoInputButton(spoofLevelString, isEditingLevel, 28f, 32)) - { - isEditingLevel = !isEditingLevel; - isEditingName = false; - isEditingFriendCode = false; - isEditingLocalFriendCode = false; - isEditingGhostChatColor = false; - } - if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) - { - isEditingLevel = false; - if (uint.TryParse(spoofLevelString, out uint parsedLvl)) - { - try { AmongUs.Data.DataManager.Player.stats.level = parsedLvl > 0 ? parsedLvl - 1 : 0; AmongUs.Data.DataManager.Player.Save(); } - catch { try { AmongUs.Data.DataManager.Player.Stats.Level = parsedLvl > 0 ? parsedLvl - 1 : 0; AmongUs.Data.DataManager.Player.Save(); } catch { } } - } - SaveConfig(); - } - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.Space(6); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("LOCAL NAME SPOOF", headerStyle); - bool newLocalNameToggle = DrawToggle(enableLocalNameSpoof, "Keep Local Nick", 180); - if (newLocalNameToggle != enableLocalNameSpoof) - { - enableLocalNameSpoof = newLocalNameToggle; - if (enableLocalNameSpoof) ApplyLocalNameSelf(customNameInput, false); - else RestoreLocalNameSelf(); - SaveConfig(); - } - GUILayout.Space(2); - GUILayout.BeginHorizontal(); - GUILayout.Label("Nick", btnStyle, GUILayout.Width(58), GUILayout.Height(28)); - if (DrawPseudoInputButton(customNameInput, isEditingName, 28f, 54)) - { - isEditingName = !isEditingName; - isEditingLevel = false; - isEditingFriendCode = false; - isEditingLocalFriendCode = false; - isEditingGhostChatColor = false; - } - if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) - { - isEditingName = false; - ApplyLocalNameSelf(customNameInput, true); - SaveConfig(); - } - GUILayout.EndHorizontal(); - DrawClippedHint("Local only: no RPC broadcast. Supports shimmer:Text, #68B6E7Text and raw rich text."); - GUILayout.EndVertical(); - - GUILayout.Space(6); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("LOCAL FAKE FRIEND CODE", headerStyle); - bool newLocalFcToggle = DrawToggle(enableLocalFriendCodeSpoof, "Keep Fake FC Local", 180); - if (newLocalFcToggle != enableLocalFriendCodeSpoof) - { - enableLocalFriendCodeSpoof = newLocalFcToggle; - if (enableLocalFriendCodeSpoof) ApplyLocalFriendCodeSelf(localFriendCodeInput, false); - else RestoreLocalFriendCodeSelf(); - SaveConfig(); - } - GUILayout.Space(2); - GUILayout.BeginHorizontal(); - GUILayout.Label("Fake FC", btnStyle, GUILayout.Width(58), GUILayout.Height(28)); - if (DrawPseudoInputButton(localFriendCodeInput, isEditingLocalFriendCode, 28f, 54)) - { - isEditingLocalFriendCode = !isEditingLocalFriendCode; - isEditingName = false; - isEditingLevel = false; - isEditingFriendCode = false; - isEditingGhostChatColor = false; - } - if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) - { - isEditingLocalFriendCode = false; - ApplyLocalFriendCodeSelf(localFriendCodeInput, true); - SaveConfig(); - } - GUILayout.EndHorizontal(); - DrawClippedHint("Local only: any text, any symbols. Used in this client UI only."); - GUILayout.EndVertical(); - - GUILayout.Space(6); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("FRIEND CODE SPOOF", headerStyle); - enableFriendCodeSpoof = DrawToggle(enableFriendCodeSpoof, "Enable FC Spoof", 180); - GUILayout.Space(2); - GUILayout.BeginHorizontal(); - if (DrawPseudoInputButton(spoofFriendCodeInput, isEditingFriendCode, 28f, 54)) - { - isEditingFriendCode = !isEditingFriendCode; - isEditingName = false; - isEditingLevel = false; - isEditingLocalFriendCode = false; - isEditingGhostChatColor = false; - } - if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) - { - isEditingFriendCode = false; - spoofFriendCodeInput = SanitizeSpoofFriendCode(spoofFriendCodeInput); - SaveConfig(); - } - GUILayout.EndHorizontal(); - DrawClippedHint("Guest-style code: <=10, [a-z0-9], no spaces"); - GUILayout.EndVertical(); - - GUILayout.Space(6); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("PLATFORM SPOOF", headerStyle); - if (GUILayout.Button("Spoof Platform", enablePlatformSpoof ? activeTabStyle : btnStyle, GUILayout.Height(26))) - { - enablePlatformSpoof = !enablePlatformSpoof; - SaveConfig(); - } - GUILayout.Space(2); - string hexColor = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); - GUILayout.Label($"Platform: {platformNames[currentPlatformIndex]}", new GUIStyle(toggleLabelStyle) { fontSize = 12, richText = true }, GUILayout.Height(23)); - int newPlatIdx = (int)GUILayout.HorizontalSlider(currentPlatformIndex, 0, platformNames.Length - 1, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); - if (newPlatIdx != currentPlatformIndex) - { - currentPlatformIndex = newPlatIdx; - SaveConfig(); - } - GUILayout.EndVertical(); - - GUILayout.Space(8); - GUILayout.Label("TASKS", headerStyle); - if (GUILayout.Button("Complete My Tasks", btnStyle, GUILayout.Height(30))) - { - if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.myTasks != null) - foreach (var task in PlayerControl.LocalPlayer.myTasks) - if (task != null && !task.IsComplete) PlayerControl.LocalPlayer.RpcCompleteTask((uint)task.Id); - } - GUILayout.EndVertical(); - } - - - - private void DrawVisualsTab() - { - GUILayout.BeginHorizontal(); - for (int i = 0; i < visualsSubTabs.Length; i++) - if (GUILayout.Button(visualsSubTabs[i], currentVisualsSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) - { currentVisualsSubTab = i; scrollPosition = Vector2.zero; } - GUILayout.EndHorizontal(); - GUILayout.Space(8); - if (currentVisualsSubTab == 0) DrawVisualsInGame(); - } - - - - [HarmonyPatch(typeof(PlayerBanData), nameof(PlayerBanData.BanPoints), MethodType.Setter)] - public static class RemoveDisconnectPenalty_Patch - { - public static bool Prefix(PlayerBanData __instance, ref float value) - { - if (!ElysiumModMenuGUI.removePenalty) return true; - if (AmongUsClient.Instance == null || AmongUsClient.Instance.NetworkMode != NetworkModes.OnlineGame) - return true; - - value = 0f; - return false; - } - } - - [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Start))] - public static class ShowLobbyTimer_Patch - { - public static void Postfix(GameStartManager __instance) - { - if (!ElysiumModMenuGUI.alwaysShowLobbyTimer) return; - - if (__instance == null || GameData.Instance == null || AmongUsClient.Instance == null) return; - if (AmongUsClient.Instance.NetworkMode == NetworkModes.LocalGame || !AmongUsClient.Instance.AmHost) return; - - if (HudManager.Instance != null) - { - HudManager.Instance.ShowLobbyTimer(600); - } - } - } - private void DrawPlayersTab() - { - GUILayout.BeginHorizontal(); - for (int i = 0; i < playersSubTabs.Length; i++) - if (GUILayout.Button(playersSubTabs[i], currentPlayersSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) - { currentPlayersSubTab = i; scrollPosition = Vector2.zero; } - GUILayout.EndHorizontal(); - GUILayout.Space(8); - - if (currentPlayersSubTab == 1) - { - DrawPlayersHistoryTab(); - return; - } - - GUILayout.BeginHorizontal(); - - GUILayout.BeginVertical(boxStyle, GUILayout.Width(200)); - playerListScrollPos = GUILayout.BeginScrollView(playerListScrollPos); - if (lockedPlayersList.Count > 0) - { - foreach (var pc in lockedPlayersList) - { - if (pc == null || pc.Data == null || pc.PlayerId >= 100) continue; - string pName = pc.Data.PlayerName ?? "Unknown"; - - if (forcedPreGameRoles.ContainsKey(pc.PlayerId)) pName += " [*]"; - else if (forcedImpostors.Contains(pc.PlayerId)) pName += " [Imp]"; - - bool isSelected = selectedAntiCheatPlayerId == pc.PlayerId; - - GUI.contentColor = Color.white; - try { GUI.contentColor = Palette.PlayerColors[pc.Data.DefaultOutfit.ColorId]; } catch { } - - if (GUILayout.Button(pName, isSelected ? activeTabStyle : btnStyle, GUILayout.Height(30))) - { - selectedAntiCheatPlayerId = pc.PlayerId; - } - GUI.contentColor = Color.white; - } - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - - GUILayout.BeginVertical(boxStyle, GUILayout.ExpandWidth(true)); - playerActionScrollPos = GUILayout.BeginScrollView(playerActionScrollPos); - - PlayerControl target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedAntiCheatPlayerId); - - if (target != null && target.Data != null) - { - GUILayout.Label($"Selected: {target.Data.PlayerName}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 14 }); - GUILayout.Space(10); - GUILayout.BeginHorizontal(); - - GUI.backgroundColor = new Color(0.8f, 0.2f, 0.2f, 1f); - if (GUILayout.Button("KILL", btnStyle, GUILayout.Height(25))) - { - Vector3 op = PlayerControl.LocalPlayer.transform.position; - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(target.transform.position); - PlayerControl.LocalPlayer.CmdCheckMurder(target); - PlayerControl.LocalPlayer.RpcMurderPlayer(target, true); - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(op); - } - GUI.backgroundColor = Color.white; - - if (GUILayout.Button("TP TO", activeTabStyle, GUILayout.Height(25))) - { - teleportToPlayer(target); - ShowNotification($"[TELEPORT] Телепортирован к {target.Data.PlayerName}!"); - } - - GUI.backgroundColor = new Color(1f, 0.5f, 0f, 1f); - if (GUILayout.Button("Force Eject", btnStyle, GUILayout.Height(25))) ForceGlobalEject(target); - GUI.backgroundColor = Color.white; - - GUILayout.EndHorizontal(); - - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - - if (GUILayout.Button("Force Meeting", btnStyle, GUILayout.Height(25))) ForceMeetingAsPlayer(target); - - bool hr = rainbowPlayers.Contains(target.PlayerId); - if (GUILayout.Button(hr ? "RGB: ON" : "RGB: OFF", hr ? activeTabStyle : btnStyle, GUILayout.Height(25))) - { - if (!hr) rainbowPlayers.Add(target.PlayerId); - else rainbowPlayers.Remove(target.PlayerId); - } - - GUILayout.EndHorizontal(); - - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - - if (GUILayout.Button("Report Body", btnStyle, GUILayout.Height(25))) - AttemptReportBody(target); - - if (GUILayout.Button("Flood Tasks", btnStyle, GUILayout.Height(25))) - FloodPlayerWithTasks(target); - - if (GUILayout.Button("Clear Tasks", btnStyle, GUILayout.Height(25))) - ClearPlayerTasks(target); - - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - GUILayout.Label("TARGET ROLE CONTROL", headerStyle); - - GUILayout.BeginHorizontal(); - GUIStyle roleMidStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, alignment = TextAnchor.MiddleCenter }; - if (GUILayout.Button("<", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) - { - targetRoleAssignIdx--; - if (targetRoleAssignIdx < 0) targetRoleAssignIdx = roleAssignOptions.Length - 1; - } - GUILayout.Label(roleAssignNames[targetRoleAssignIdx], roleMidStyle, GUILayout.Height(24), GUILayout.ExpandWidth(true)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) - { - targetRoleAssignIdx++; - if (targetRoleAssignIdx >= roleAssignOptions.Length) targetRoleAssignIdx = 0; - } - GUILayout.EndHorizontal(); - - GUILayout.Space(4); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("TARGET -> ROLE", btnStyle, GUILayout.Height(26))) - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) - { - ShowNotification("[ОШИБКА] Требуются права хоста!"); - } - else - { - if (IsGhostRoleSelection(targetRoleAssignIdx)) - { - MakePlayerGhost(target); - } - else if (IsGhostImpostorRoleSelection(targetRoleAssignIdx)) - { - MakePlayerGhost(target, true); - } - else - { - SetPlayerRole(target, roleAssignOptions[targetRoleAssignIdx]); - ShowNotification($"[ROLE] {target.Data.PlayerName} -> {roleAssignNames[targetRoleAssignIdx]}"); - } - } - } - if (GUILayout.Button("TARGET -> GHOST", btnStyle, GUILayout.Height(26))) - { - MakePlayerGhost(target); - } - GUILayout.EndHorizontal(); - - GUILayout.Space(4); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("REVIVE TARGET", activeTabStyle, GUILayout.Height(26))) - { - RevivePlayer(target); - } - GUILayout.EndHorizontal(); - - GUILayout.Space(15); - GUILayout.Label("Morph Target:", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11 }); - GUILayout.BeginHorizontal(); - - int mIdx = lockedPlayersList.FindIndex(p => p.PlayerId == selectedMorphTargetId); - - GUI.backgroundColor = currentAccentColor; - if (GUILayout.Button("<", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) - { - if (lockedPlayersList.Count > 0) { mIdx--; if (mIdx < 0) mIdx = lockedPlayersList.Count - 1; selectedMorphTargetId = lockedPlayersList[mIdx].PlayerId; } - } - GUI.backgroundColor = Color.white; - - string morphName = "Target"; - if (mIdx >= 0 && mIdx < lockedPlayersList.Count) morphName = lockedPlayersList[mIdx].Data.PlayerName; - if (morphName.Length > 10) morphName = morphName.Substring(0, 10) + ".."; - - GUIStyle morphLabelStyle = new GUIStyle(btnStyle); - morphLabelStyle.normal.background = null; - morphLabelStyle.hover.background = null; - morphLabelStyle.normal.textColor = GetThemeAccentColor(currentAccentColor); - morphLabelStyle.fontStyle = FontStyle.Bold; - morphLabelStyle.alignment = TextAnchor.MiddleCenter; - - GUILayout.Label(morphName, morphLabelStyle, GUILayout.Height(25), GUILayout.ExpandWidth(true)); - - GUI.backgroundColor = currentAccentColor; - if (GUILayout.Button(">", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) - { - if (lockedPlayersList.Count > 0) { mIdx++; if (mIdx >= lockedPlayersList.Count) mIdx = 0; selectedMorphTargetId = lockedPlayersList[mIdx].PlayerId; } - } - GUILayout.EndHorizontal(); - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - GUILayout.FlexibleSpace(); - - GUI.backgroundColor = currentAccentColor; - if (GUILayout.Button("MORPH TARGET", btnStyle, GUILayout.Width(160), GUILayout.Height(25))) - { - var morphTarget = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedMorphTargetId) ?? target; - this.StartCoroutine(AttemptShapeshiftFrame(target, morphTarget).WrapToIl2Cpp()); - } - GUI.backgroundColor = Color.white; - - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.Space(15); - GUILayout.Label("SET PLAYER COLOR", headerStyle); - GUILayout.BeginVertical(boxStyle); - - GUIStyle roundedColorBtnStyle = new GUIStyle(); - roundedColorBtnStyle.normal.background = texColorBtn; - roundedColorBtnStyle.margin = CreateRectOffset(2, 2, 2, 2); - - int colorsPerRow = 7; - for (int i = 0; i < Palette.PlayerColors.Length; i++) - { - if (i % colorsPerRow == 0) GUILayout.BeginHorizontal(); - - GUI.color = Palette.PlayerColors[i]; - - if (GUILayout.Button("", roundedColorBtnStyle, GUILayout.Width(32), GUILayout.Height(30))) - target.RpcSetColor((byte)i); - - if (i % colorsPerRow == colorsPerRow - 1 || i == Palette.PlayerColors.Length - 1) - GUILayout.EndHorizontal(); - } - GUI.color = Color.white; - GUILayout.EndVertical(); - - GUILayout.Space(15); - GUILayout.Label("PRE-GAME ROLE (HOST)", headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Impostor", btnStyle, GUILayout.Height(25))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Add(target.PlayerId); enablePreGameRoleForce = true; } - if (GUILayout.Button("Crewmate", btnStyle, GUILayout.Height(25))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Crewmate; enablePreGameRoleForce = true; } - if (GUILayout.Button("Shapeshifter", btnStyle, GUILayout.Height(25))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Shapeshifter; enablePreGameRoleForce = true; } - GUILayout.EndHorizontal(); - GUILayout.Space(5); - if (GUILayout.Button("REMOVE FORCED ROLE", activeTabStyle, GUILayout.Height(25))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Remove(target.PlayerId); } - } - else - { - GUILayout.FlexibleSpace(); - GUILayout.Label("Select a player...", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); - GUILayout.FlexibleSpace(); - } - - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - - GUILayout.EndHorizontal(); - } - - private void DrawPlayersHistoryTab() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("PLAYER HISTORY", headerStyle); - - GUILayout.BeginHorizontal(); - GUILayout.Label($"Entries: {playerHistoryEntries.Count}", new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(120)); - GUILayout.Label("File: ElysiumPlayerHistory.txt", new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(190)); - GUILayout.FlexibleSpace(); - if (GUILayout.Button("Clear History", btnStyle, GUILayout.Width(120), GUILayout.Height(24))) - { - playerHistoryEntries.Clear(); - playerHistoryKeysById.Clear(); - WritePlayerHistoryFile(); - } - GUILayout.EndHorizontal(); - - GUILayout.Space(6); - playersHistoryScroll = GUILayout.BeginScrollView(playersHistoryScroll); - if (playerHistoryEntries.Count == 0) - { - GUILayout.Label("История пока пустая.", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); - } - else - { - foreach (var e in playerHistoryEntries.OrderByDescending(x => x.LastSeenUtc)) - { - GUILayout.BeginVertical(boxStyle); - string status = e.IsOnline ? "ONLINE" : "LEFT"; - GUILayout.Label($"{e.Name} {status}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 13 }); - GUILayout.Label($"Lv: {e.Level} | FC: {e.FriendCode} | PUID: {e.Puid}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); - GUILayout.Label($"Joined: {e.FirstSeenUtc:HH:mm:ss} | Left: {(e.LeftUtc.HasValue ? e.LeftUtc.Value.ToString("HH:mm:ss") : "online")}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); - GUILayout.Label($"Platform: {FormatPlatformHistory(e)}", new GUIStyle(GUI.skin.label) { fontSize = 11, wordWrap = true }); - GUILayout.Label($"RPC: {FormatRpcHistory(e)}", new GUIStyle(GUI.skin.label) { fontSize = 11, wordWrap = true }); - GUILayout.EndVertical(); - GUILayout.Space(2); - } - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - } - private void ForceGlobalEject(PlayerControl target) - { - if (target == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) - { - ShowNotification("[ERROR] Нужны права Хоста!"); - return; - } - - try - { - target.Data.IsDead = false; - - if (MeetingHud.Instance == null) - { - MeetingHud.Instance = UnityEngine.Object.Instantiate(DestroyableSingleton.Instance.MeetingPrefab); - AmongUsClient.Instance.Spawn(MeetingHud.Instance.Cast(), -2, SpawnFlags.None); - } - - var emptyStates = new Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray(0); - - MeetingHud.Instance.RpcVotingComplete(emptyStates, target.Data, false); - - MeetingHud.Instance.RpcClose(); - - ShowNotification($"[EJECT] Изгоняем {target.Data.PlayerName}..."); - } - catch (Exception) - { - } - } - - private static bool IsDeadBodyForPlayerPresent(byte playerId) - { - try - { - var allBehaviours = UnityEngine.Object.FindObjectsOfType(); - foreach (var mb in allBehaviours) - { - if (mb == null || mb.gameObject == null) continue; - Type t = mb.GetType(); - if (t == null || t.Name != "DeadBody") continue; - - byte parentId = byte.MaxValue; - var parentProp = t.GetProperty("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (parentProp != null) - { - object val = parentProp.GetValue(mb, null); - if (val is byte b) parentId = b; - else if (val is int i) parentId = (byte)i; - } - else - { - var parentField = t.GetField("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (parentField != null) - { - object val = parentField.GetValue(mb); - if (val is byte b) parentId = b; - else if (val is int i) parentId = (byte)i; - } - } - - if (parentId == playerId) return true; - } - } - catch { } - - return false; - } - - private static void AttemptReportBody(PlayerControl target) - { - if (target == null || target.Data == null || PlayerControl.LocalPlayer == null) return; - - try - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - PlayerControl.LocalPlayer.CmdReportDeadBody(target.Data); - ShowNotification($"[REPORT] Репорт {target.Data.PlayerName}"); - return; - } - - if (LobbyBehaviour.Instance != null) - { - ShowNotification("[REPORT] Игра должна начаться."); - return; - } - - if (!target.Data.IsDead) - { - ShowNotification("[REPORT] Можно репортить только мертвых игроков."); - return; - } - - if (!IsDeadBodyForPlayerPresent(target.PlayerId)) - { - ShowNotification("[REPORT] Труп не найден или уже исчез."); - return; - } - - PlayerControl.LocalPlayer.CmdReportDeadBody(target.Data); - ShowNotification($"[REPORT] Репорт {target.Data.PlayerName}"); - } - catch (Exception) - { - } - } - - private static void FloodPlayerWithTasks(PlayerControl target) - { - if (target == null || target.Data == null) - { - ShowNotification("[TASKS] Цель не найдена."); - return; - } - - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) - { - ShowNotification("[TASKS] Нужны права хоста."); - return; - } - - try - { - byte[] taskIds = new byte[255]; - for (byte i = 0; i < 255; i++) taskIds[i] = i; - target.Data.RpcSetTasks(taskIds); - ShowNotification($"[TASKS] {target.Data.PlayerName} получил flood tasks."); - } - catch (Exception) - { - } - } - - private static void ClearPlayerTasks(PlayerControl target) - { - if (target == null || target.Data == null) - { - ShowNotification("[TASKS] Цель не найдена."); - return; - } - - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) - { - ShowNotification("[TASKS] Нужны права хоста."); - return; - } - - try - { - target.Data.RpcSetTasks(Array.Empty()); - ShowNotification($"[TASKS] Задачи {target.Data.PlayerName} очищены."); - } - catch (Exception) - { - } - } - - private static string GetRoleDisplayName(RoleTypes role) - { - for (int i = 0; i < roleAssignOptions.Length; i++) - if (roleAssignOptions[i].Equals(role)) - return roleAssignNames[i]; - return role.ToString(); - } - - private static bool IsGhostRoleSelection(int roleIndex) - { - return roleIndex >= 0 && - roleIndex < roleAssignNames.Length && - string.Equals(roleAssignNames[roleIndex], "Ghost", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsGhostImpostorRoleSelection(int roleIndex) - { - return roleIndex >= 0 && - roleIndex < roleAssignNames.Length && - string.Equals(roleAssignNames[roleIndex], "Ghost Imp", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsImpostorTeamRole(RoleTypes role) - { - int roleId = (int)role; - return role == RoleTypes.Impostor || role == RoleTypes.Shapeshifter || roleId == 9 || roleId == 18; - } - - public static void RevivePlayer(PlayerControl target) - { - if (target == null || target.Data == null) - { - ShowNotification("[ОШИБКА] Цель не найдена!"); - return; - } - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) - { - ShowNotification("[ОШИБКА] Требуются права хоста!"); - return; - } - if (!target.Data.IsDead) - { - ShowNotification($"{target.Data.PlayerName} уже жив!"); - return; - } - - try - { - target.Data.IsDead = false; - - if (target.Collider != null) target.Collider.enabled = true; - - if (target.MyPhysics != null) - target.MyPhysics.gameObject.layer = LayerMask.NameToLayer("Players"); - - try - { - var allBehaviours = UnityEngine.Object.FindObjectsOfType(); - foreach (var mb in allBehaviours) - { - if (mb == null || mb.gameObject == null) continue; - Type t = mb.GetType(); - if (t == null || t.Name != "DeadBody") continue; - - byte parentId = byte.MaxValue; - - var parentProp = t.GetProperty("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (parentProp != null) - { - object val = parentProp.GetValue(mb, null); - if (val is byte b) parentId = b; - else if (val is int i) parentId = (byte)i; - } - else - { - var parentField = t.GetField("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (parentField != null) - { - object val = parentField.GetValue(mb); - if (val is byte b) parentId = b; - else if (val is int i) parentId = (byte)i; - } - } - - if (parentId == target.PlayerId) - mb.gameObject.SetActive(false); - } - } - catch { } - - bool wasImpTeam = false; - try - { - if (target.Data.Role != null) - { - int roleId = (int)target.Data.Role.Role; - wasImpTeam = roleId == 1 || roleId == 5 || roleId == 7 || roleId == 9 || roleId == 18; - } - else - { - var rt = target.Data.RoleType; - wasImpTeam = rt == RoleTypes.Impostor || rt == RoleTypes.Shapeshifter || (int)rt == 9 || (int)rt == 18; - } - } - catch { } - - target.RpcSetRole(wasImpTeam ? RoleTypes.Impostor : RoleTypes.Crewmate, true); - - var netObj = GameData.Instance?.GetComponent(); - if (netObj != null) netObj.SetDirtyBit(uint.MaxValue); - - ShowNotification($"[ВОСКРЕШЕНИЕ] {target.Data.PlayerName} воскрешён!"); - } - catch (Exception) - { - ShowNotification("Ошибка воскрешения!"); - } - } - - public static void MakePlayerGhost(PlayerControl target, bool impostorGhost = false, bool notify = true) - { - if (target == null || target.Data == null) - { - if (notify) ShowNotification("[ОШИБКА] Цель не найдена!"); - return; - } - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) - { - if (notify) ShowNotification("[ОШИБКА] Требуются права хоста!"); - return; - } - if (target.Data.IsDead) - { - if (!TrySetGhostRole(target, impostorGhost, out _)) - SetPlayerRole(target, impostorGhost ? RoleTypes.Impostor : (IsImpostorTeamRole(target.Data.RoleType) ? RoleTypes.Impostor : RoleTypes.Crewmate)); - if (notify) ShowNotification($"{target.Data.PlayerName} уже призрак!"); - return; - } - - try - { - string methodUsed; - if (!TrySetGhostRole(target, impostorGhost, out methodUsed)) - { - RoleTypes fallbackRole = impostorGhost ? RoleTypes.Impostor : (IsImpostorTeamRole(target.Data.RoleType) ? RoleTypes.Impostor : RoleTypes.Crewmate); - SetPlayerRole(target, fallbackRole); - TryActivateGhostState(target, out methodUsed); - } - - var netObj = GameData.Instance?.GetComponent(); - if (netObj != null) netObj.SetDirtyBit(uint.MaxValue); - - if (notify) ShowNotification($"[GHOST] {target.Data.PlayerName} стал призраком ({methodUsed})!"); - } - catch (Exception) - { - if (notify) ShowNotification("Ошибка перевода в призрака!"); - } - } - - private static bool TrySetGhostRole(PlayerControl target, bool impostorGhost, out string methodUsed) - { - methodUsed = string.Empty; - if (target == null || target.Data == null) return false; - - string[] roleNames = impostorGhost - ? new[] { "ImpostorGhost", "GhostImpostor", "ImpGhost", "Ghost" } - : new[] { "CrewmateGhost", "GhostCrewmate", "CrewGhost", "Ghost" }; - - foreach (string roleName in roleNames) - { - if (!Enum.TryParse(roleName, true, out RoleTypes ghostRole)) continue; - - try { target.RpcSetRole(ghostRole, true); } catch { } - try { RoleManager.Instance?.SetRole(target, ghostRole); } catch { } - - methodUsed = $"SetRole:{roleName}"; - return true; - } - - return false; - } - - private static bool TryActivateGhostState(PlayerControl target, out string methodUsed) - { - methodUsed = string.Empty; - if (target == null) return false; - if (target.Data != null && target.Data.IsDead) - { - methodUsed = "already_dead"; - return true; - } - - if (TryDie(target, DeathReason.Exile, true) || - TryDie(target, DeathReason.Exile, false) || - TryDie(target, DeathReason.Kill, true) || - TryDie(target, DeathReason.Kill, false)) - { - methodUsed = "Die"; - return true; - } - - if (TryInvokeNoArg(target, "Exiled") || - TryInvokeNoArg(target, "RpcExiled") || - TryInvokeNoArg(target, "RpcExiledV2") || - TryInvokeNoArg(target, "SetDead")) - { - methodUsed = "Exiled/SetDead"; - return true; - } - - if (TrySetDeadFlag(target)) - { - methodUsed = "Data.IsDead"; - return true; - } - - methodUsed = "fallback"; - return false; - } - - private static bool TryDie(PlayerControl target, DeathReason reason, bool allowAnimation) - { - try { target.Die(reason, allowAnimation); } - catch { } - return target != null && target.Data != null && target.Data.IsDead; - } - - private static bool TryInvokeNoArg(object target, string methodName) - { - if (target == null || string.IsNullOrWhiteSpace(methodName)) return false; - try - { - for (Type type = target.GetType(); type != null; type = type.BaseType) - { - MethodInfo method = type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); - if (method == null) continue; - method.Invoke(target, null); - return target is PlayerControl player && player.Data != null && player.Data.IsDead; - } - } - catch { } - return false; - } - - private static bool TrySetDeadFlag(PlayerControl target) - { - if (target == null || target.Data == null) return false; - try - { - target.Data.IsDead = true; - if (target.Collider != null) target.Collider.enabled = false; - if (target.MyPhysics != null) target.MyPhysics.gameObject.layer = LayerMask.NameToLayer("Ghost"); - } - catch { } - return target.Data.IsDead; - } - - public static void SetAllPlayersGhost() - { - SetAllPlayersGhost(false); - } - - public static void SetAllPlayersGhost(bool impostorGhost) - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) - { - ShowNotification("[ОШИБКА] Требуются права хоста!"); - return; - } - if (PlayerControl.AllPlayerControls == null) return; - - int count = 0; - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null && pc.Data != null && !pc.Data.Disconnected) - { - MakePlayerGhost(pc, impostorGhost, false); - count++; - } - } - - ShowNotification($"[GHOST] {count} игрок(а/ов) стали {(impostorGhost ? "ghost impostor" : "призраками")}!"); - } - - public static void SetAllPlayersRole(RoleTypes role) - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) - { - ShowNotification("[ОШИБКА] Требуются права хоста!"); - return; - } - if (PlayerControl.AllPlayerControls == null) return; - - int count = 0; - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null && pc.Data != null && !pc.Data.Disconnected) - { - pc.RpcSetRole(role, true); - count++; - } - } - - ShowNotification($"[РОЛИ] {count} игрок(а/ов) получили роль {GetRoleDisplayName(role)}!"); - } - - public static void SetPlayerRole(PlayerControl target, RoleTypes newRole) - { - if (target == null || target.Data == null) return; - target.RpcSetRole(newRole, true); - } - - private void DrawRolesTab() - { - GUILayout.BeginHorizontal(); - - GUILayout.BeginVertical(GUILayout.Width(280)); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Roles", headerStyle); - GUILayout.BeginHorizontal(); - GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) } }; - if (GUILayout.Button("<", btnStyle, GUILayout.Width(25), GUILayout.Height(22))) { fakeRoleIdx--; if (fakeRoleIdx < 0) fakeRoleIdx = forceRoleOptions.Length - 1; } - GUILayout.Label(forceRoleOptions[fakeRoleIdx].ToString(), middleLabelStyle, GUILayout.Width(100), GUILayout.Height(22)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(25), GUILayout.Height(22))) { fakeRoleIdx++; if (fakeRoleIdx >= forceRoleOptions.Length) fakeRoleIdx = 0; } - GUILayout.Space(15); - if (GUILayout.Button("Set", activeTabStyle, GUILayout.Width(45), GUILayout.Height(22))) RoleManager.Instance?.SetRole(PlayerControl.LocalPlayer, forceRoleOptions[fakeRoleIdx]); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.Space(5); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Impostor", headerStyle); - killReach = DrawToggle(killReach, "Kill Reach", 160); - GUILayout.Space(5); - killAnyone = DrawToggle(killAnyone, "Kill Anyone", 160); - GUILayout.Space(5); - killAuraHostOnly = DrawToggle(killAuraHostOnly, "Kill Aura", 160); - GUILayout.Space(5); - noKillCooldownHostOnly = DrawToggle(noKillCooldownHostOnly, "Kill Cooldown 0 (Host)", 160); - GUILayout.Space(5); - spamReportBodies = DrawToggle(spamReportBodies, "Spam Report Bodies", 160); - GUILayout.EndVertical(); - - GUILayout.Space(5); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Shapeshifter", headerStyle); - NoShapeshiftAnim = DrawToggle(NoShapeshiftAnim, "No Ss Animation", 160); - GUILayout.Space(5); - endlessSsDuration = DrawToggle(endlessSsDuration, "Endless Ss Duration", 160); - GUILayout.EndVertical(); - - GUILayout.Space(5); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Tracker", headerStyle); - EndlessTracking = DrawToggle(EndlessTracking, "Endless Tracking", 160); - GUILayout.Space(5); - NoTrackingCooldown = DrawToggle(NoTrackingCooldown, "No Track Cooldown", 160); - GUILayout.EndVertical(); - - GUILayout.EndVertical(); - - GUILayout.Space(10); - - GUILayout.BeginVertical(GUILayout.Width(280)); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Engineer", headerStyle); - endlessVentTime = DrawToggle(endlessVentTime, "Endless Vent Time", 160); - GUILayout.Space(5); - noVentCooldown = DrawToggle(noVentCooldown, "No Vent Cooldown", 160); - GUILayout.Space(5); - noMapCooldowns = DrawToggle(noMapCooldowns, "No Map Cooldowns", 160); - GUILayout.EndVertical(); - - GUILayout.Space(5); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Scientist", headerStyle); - endlessBattery = DrawToggle(endlessBattery, "Endless Battery", 160); - GUILayout.Space(5); - noVitalsCooldown = DrawToggle(noVitalsCooldown, "No Vitals Cooldown", 160); - GUILayout.EndVertical(); - - GUILayout.Space(5); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("Detective", headerStyle); - UnlimitedInterrogateRange = DrawToggle(UnlimitedInterrogateRange, "Interrogate Reach", 160); - GUILayout.EndVertical(); - - GUILayout.EndVertical(); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - } - - private Vector2 doorsScrollPos = Vector2.zero; - - private void DrawSabotagesTab() - { - GUIStyle miniLabelStyle = new GUIStyle(toggleLabelStyle) { fontSize = 11, richText = true, wordWrap = true }; - miniLabelStyle.normal.textColor = whiteMenuTheme ? new Color(0.25f, 0.25f, 0.25f, 1f) : new Color(0.72f, 0.72f, 0.72f, 1f); - - GUILayout.BeginHorizontal(); - - GUILayout.BeginVertical(boxStyle, GUILayout.Width(276), GUILayout.ExpandHeight(false)); - GUILayout.Label("CRITICAL SABOTAGES", headerStyle); - GUILayout.Space(4); - - GUILayout.BeginHorizontal(); - if (DrawColoredActionButton("FIX ALL", new Color32(83, 231, 139, 255), 126f, 32f)) FixAllSabotages(); - GUILayout.Space(6); - if (DrawColoredActionButton("TRIGGER ALL", new Color32(255, 74, 74, 255), 126f, 32f)) TriggerAllSabotages(); - GUILayout.EndHorizontal(); - - GUILayout.Space(6); - if (GUILayout.Button("CALL MEETING", btnStyle, GUILayout.Height(30))) callMeetingPublic(); - - GUILayout.Space(8); - GUILayout.BeginHorizontal(); - DrawSabotageButton("Reactor", ref reactorSab, ToggleReactor, new Color32(255, 84, 84, 255)); - GUILayout.Space(6); - DrawSabotageButton("Oxygen", ref oxygenSab, ToggleO2, new Color32(255, 132, 54, 255)); - GUILayout.EndHorizontal(); - - GUILayout.Space(6); - GUILayout.BeginHorizontal(); - DrawSabotageButton("Comms", ref commsSab, ToggleComms, new Color32(66, 205, 128, 255)); - GUILayout.Space(6); - DrawSabotageButton("Lights", ref elecSab, ToggleLights, new Color32(255, 218, 77, 255)); - GUILayout.EndHorizontal(); - - GUILayout.Space(8); - if (GUILayout.Button("MUSHROOM MIXUP", btnStyle, GUILayout.Height(28))) SabotageMushroom(); - GUILayout.EndVertical(); - - GUILayout.Space(10); - - GUILayout.BeginVertical(boxStyle, GUILayout.ExpandWidth(true)); - GUILayout.Label("DOOR LOCKDOWN", headerStyle); - GUILayout.Space(4); - GUILayout.Label("Global controls", miniLabelStyle); - - GUILayout.BeginHorizontal(); - if (DrawColoredActionButton("CLOSE", new Color32(255, 106, 66, 255), 88f, 30f)) SabotageDoors(); - GUILayout.Space(6); - if (DrawColoredActionButton("LOCK", new Color32(255, 184, 64, 255), 88f, 30f)) LockAllDoors(); - GUILayout.Space(6); - if (DrawColoredActionButton("OPEN", new Color32(89, 219, 146, 255), 88f, 30f)) OpenAllDoors(); - GUILayout.EndHorizontal(); - - GUILayout.Space(8); - GUILayout.Label("Target doors", miniLabelStyle); - - if (ShipStatus.Instance != null && ShipStatus.Instance.AllDoors != null) - { - var rooms = ShipStatus.Instance.AllDoors - .Where(d => d != null) - .Select(d => d.Room) - .Distinct() - .OrderBy(r => r.ToString()) - .ToList(); - - doorsScrollPos = GUILayout.BeginScrollView(doorsScrollPos, false, true, GUILayout.Height(214)); - foreach (var room in rooms) - { - DrawDoorTargetRow(room); - GUILayout.Space(3); - } - GUILayout.EndScrollView(); - } - else - { - GUILayout.FlexibleSpace(); - GUILayout.Label("Вы не в игре или на карте нет дверей.", new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, richText = true }); - GUILayout.FlexibleSpace(); - } - GUILayout.EndVertical(); - - GUILayout.EndHorizontal(); - } - - private void DrawSabotageButton(string label, ref bool state, Action toggleAction, Color accent) - { - GUIStyle style = state ? activeTabStyle : btnStyle; - Color oldBackground = GUI.backgroundColor; - GUI.backgroundColor = state ? accent : Color.white; - - if (GUILayout.Button(state ? label + " ON" : label, style, GUILayout.Height(30))) - { - state = !state; - toggleAction(state); - } - - GUI.backgroundColor = oldBackground; - } - - private void DrawDoorTargetRow(SystemTypes room) - { - GUILayout.BeginHorizontal(boxStyle); - GUILayout.Label($"{room}", toggleLabelStyle, GUILayout.Width(96)); - GUILayout.FlexibleSpace(); - - if (GUILayout.Button("Close", btnStyle, GUILayout.Width(52), GUILayout.Height(24))) CloseDoorsOfType(room); - GUILayout.Space(4); - if (GUILayout.Button("Lock", activeSubTabStyle, GUILayout.Width(52), GUILayout.Height(24))) LockDoorsOfType(room); - GUILayout.Space(4); - if (GUILayout.Button("Open", btnStyle, GUILayout.Width(52), GUILayout.Height(24))) OpenDoorsOfType(room); - - GUILayout.EndHorizontal(); - } - private void callMeetingPublic() - { - if (PlayerControl.LocalPlayer == null || PlayerControl.AllPlayerControls == null) return; - try - { - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null && pc.Data != null && pc.Data.IsDead && !pc.Data.Disconnected) - { - PlayerControl.LocalPlayer.CmdReportDeadBody(pc.Data); - ShowNotification($"[MEETING] Найден и зарепорчен труп: {pc.Data.PlayerName}!"); - return; - } - } - - PlayerControl.LocalPlayer.CmdReportDeadBody(null); - ShowNotification("[MEETING] Легально нажата кнопка собрания!"); - } - catch { } - } - private void TriggerAllSabotages() - { - if (ShipStatus.Instance == null) return; - try - { - reactorSab = true; - oxygenSab = true; - commsSab = true; - elecSab = true; - - ToggleReactor(true); - ToggleO2(true); - ToggleComms(true); - ToggleLights(true); - - ShowNotification("[SABOTAGE] Все системы саботированы!"); - } - catch { } - } - private void FixAllSabotages() - { - if (ShipStatus.Instance == null) return; - try - { - reactorSab = false; - oxygenSab = false; - commsSab = false; - elecSab = false; - - ToggleReactor(false); - ToggleO2(false); - ToggleComms(false); - ToggleLights(false); - - if (ShipStatus.Instance.AllDoors != null) - { - foreach (var door in ShipStatus.Instance.AllDoors) - { - if (door != null) - { - door.SetDoorway(true); - try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(door.Id | 64)); } catch { } - } - } - } - try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.MushroomMixupSabotage, 0); } catch { } - ShowNotification("[SABOTAGE] Все саботажи и двери починены!"); - } - catch { } - } - - private void SabotageDoors() - { - if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; - try - { - var rooms = new System.Collections.Generic.HashSet(); - foreach (var door in ShipStatus.Instance.AllDoors) - { - if (door != null) - { - rooms.Add(door.Room); - try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); } catch { } - } - } - foreach (var room in rooms) - { - try { ShipStatus.Instance.RpcCloseDoorsOfType(room); } catch { } - } - ShowNotification("[DOORS] Сигнал на закрытие отправлен!"); - } - catch { } - } - - - private void CloseDoorsOfType(SystemTypes room) - { - if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; - try - { - ShipStatus.Instance.RpcCloseDoorsOfType(room); - foreach (var door in ShipStatus.Instance.AllDoors) - { - if (door != null && door.Room == room) - ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); - } - ShowNotification($"[DOORS] {room}: close sent"); - } - catch { } - } - - private void LockDoorsOfType(SystemTypes room) - { - if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; - try - { - foreach (var door in ShipStatus.Instance.AllDoors) - { - if (door != null && door.Room == room) - { - door.SetDoorway(false); - ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); - } - } - ShipStatus.Instance.RpcCloseDoorsOfType(room); - ShowNotification($"[DOORS] {room}: locked"); - } - catch { } - } - - private void OpenDoorsOfType(SystemTypes room) - { - if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; - try - { - foreach (var door in ShipStatus.Instance.AllDoors) - { - if (door != null && door.Room == room) - { - door.SetDoorway(true); - ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(door.Id | 64)); - } - } - ShowNotification($"[DOORS] {room}: opened"); - } - catch { } - } - - private void LockAllDoors() - { - if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; - try - { - var rooms = new System.Collections.Generic.HashSet(); - foreach (var door in ShipStatus.Instance.AllDoors) - { - if (door != null) - { - door.SetDoorway(false); - rooms.Add(door.Room); - ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); - } - } - foreach (var room in rooms) - ShipStatus.Instance.RpcCloseDoorsOfType(room); - - ShowNotification("[DOORS] Все двери залочены!"); - } - catch { } - } - private void OpenAllDoors() - { - if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; - try - { - foreach (var door in ShipStatus.Instance.AllDoors) - { - if (door != null) - { - door.SetDoorway(true); - try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(door.Id | 64)); } catch { } - } - } - ShowNotification("[DOORS] Все двери открыты!"); - } - catch { } - } - - private void ToggleReactor(bool state) { if (ShipStatus.Instance == null) return; byte flag = (byte)(state ? 128 : 16); try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Reactor, flag); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Laboratory, flag); if (state) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)128); else { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)16); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)17); } } catch { } } - private void ToggleO2(bool state) { if (ShipStatus.Instance == null) return; try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.LifeSupp, (byte)(state ? 128 : 16)); } catch { } } - private void ToggleComms(bool state) { if (ShipStatus.Instance == null) return; try { if (state) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)128); else { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)16); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)17); } } catch { } } - private void ToggleLights(bool state) - { - if (ShipStatus.Instance == null) return; - try - { - if (state) - { - byte b = 4; - for (int i = 0; i < 5; i++) if (UnityEngine.Random.value > 0.5f) b |= (byte)(1 << i); - ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, (byte)(b | 128)); - } - else - { - var sys = ShipStatus.Instance.Systems[SystemTypes.Electrical].Cast(); - if (sys != null) - { - for (int i = 0; i < 5; i++) - { - bool expected = (sys.ExpectedSwitches & (1 << i)) != 0; - bool actual = (sys.ActualSwitches & (1 << i)) != 0; - if (expected != actual) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, (byte)i); - } - } - } - } - catch { } - } - private void SabotageMushroom() { if (ShipStatus.Instance == null) return; try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.MushroomMixupSabotage, (byte)1); } catch { } } - - private void DrawPlayersRoles() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("PRE-GAME ROLE MANAGER", headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button(enablePreGameRoleForce ? "Role Forcing: ON" : "Role Forcing: OFF", enablePreGameRoleForce ? activeTabStyle : btnStyle, GUILayout.Height(25))) enablePreGameRoleForce = !enablePreGameRoleForce; - if (GUILayout.Button("Random 2 Imps", btnStyle, GUILayout.Width(110), GUILayout.Height(25))) - { - forcedPreGameRoles.Clear(); forcedImpostors.Clear(); - var activePlayers = PlayerControl.AllPlayerControls.ToArray().Where(p => p != null && !p.Data.Disconnected).ToList(); - if (activePlayers.Count >= 2) - { - for (int i = activePlayers.Count - 1; i > 0; i--) { int swapIndex = UnityEngine.Random.Range(0, i + 1); var temp = activePlayers[i]; activePlayers[i] = activePlayers[swapIndex]; activePlayers[swapIndex] = temp; } - forcedImpostors.Add(activePlayers[0].PlayerId); forcedImpostors.Add(activePlayers[1].PlayerId); - enablePreGameRoleForce = true; - } - } - if (GUILayout.Button("Clear All Roles", btnStyle, GUILayout.Width(110), GUILayout.Height(25))) { forcedPreGameRoles.Clear(); forcedImpostors.Clear(); } - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.Space(8); - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("LIVE ROLE DISTRIBUTOR (HOST)", headerStyle); - GUILayout.BeginHorizontal(); - - GUIStyle allRoleMidStyle = new GUIStyle(btnStyle) - { - fontStyle = FontStyle.Bold, - normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, - alignment = TextAnchor.MiddleCenter - }; - - if (GUILayout.Button("<", btnStyle, GUILayout.Width(28), GUILayout.Height(25))) - { - allPlayersRoleAssignIdx--; - if (allPlayersRoleAssignIdx < 0) allPlayersRoleAssignIdx = roleAssignOptions.Length - 1; - } - - GUILayout.Label(roleAssignNames[allPlayersRoleAssignIdx], allRoleMidStyle, GUILayout.Height(25), GUILayout.ExpandWidth(true)); - - if (GUILayout.Button(">", btnStyle, GUILayout.Width(28), GUILayout.Height(25))) - { - allPlayersRoleAssignIdx++; - if (allPlayersRoleAssignIdx >= roleAssignOptions.Length) allPlayersRoleAssignIdx = 0; - } - GUILayout.EndHorizontal(); - - GUILayout.Space(5); - if (GUILayout.Button("SET ALL PLAYERS ROLE", activeTabStyle, GUILayout.Height(28))) - { - if (IsGhostRoleSelection(allPlayersRoleAssignIdx)) - SetAllPlayersGhost(); - else if (IsGhostImpostorRoleSelection(allPlayersRoleAssignIdx)) - SetAllPlayersGhost(true); - else - SetAllPlayersRole(roleAssignOptions[allPlayersRoleAssignIdx]); - } - GUILayout.Space(4); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("ALL -> GHOST", btnStyle, GUILayout.Height(26))) - SetAllPlayersGhost(); - if (GUILayout.Button("ALL -> GHOST IMP", btnStyle, GUILayout.Height(26))) - SetAllPlayersGhost(true); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.Space(10); - GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(boxStyle, GUILayout.Width(200)); - preRolesListScrollPos = GUILayout.BeginScrollView(preRolesListScrollPos); - foreach (var pc in lockedPlayersList) - { - if (pc == null || pc.Data == null || pc.PlayerId >= 100) continue; - string pName = pc.Data.PlayerName ?? "Unknown"; - if (forcedPreGameRoles.ContainsKey(pc.PlayerId)) { string rShort = forcedPreGameRoles[pc.PlayerId].ToString().Replace("9", "Pha").Replace("10", "Tra").Replace("8", "Noi").Replace("12", "Det").Replace("18", "Vip"); if (rShort.Length > 3) rShort = rShort.Substring(0, 3); pName += $" [{rShort}]"; } - else if (forcedImpostors.Contains(pc.PlayerId)) pName += " [Imp]"; - bool isSelected = selectedPreRoleId == pc.PlayerId; - try { GUI.contentColor = Palette.PlayerColors[pc.Data.DefaultOutfit.ColorId]; } catch { } - if (GUILayout.Button(pName, isSelected ? activeTabStyle : btnStyle, GUILayout.Height(30))) selectedPreRoleId = pc.PlayerId; - GUI.contentColor = Color.white; - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - - GUILayout.BeginVertical(boxStyle, GUILayout.ExpandWidth(true)); - preRolesActionScrollPos = GUILayout.BeginScrollView(preRolesActionScrollPos); - PlayerControl target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedPreRoleId); - if (target != null && target.Data != null) - { - GUIStyle infoStyle = new GUIStyle(GUI.skin.label) { richText = true, fontSize = 14 }; - GUILayout.Label($"Selecting role for: {target.Data.PlayerName}", infoStyle); - RoleTypes currentForced = forcedPreGameRoles.ContainsKey(target.PlayerId) ? forcedPreGameRoles[target.PlayerId] : RoleTypes.Crewmate; - bool isForced = forcedPreGameRoles.ContainsKey(target.PlayerId) || forcedImpostors.Contains(target.PlayerId); - string roleNameStr = currentForced.ToString().Replace("9", "Phantom").Replace("10", "Tracker").Replace("8", "Noisemaker").Replace("12", "Detective").Replace("18", "Viper"); - if (forcedImpostors.Contains(target.PlayerId)) roleNameStr = "Impostor"; - GUILayout.Label($"Status: {(isForced ? $"Forced ({roleNameStr})" : "Not Forced (Random)")}", infoStyle); - GUILayout.Space(15); - GUILayout.Label("IMPOSTOR ROLES (Red Team)", headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Impostor", btnStyle, GUILayout.Height(30))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Add(target.PlayerId); } - if (GUILayout.Button("Shapeshifter", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Shapeshifter; } - if (GUILayout.Button("Phantom", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)9; } - if (GUILayout.Button("Viper", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)18; } - GUILayout.EndHorizontal(); - GUILayout.Space(10); - GUILayout.Label("CREWMATE ROLES (Blue Team)", headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Crewmate", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Crewmate; } - if (GUILayout.Button("Engineer", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Engineer; } - if (GUILayout.Button("Scientist", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Scientist; } - if (GUILayout.Button("Tracker", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)10; } - GUILayout.EndHorizontal(); - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Noisemaker", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)8; } - if (GUILayout.Button("Guardian Angel", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.GuardianAngel; } - if (GUILayout.Button("Detective", btnStyle, GUILayout.Height(30))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)12; } - GUILayout.EndHorizontal(); - GUILayout.Space(15); - if (GUILayout.Button("REMOVE FORCED ROLE", activeTabStyle, GUILayout.Height(35))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Remove(target.PlayerId); } - GUILayout.Space(20); - GUILayout.Label("Hide & Seek Notice:\nВыбор Impostor/Shapeshifter/Phantom/Viper расширит лимит маньяков (Seekers) в Прятках!", new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true }); - } - else - { - GUILayout.FlexibleSpace(); - GUILayout.Label("Select a player to set their role", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); - GUILayout.FlexibleSpace(); - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - } - - private void DrawMenuTab() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("MENU CUSTOMIZATION", headerStyle); - GUILayout.Space(5); - bool menuPrefsChanged = false; - - bool prevRgb = rgbMenuMode; - rgbMenuMode = DrawToggle(rgbMenuMode, "RGB Menu Mode"); - if (prevRgb && !rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); - if (prevRgb != rgbMenuMode) menuPrefsChanged = true; - - GUILayout.Space(5); - - bool prevWhiteTheme = whiteMenuTheme; - whiteMenuTheme = DrawToggle(whiteMenuTheme, "White Theme"); - if (prevWhiteTheme != whiteMenuTheme) - { - InitStyles(); - UpdateAccentColor(currentAccentColor); - menuPrefsChanged = true; - } - - GUILayout.Space(5); - - bool prevBg = enableBackground; - enableBackground = DrawToggle(enableBackground, "Enable Image Background"); - if (enableBackground && !prevBg) LoadBackgroundImage(); - if (prevBg != enableBackground) menuPrefsChanged = true; - - GUILayout.Space(5); - GUILayout.Label("Put 'MenuBG.png' or .jpg in BepInEx/config to add a background image.", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11 }); - - GUILayout.Space(10); - GUILayout.BeginHorizontal(); - GUILayout.Label($"{L("FPS Limit", "Лимит FPS")}: {fpsLimit}", new GUIStyle(toggleLabelStyle) { richText = true }, GUILayout.Width(125)); - int newFpsLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(fpsLimit, 60f, 240f, sliderStyle, sliderThumbStyle, GUILayout.Width(210)), 60, 240); - if (newFpsLimit != fpsLimit) - { - fpsLimit = newFpsLimit; - ApplyFpsLimit(); - menuPrefsChanged = true; - } - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - - GUILayout.BeginHorizontal(); - GUIStyle middleColorStyle = new GUIStyle(btnStyle) { normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) }, fontStyle = FontStyle.Bold }; - GUI.enabled = !rgbMenuMode; - if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex--; if (currentMenuColorIndex < 0) currentMenuColorIndex = menuColors.Length - 1; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); menuPrefsChanged = true; } - GUILayout.Label(menuColorNames[currentMenuColorIndex], middleColorStyle, GUILayout.Width(110), GUILayout.Height(25)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex++; if (currentMenuColorIndex >= menuColors.Length) currentMenuColorIndex = 0; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); menuPrefsChanged = true; } - GUI.enabled = true; - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.Space(10); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("SPOOF MENU IDENTITY", headerStyle); - bool prevSpoofMenuEnabled = SpoofMenuEnabled; - SpoofMenuEnabled = DrawToggle(SpoofMenuEnabled, "Enable Fake RPC"); - if (prevSpoofMenuEnabled != SpoofMenuEnabled) menuPrefsChanged = true; - GUILayout.Space(5); - GUILayout.BeginHorizontal(); - GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetThemeAccentColor(currentAccentColor) } }; - if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex--; if (selectedSpoofMenuIndex < 0) selectedSpoofMenuIndex = spoofMenuNames.Length - 1; menuPrefsChanged = true; } - GUILayout.Label($"{spoofMenuNames[selectedSpoofMenuIndex]}", middleLabelStyle, GUILayout.Width(110), GUILayout.Height(25)); - if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex++; if (selectedSpoofMenuIndex >= spoofMenuNames.Length) selectedSpoofMenuIndex = 0; menuPrefsChanged = true; } - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.Space(10); - - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("NOTIFICATIONS & LOGGING", headerStyle); - GUILayout.Space(5); - bool prevCustomNotifs = EnableCustomNotifs; - EnableCustomNotifs = DrawToggle(EnableCustomNotifs, "Enable Custom UI Notifications", 250); - if (prevCustomNotifs != EnableCustomNotifs) menuPrefsChanged = true; - GUILayout.Space(5); - bool prevLogAllRpcs = LogAllRPCs; - LogAllRPCs = DrawToggle(LogAllRPCs, "Sniff All RPCs (On-Screen)", 250); - if (prevLogAllRpcs != LogAllRPCs) menuPrefsChanged = true; - GUILayout.EndVertical(); - - if (menuPrefsChanged) SaveConfig(); - } - private Vector2 outfitsScrollPos = Vector2.zero; - public static bool AutoHostEnabled = false; - public static bool AutoReturnLobbyAfterMatch = true; - public static bool AutoHostNotifications = true; - public static bool AutoHostForceLastMinute = true; - public static bool AutoHostWaitLoadedPlayers = true; - public static bool AutoHostCancelBelowMin = true; - public static bool AutoHostInstantStart = false; - - public static int AutoHostMinPlayers = 4; - public static int AutoHostForceMinPlayers = 2; - public static float AutoHostStartDelaySeconds = 15f; - public static float AutoHostBackoffSeconds = 8f; - public static float AutoHostWarmupSeconds = 5f; - public static float AutoHostLoadGraceSeconds = 20f; - - public static int AutoHostForceAfterMinutes = 0; - public static int AutoHostFastStartPlayers = 13; - public static float AutoHostFastStartDelaySeconds = 5f; - - private int currentAutoHostSubTab = 0; - private string[] autoHostSubTabs = { "LOBBY CONTROLS", "ROLE MANAGER", "ANTI CHEAT", "AUTO HOST" }; - - private struct FavoriteOutfitSnapshot - { - public int ColorId; - public string HatId; - public string SkinId; - public string VisorId; - public string NamePlateId; - public string PetId; - - public FavoriteOutfitSnapshot(int colorId, string hatId, string skinId, string visorId, string namePlateId, string petId) - { - ColorId = colorId; - HatId = hatId ?? string.Empty; - SkinId = skinId ?? string.Empty; - VisorId = visorId ?? string.Empty; - NamePlateId = namePlateId ?? string.Empty; - PetId = petId ?? string.Empty; - } - } - - private void DrawOutfitsTab() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label(L("FAVORITE OUTFITS", "ИЗБРАННЫЕ ОБРАЗЫ"), headerStyle); - - PlayerControl selected = SelectedOutfitSourcePlayer(); - for (int i = 0; i < FavoriteOutfitSlotCount; i++) - { - bool hasOutfit = TryDeserializeFavoriteOutfit(favoriteOutfitSlots[i], out FavoriteOutfitSnapshot outfit); - GUILayout.BeginVertical(boxStyle); - - GUILayout.BeginHorizontal(); - GUILayout.Label($"{L("Slot", "Слот")} {i + 1}", toggleLabelStyle, GUILayout.Width(52), GUILayout.Height(22)); - GUILayout.Label(hasOutfit ? FavoriteOutfitSummary(outfit) : L("Empty", "Пусто"), new GUIStyle(GUI.skin.label) { fontSize = 11, clipping = TextClipping.Clip, alignment = TextAnchor.MiddleLeft }, GUILayout.ExpandWidth(true), GUILayout.Height(22)); - GUI.enabled = hasOutfit; - if (GUILayout.Button(L("Apply", "Надеть"), btnStyle, GUILayout.Width(58), GUILayout.Height(22))) - ApplyFavoriteOutfitSlot(i, outfit, hasOutfit); - GUI.enabled = true; - if (GUILayout.Button("X", btnStyle, GUILayout.Width(28), GUILayout.Height(22))) - ClearFavoriteOutfitSlot(i); - GUILayout.EndHorizontal(); - - GUILayout.BeginHorizontal(); - GUILayout.Space(52); - if (GUILayout.Button(L("Save Mine", "Сохр. мой"), btnStyle, GUILayout.Width(100), GUILayout.Height(22))) - SaveFavoriteOutfitSlot(i, PlayerControl.LocalPlayer); - if (GUILayout.Button(L("Save Selected", "Сохр. выбран"), btnStyle, GUILayout.Width(120), GUILayout.Height(22))) - SaveFavoriteOutfitSlot(i, selected); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.EndVertical(); - GUILayout.Space(4); - } - - GUILayout.Space(12); - GUILayout.Label("COPY SPECIFIC PLAYER", headerStyle); - - outfitsScrollPos = GUILayout.BeginScrollView(outfitsScrollPos); - if (lockedPlayersList.Count > 0) - { - foreach (var pc in lockedPlayersList) - { - if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null) continue; - - GUILayout.BeginHorizontal(boxStyle); - try - { - string pName = pc.Data.PlayerName ?? "Unknown"; - GUILayout.Label(pName, GUILayout.Width(150)); - - if (GUILayout.Button("Copy Outfit", btnStyle, GUILayout.Height(25))) - { - try - { - PlayerControl.LocalPlayer.RpcSetSkin(pc.Data.DefaultOutfit.SkinId); - PlayerControl.LocalPlayer.RpcSetHat(pc.Data.DefaultOutfit.HatId); - PlayerControl.LocalPlayer.RpcSetVisor(pc.Data.DefaultOutfit.VisorId); - PlayerControl.LocalPlayer.RpcSetNamePlate(pc.Data.DefaultOutfit.NamePlateId); - PlayerControl.LocalPlayer.RpcSetPet(pc.Data.DefaultOutfit.PetId); - } - catch { } - } - } - finally { GUILayout.EndHorizontal(); } - GUILayout.Space(2); - } - } - else - { - GUILayout.Label("Нет игроков для копирования."); - } - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - } - - private static PlayerControl SelectedOutfitSourcePlayer() - { - try - { - if (lockedPlayersList != null) - { - foreach (PlayerControl pc in lockedPlayersList) - { - if (pc != null && pc != PlayerControl.LocalPlayer && pc.Data != null && !pc.Data.Disconnected) - return pc; - } - } - } - catch { } - - return PlayerControl.LocalPlayer; - } - - private static int MaxOutfitColorId() - { - try { return Palette.PlayerColors != null ? Mathf.Max(0, Palette.PlayerColors.Length - 1) : 18; } - catch { return 18; } - } - - private static bool TryCaptureFavoriteOutfit(PlayerControl source, out FavoriteOutfitSnapshot outfit) - { - outfit = default; - try - { - if (source == null || source.Data == null || source.Data.DefaultOutfit == null) return false; - var sourceOutfit = source.Data.DefaultOutfit; - outfit = new FavoriteOutfitSnapshot( - Mathf.Clamp(sourceOutfit.ColorId, 0, MaxOutfitColorId()), - sourceOutfit.HatId, - sourceOutfit.SkinId, - sourceOutfit.VisorId, - sourceOutfit.NamePlateId, - sourceOutfit.PetId); - return true; - } - catch { } - - return false; - } - - private static void ApplyFavoriteOutfit(PlayerControl target, FavoriteOutfitSnapshot outfit) - { - if (target == null) return; - target.RpcSetColor((byte)Mathf.Clamp(outfit.ColorId, 0, MaxOutfitColorId())); - target.RpcSetSkin(outfit.SkinId ?? string.Empty); - target.RpcSetHat(outfit.HatId ?? string.Empty); - target.RpcSetVisor(outfit.VisorId ?? string.Empty); - target.RpcSetNamePlate(outfit.NamePlateId ?? string.Empty); - target.RpcSetPet(outfit.PetId ?? string.Empty); - } - - private static string SerializeFavoriteOutfit(FavoriteOutfitSnapshot outfit) - { - return string.Join("\t", new[] - { - Mathf.Clamp(outfit.ColorId, 0, MaxOutfitColorId()).ToString(), - CleanFavoriteOutfitPart(outfit.HatId), - CleanFavoriteOutfitPart(outfit.SkinId), - CleanFavoriteOutfitPart(outfit.VisorId), - CleanFavoriteOutfitPart(outfit.NamePlateId), - CleanFavoriteOutfitPart(outfit.PetId) - }); - } - - private static bool TryDeserializeFavoriteOutfit(string value, out FavoriteOutfitSnapshot outfit) - { - outfit = default; - if (string.IsNullOrWhiteSpace(value)) return false; - - string[] parts = value.Split('\t'); - if (parts.Length < 6 || !int.TryParse(parts[0], out int colorId)) return false; - - outfit = new FavoriteOutfitSnapshot(Mathf.Clamp(colorId, 0, MaxOutfitColorId()), parts[1], parts[2], parts[3], parts[4], parts[5]); - return true; - } - - private static string CleanFavoriteOutfitPart(string value) - { - if (string.IsNullOrEmpty(value)) return string.Empty; - return value.Replace("\t", " ").Replace("\r", " ").Replace("\n", " ").Trim(); - } - - private static string FavoriteOutfitSummary(FavoriteOutfitSnapshot outfit) - { - string color = "Color " + outfit.ColorId; - try { color = Palette.GetColorName(outfit.ColorId); } catch { } - return $"{color} | {ShortOutfitId(outfit.HatId)}"; - } - - private static string ShortOutfitId(string value) - { - if (string.IsNullOrWhiteSpace(value)) return "-"; - string cleaned = value.Trim(); - return cleaned.Length <= 10 ? cleaned : cleaned.Substring(0, 10); - } - - private void SaveFavoriteOutfitSlot(int index, PlayerControl source) - { - if (index < 0 || index >= favoriteOutfitSlots.Length) return; - if (!TryCaptureFavoriteOutfit(source, out FavoriteOutfitSnapshot outfit)) - { - ShowNotification($"[OUTFIT] {L("Player outfit is not ready.", "Образ игрока еще не готов.")}"); - return; - } - - favoriteOutfitSlots[index] = SerializeFavoriteOutfit(outfit); - SaveConfig(); - ShowNotification($"[OUTFIT] {L("Saved slot", "Сохранен слот")} {index + 1}"); - } - - private void ApplyFavoriteOutfitSlot(int index, FavoriteOutfitSnapshot outfit, bool hasOutfit) - { - if (!hasOutfit) - { - ShowNotification($"[OUTFIT] {L("Slot is empty.", "Слот пуст.")}"); - return; - } - - try - { - ApplyFavoriteOutfit(PlayerControl.LocalPlayer, outfit); - ShowNotification($"[OUTFIT] {L("Applied slot", "Надет слот")} {index + 1}"); - } - catch { } - } - - private void ClearFavoriteOutfitSlot(int index) - { - if (index < 0 || index >= favoriteOutfitSlots.Length) return; - favoriteOutfitSlots[index] = string.Empty; - SaveConfig(); - ShowNotification($"[OUTFIT] {L("Cleared slot", "Очищен слот")} {index + 1}"); - } - public static bool removePenalty = true; - public static bool alwaysShowLobbyTimer = false; - public static bool enableChatLog = true; - public static bool enableFastChat = true; - public static bool allowLinksAndSymbols = false; - - private static readonly System.Collections.Generic.Dictionary CachedSprites = new(); - - public static Sprite LoadEmbeddedSprite(string fileName, float pixelsPerUnit = 1f) - { - string path = $"ElysiumModMenu.{fileName}"; - - try - { - if (CachedSprites.TryGetValue(path + pixelsPerUnit, out var cachedSprite)) - return cachedSprite; - - var assembly = System.Reflection.Assembly.GetExecutingAssembly(); - using var stream = assembly.GetManifestResourceStream(path); - - if (stream == null) - { - return null; - } - - var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); - using System.IO.MemoryStream ms = new System.IO.MemoryStream(); - stream.CopyTo(ms); - - ImageConversion.LoadImage(texture, ms.ToArray(), false); - - Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); - - sprite.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontSaveInEditor; - - return CachedSprites[path + pixelsPerUnit] = sprite; - } - catch (System.Exception) - { - return null; - } - } - public void Start() - { - if (enableBackground) LoadBackgroundImage(); - UnlockCosmetics(); - LoadConfig(); - LoadBanList(); - ClearSpamErrorLogOnStartup(); - StartBackgroundAnomalyLogMonitor(); - - - try - { - int starts = UnityEngine.PlayerPrefs.GetInt("Elysium_GameStarts", 0); - starts++; - - string chatLogPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ChatLog.txt"); - - if (starts >= 3) - { - if (System.IO.File.Exists(chatLogPath)) - { - System.IO.File.WriteAllText(chatLogPath, string.Empty); - } - starts = 0; - } - - UnityEngine.PlayerPrefs.SetInt("Elysium_GameStarts", starts); - UnityEngine.PlayerPrefs.Save(); - } - catch { } - } - - public void OnApplicationQuit() - { - StopBackgroundAnomalyLogMonitor(); - SaveConfig(); - } - - public void OnDisable() - { - SaveConfig(); - } - - private static void ClearSpamErrorLogOnStartup() - { - try - { - watchedLogLineCounts.Clear(); - logBurstWindowStartedAt = -1f; - logBurstCooldownUntil = 0f; - logBurstLineCount = 0; - anomalyLogWatchNotified = false; - logMonitorNextScanAt = 0f; - - string root = string.IsNullOrWhiteSpace(Plugin.ElysiumFolder) - ? System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu") - : Plugin.ElysiumFolder; - - if (!System.IO.Directory.Exists(root)) return; - - foreach (string file in System.IO.Directory.GetFiles(root, "SpamErrorLog*.txt", System.IO.SearchOption.AllDirectories)) - { - try { System.IO.File.Delete(file); } - catch { } - } - - System.Console.WriteLine("[ElysiumModMenu] Cleared previous SpamErrorLog files and reset log monitor state."); - } - catch (Exception ex) - { - System.Console.WriteLine($"[ElysiumModMenu] Failed to clear SpamErrorLog files: {ex.GetType().Name}: {ex.Message}"); - } - } - - public static void SendAnomalyAlert(string title, string message, string dedupeKey = null, bool waitForCompletion = false, IEnumerable attachmentPaths = null) - { - if (!enableAnomalyLogReports) return; - if (!DiscordStatusReporter.IsEnabled) return; - string webhookUrl = DiscordStatusReporter.ConfiguredWebhookUrl; - if (!DiscordStatusReporter.IsValidWebhookUrl(webhookUrl)) return; - int attachmentCount = attachmentPaths == null ? 0 : attachmentPaths.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().Count(); - System.Console.WriteLine($"[ElysiumModMenu] Sending freeze/overload logs to configured webhook. Type={title}. Attachments={attachmentCount}."); - DiscordStatusReporter.SendDiagnosticAlert(webhookUrl, title, message, waitForCompletion, attachmentPaths); - } - - private static void StartBackgroundAnomalyLogMonitor() - { - try - { - if (anomalyLogMonitorTimer != null) return; - anomalyLogMonitorTimer = new System.Threading.Timer(_ => TryDetectLogBurstTick(false), null, 1000, 1000); - System.Console.WriteLine("[ElysiumModMenu] Background freeze/overload log monitor started."); - } - catch (Exception ex) - { - System.Console.WriteLine($"[ElysiumModMenu] Failed to start background log monitor: {ex.GetType().Name}: {ex.Message}"); - } - } - - private static void StopBackgroundAnomalyLogMonitor() - { - try - { - anomalyLogMonitorTimer?.Dispose(); - anomalyLogMonitorTimer = null; - } - catch { } - } - - private static float GetLogMonitorSeconds() - { - try { return (float)(DateTime.UtcNow - logMonitorStartedUtc).TotalSeconds; } - catch { return 0f; } - } - - private static string BuildAnomalyReportDetails(bool allowUnityAccess = true) - { - if (!allowUnityAccess) - return anomalyReportDetailsCache + "\nmonitor=background"; - - string clientId = "Unknown"; - string networkMode = "Unknown"; - string inGame = "no"; - string host = "no"; - string platform = "Unknown"; - - try - { - if (AmongUsClient.Instance != null) - { - clientId = AmongUsClient.Instance.ClientId.ToString(); - networkMode = AmongUsClient.Instance.NetworkMode.ToString(); - host = AmongUsClient.Instance.AmHost ? "yes" : "no"; - - ClientData client = AmongUsClient.Instance.GetClientFromCharacter(PlayerControl.LocalPlayer); - if (client != null) - { - platform = GetPlatform(client); - } - } - } - catch { } - - try { inGame = ShipStatus.Instance != null && LobbyBehaviour.Instance == null ? "yes" : "no"; } catch { } - - string details = $"sessionId={relaySessionId}\nclientId={clientId}\nnetworkMode={networkMode}\nhost={host}\nplatform={platform}\ninGame={inGame}\nmonitor=unity"; - anomalyReportDetailsCache = details; - return details; - } - - private static void TryDetectLogBurstTick(bool allowUnityAccess = true) - { - if (!enableAnomalyLogReports) return; - lock (anomalyLogMonitorLock) - { - try - { - float now = GetLogMonitorSeconds(); - if (now < logMonitorNextScanAt) return; - logMonitorNextScanAt = now + LogBurstScanIntervalSeconds; - - List watchedFiles = GetWatchedLogFiles().ToList(); - if (!anomalyLogWatchNotified) - { - anomalyLogWatchNotified = true; - System.Console.WriteLine($"[ElysiumModMenu] Freeze/overload log reporting is enabled. Watching: {string.Join(", ", watchedFiles.Select(System.IO.Path.GetFileName).ToArray())}. Sends only summary counters when error/red logs appear or {LogBurstLineThreshold}+ new lines arrive within {LogBurstWindowSeconds:0}s."); - } - - int newLines = 0; - int errorLines = 0; - int storedMsgLines = 0; - List touchedLogs = new List(); - List touchedLogFiles = new List(); - foreach (string file in watchedFiles) - { - List addedLines = ReadNewLogLines(file, out int currentLines); - if (!watchedLogLineCounts.TryGetValue(file, out int previousLines)) - { - watchedLogLineCounts[file] = currentLines; - addedLines = ReadInitialRecentLogTail(file); - if (addedLines.Count <= 0) continue; - } - else - { - watchedLogLineCounts[file] = currentLines; - } - - if (addedLines.Count <= 0) continue; - - newLines += addedLines.Count; - touchedLogs.Add(System.IO.Path.GetFileName(file)); - touchedLogFiles.Add(file); - errorLines += addedLines.Count(IsErrorLogLine); - storedMsgLines += addedLines.Count(IsStoredMessageOverloadLine); - } - - if (newLines <= 0) return; - - if (logBurstWindowStartedAt < 0f || now - logBurstWindowStartedAt > LogBurstWindowSeconds) - { - logBurstWindowStartedAt = now; - logBurstLineCount = 0; - } - - logBurstLineCount += newLines; - bool isStoredRpcBurst = storedMsgLines > 0; - bool isErrorBurst = errorLines > 0; - bool isLineBurst = logBurstLineCount >= LogBurstLineThreshold; - if ((!isErrorBurst && !isLineBurst) || (!isStoredRpcBurst && now < logBurstCooldownUntil)) return; - - logBurstCooldownUntil = now + (isStoredRpcBurst ? 5f : LogBurstAlertCooldownSeconds); - string reason = isStoredRpcBurst ? "stored rpc overload detected" : (isErrorBurst ? "error/red log detected" : "log spam detected"); - string message = $"{BuildAnomalyReportDetails(allowUnityAccess)}\nnewLogLines={logBurstLineCount}\nerrorLines={errorLines}\nstoredMsgLines={storedMsgLines}\nwindowSeconds={LogBurstWindowSeconds}\nthreshold={LogBurstLineThreshold}\nreason={reason}, needs fix\nwatchedLogs={string.Join(", ", touchedLogs.Distinct().ToArray())}"; - SendAnomalyAlert(isErrorBurst ? "Abnormal error log" : "Abnormal log spam", message, "abnormal-log-spam", !allowUnityAccess, touchedLogFiles); - logBurstWindowStartedAt = -1f; - logBurstLineCount = 0; - } - catch (Exception ex) - { - System.Console.WriteLine($"[ElysiumModMenu] Log monitor failed: {ex.GetType().Name}: {ex.Message}"); - } - } - } - - private static IEnumerable GetWatchedLogFiles() - { - string root = GetAmongUsRoot(); - List files = new List(); - - try - { - string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - string unityLogRoot = System.IO.Path.Combine(userProfile, "AppData", "LocalLow", "Innersloth", "Among Us"); - AddLogPath(files, System.IO.Path.Combine(unityLogRoot, "Player.log")); - AddLogPath(files, System.IO.Path.Combine(unityLogRoot, "Player-prev.log")); - } - catch { } - - AddLogPath(files, System.IO.Path.Combine(root, "BepInEx", "ErrorLog.log")); - AddLogPath(files, System.IO.Path.Combine(root, "ErrorLog.log")); - AddLogPath(files, System.IO.Path.Combine(root, "BepInEx", "LogOutput.log")); - AddLogPath(files, System.IO.Path.Combine(root, "LogOutput.log")); - - try - { - string[] banLogDirs = - { - System.IO.Path.Combine(root, "BepInEx", "BAN_DATA", "LOG"), - System.IO.Path.Combine(root, "BAN_DATA", "LOG") - }; - - foreach (string banLogDir in banLogDirs) - { - if (!System.IO.Directory.Exists(banLogDir)) continue; - foreach (string file in System.IO.Directory.GetFiles(banLogDir)) - AddLogPath(files, file); - } - } - catch { } - - try - { - string playerLogRoot = System.IO.Path.Combine(root, "ElysiumModMenu", "PlayerLogs"); - if (System.IO.Directory.Exists(playerLogRoot)) - { - foreach (string dir in System.IO.Directory.GetDirectories(playerLogRoot)) - { - AddLogPath(files, System.IO.Path.Combine(dir, "LogOutput.txt")); - AddLogPath(files, System.IO.Path.Combine(dir, "LogOutput.log")); - } - } - } - catch { } - - return files; - } - - private static void AddLogPath(List files, string file) - { - try - { - if (!string.IsNullOrWhiteSpace(file) && System.IO.File.Exists(file) && !files.Contains(file)) - files.Add(file); - } - catch { } - } - - private static List ReadNewLogLines(string file, out int currentLines) - { - currentLines = 0; - List lines = new List(); - try - { - if (string.IsNullOrWhiteSpace(file) || !System.IO.File.Exists(file)) return lines; - - watchedLogLineCounts.TryGetValue(file, out int previousLines); - using (System.IO.FileStream stream = new System.IO.FileStream(file, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete)) - using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8, true)) - { - string line; - while ((line = reader.ReadLine()) != null) - { - currentLines++; - if (currentLines > previousLines) - lines.Add(line); - } - } - } - catch - { - } - - return lines; - } - - private static List ReadInitialRecentLogTail(string file) - { - List result = new List(); - try - { - if (string.IsNullOrWhiteSpace(file) || !System.IO.File.Exists(file)) return result; - - Queue errorTail = new Queue(); - using (System.IO.FileStream stream = new System.IO.FileStream(file, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete)) - using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8, true)) - { - string line; - while ((line = reader.ReadLine()) != null) - { - if (!IsErrorLogLine(line)) continue; - errorTail.Enqueue(line); - while (errorTail.Count > InitialLogTailLineLimit) - errorTail.Dequeue(); - } - } - - result.AddRange(errorTail); - } - catch { } - - return result; - } - - private static bool IsErrorLogLine(string line) - { - if (string.IsNullOrWhiteSpace(line)) return false; - string lower = line.ToLowerInvariant(); - if (lower.Contains("[elysiummodmenu]")) return false; - if (lower.Contains("method ") && lower.Contains(" has unsupported ") && lower.Contains("elysiummodmenu")) return false; - if (lower.Contains("registered mono type") && lower.Contains("elysiummodmenu")) return false; - return lower.Contains("[error") || - lower.Contains("[fatal") || - lower.Contains("exception") || - lower.Contains(" stack trace") || - lower.Contains("traceback") || - lower.Contains("stored data") || - lower.Contains("storeddata") || - IsStoredMessageOverloadLine(lower) || - IsKnownSpamWarningLine(lower) || - lower.Contains("overload") || - lower.Contains("freeze") || - lower.Contains("color=red") || - lower.Contains("#ff0000"); - } - - public static bool IsRelevantAnomalyLine(string line) - { - if (string.IsNullOrWhiteSpace(line)) return false; - string lower = line.ToLowerInvariant(); - if (lower.Contains("[elysiummodmenu]")) return false; - if (lower.Contains("bepinex") && lower.Contains("chainloader")) return false; - if (lower.Contains("registered mono type") && lower.Contains("elysiummodmenu")) return false; - if (lower.Contains("method ") && lower.Contains(" has unsupported ") && lower.Contains("elysiummodmenu")) return false; - return IsStoredMessageOverloadLine(lower) || - IsKnownSpamWarningLine(lower) || - lower.Contains("[error") || - lower.Contains("[fatal") || - lower.Contains("nullreferenceexception") || - lower.Contains("invaliddataexception") || - lower.Contains("exception:") || - lower.Contains(" stack trace") || - lower.Contains("traceback") || - lower.Contains("stored data") || - lower.Contains("storeddata"); - } - - private static bool IsKnownSpamWarningLine(string line) - { - if (string.IsNullOrWhiteSpace(line)) return false; - string lower = line.ToLowerInvariant(); - return lower.Contains("sendmode set to everything") || - lower.Contains("likely should be reliable") || - lower.Contains("stored msg"); - } - - private static bool IsStoredMessageOverloadLine(string line) - { - if (string.IsNullOrWhiteSpace(line)) return false; - string lower = line.ToLowerInvariant(); - return lower.Contains("stored msg") && lower.Contains(" rpc "); - } - - private static string GetAmongUsRoot() - { - try { return System.IO.Directory.GetCurrentDirectory(); } - catch { return string.Empty; } - } - - private static string EscapeJson(string value) - { - if (string.IsNullOrEmpty(value)) return string.Empty; - - StringBuilder builder = new StringBuilder(value.Length + 16); - foreach (char c in value) - { - switch (c) - { - case '\\': builder.Append("\\\\"); break; - case '"': builder.Append("\\\""); break; - case '\n': builder.Append("\\n"); break; - case '\r': builder.Append("\\r"); break; - case '\t': builder.Append("\\t"); break; - default: - if (c < 32) builder.Append("\\u").Append(((int)c).ToString("x4")); - else builder.Append(c); - break; - } - } - - return builder.ToString(); - } - - private void TrySendDiscordLaunchStatusTick() - { - if (discordLaunchStatusSent || !DiscordStatusReporter.IsEnabled) return; - if (Time.unscaledTime < discordLaunchStatusNextTryAt) return; - - string webhookUrl = DiscordStatusReporter.ConfiguredWebhookUrl.Trim(); - if (!DiscordStatusReporter.IsValidWebhookUrl(webhookUrl)) - { - discordLaunchStatusNextTryAt = Time.unscaledTime + 10f; - if (!discordInvalidWebhookNotified) - { - discordInvalidWebhookNotified = true; - } - return; - } - - if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) - { - discordLaunchStatusNextTryAt = Time.unscaledTime + 2f; - return; - } - - string nickname = "Unknown"; - string friendCode = "Hidden"; - string puid = "Unknown"; - string platform = "Unknown"; - string roomCode = "No room"; - int level = 0; - - try - { - nickname = string.IsNullOrWhiteSpace(PlayerControl.LocalPlayer.Data.PlayerName) - ? "Unknown" - : PlayerControl.LocalPlayer.Data.PlayerName; - if (DiscordStatusReporter.IncludeLocalPuid) - friendCode = GetDisplayedFriendCode(PlayerControl.LocalPlayer.Data, "Hidden"); - - uint rawLevel = PlayerControl.LocalPlayer.Data.PlayerLevel; - if (rawLevel != uint.MaxValue && rawLevel < 10000) level = (int)rawLevel + 1; - } - catch { } - - try - { - ClientData client = AmongUsClient.Instance?.GetClientFromCharacter(PlayerControl.LocalPlayer); - if (client != null) - { - puid = GetClientPuid(client); - platform = GetPlatform(client); - } - } - catch { } - - try - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.GameId != 0) - roomCode = AmongUsClient.Instance.GameId.ToString(); - } - catch { } - - DiscordStatusReporter.SendLaunchStatus(webhookUrl, nickname, friendCode, puid, platform, level, roomCode, DiscordStatusReporter.IncludeLocalPuid); - discordLaunchStatusSent = true; - } - - public static KeyCode bindMagnetCursor = KeyCode.F9; - public static bool isWaitBindMagnetCursor = false; - - private bool CanRunHostBind(string actionName) - { - try - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) return true; - } - catch { } - - ShowNotification($"[BIND] {actionName}: host only"); - return false; - } - - public void Update() - { - bool isTypingOrBinding = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingGhostChatColor || isEditingBan || customChatInputFocused || - isWaitingForBind || isWaitBindMassMorph || isWaitBindSpawnLobby || - isWaitBindDespawnLobby || isWaitBindCloseMeeting || isWaitBindInstaStart || - isWaitBindEndCrew || isWaitBindEndImp || isWaitBindEndImpDC || isWaitBindEndHnsDC || - isWaitBindMagnetCursor || isWaitBindToggleTracers || isWaitBindToggleNoClip || - isWaitBindToggleFreecam || isWaitBindToggleCameraZoom || isWaitBindKillAll || - isWaitBindCallMeeting || isWaitBindTogglePlayerInfo || isWaitBindToggleSeeRoles || - isWaitBindToggleSeeGhosts || isWaitBindToggleFullBright || isWaitBindKickAll || - isWaitBindFixSabotages || isWaitBindSetAllGhost || isWaitBindSetAllGhostImp; - - KeyCode activeMenuKey = menuToggleKey == KeyCode.None ? KeyCode.Insert : menuToggleKey; - if (!isTypingOrBinding && Input.GetKeyDown(activeMenuKey)) - { - showMenu = !showMenu; - if (!showMenu) SaveConfig(); - } - - if (!isTypingOrBinding) - { - if (bindMassMorph != KeyCode.None && Input.GetKeyDown(bindMassMorph)) - { - if (CanRunHostBind("Mass Morph")) - this.StartCoroutine(MassMorphCoroutine().WrapToIl2Cpp()); - } - - if (bindSpawnLobby != KeyCode.None && Input.GetKeyDown(bindSpawnLobby)) - { - if (CanRunHostBind("Spawn Lobby")) SpawnLobby(); - } - - if (bindDespawnLobby != KeyCode.None && Input.GetKeyDown(bindDespawnLobby)) - { - if (CanRunHostBind("Despawn Lobby")) DespawnLobby(); - } - - if (bindCloseMeeting != KeyCode.None && Input.GetKeyDown(bindCloseMeeting)) - { - if (CanRunHostBind("Close Meeting") && MeetingHud.Instance != null) - MeetingHud.Instance.RpcClose(); - } - - if (bindInstaStart != KeyCode.None && Input.GetKeyDown(bindInstaStart) && CanRunHostBind("Insta Start") && GameStartManager.Instance != null) - { - GameStartManager.Instance.startState = GameStartManager.StartingStates.Countdown; - GameStartManager.Instance.countDownTimer = 0f; - } - if (bindMagnetCursor != KeyCode.None && Input.GetKeyDown(bindMagnetCursor)) - { - autoFollowCursor = !autoFollowCursor; - ShowNotification(autoFollowCursor ? - "[MAGNET] Magnet Cursor: ON" : - "[MAGNET] Magnet Cursor: OFF"); - } - if (bindEndCrew != KeyCode.None && Input.GetKeyDown(bindEndCrew) && CanRunHostBind("End: Crewmate Win")) SmartEndGame("CrewWin"); - if (bindEndImp != KeyCode.None && Input.GetKeyDown(bindEndImp) && CanRunHostBind("End: Impostor Win")) SmartEndGame("ImpWin"); - if (bindEndImpDC != KeyCode.None && Input.GetKeyDown(bindEndImpDC) && CanRunHostBind("End: Imp Disconnect")) SmartEndGame("ImpDisconnect"); - if (bindEndHnsDC != KeyCode.None && Input.GetKeyDown(bindEndHnsDC) && CanRunHostBind("End: H&S Disconnect")) SmartEndGame("HnsImpDisconnect"); - if (bindToggleTracers != KeyCode.None && Input.GetKeyDown(bindToggleTracers)) - { - showTracers = !showTracers; - ShowNotification(showTracers ? "[TRACERS] ON" : "[TRACERS] OFF"); - } - if (bindToggleNoClip != KeyCode.None && Input.GetKeyDown(bindToggleNoClip)) - { - noClip = !noClip; - ShowNotification(noClip ? "[NOCLIP] ON" : "[NOCLIP] OFF"); - } - if (bindToggleFreecam != KeyCode.None && Input.GetKeyDown(bindToggleFreecam)) - { - freecam = !freecam; - ShowNotification(freecam ? "[FREECAM] ON" : "[FREECAM] OFF"); - } - if (bindToggleCameraZoom != KeyCode.None && Input.GetKeyDown(bindToggleCameraZoom)) - { - cameraZoom = !cameraZoom; - ShowNotification(cameraZoom ? "[ZOOM] ON" : "[ZOOM] OFF"); - } - if (bindTogglePlayerInfo != KeyCode.None && Input.GetKeyDown(bindTogglePlayerInfo)) - { - showPlayerInfo = !showPlayerInfo; - ShowNotification(showPlayerInfo ? "[PLAYER INFO] ON" : "[PLAYER INFO] OFF"); - } - if (bindToggleSeeRoles != KeyCode.None && Input.GetKeyDown(bindToggleSeeRoles)) - { - seeRoles = !seeRoles; - ShowNotification(seeRoles ? "[ROLES] ON" : "[ROLES] OFF"); - } - if (bindToggleSeeGhosts != KeyCode.None && Input.GetKeyDown(bindToggleSeeGhosts)) - { - seeGhosts = !seeGhosts; - ShowNotification(seeGhosts ? "[GHOSTS] ON" : "[GHOSTS] OFF"); - } - if (bindToggleFullBright != KeyCode.None && Input.GetKeyDown(bindToggleFullBright)) - { - fullBright = !fullBright; - ShowNotification(fullBright ? "[FULL BRIGHT] ON" : "[FULL BRIGHT] OFF"); - } - if (bindKillAll != KeyCode.None && Input.GetKeyDown(bindKillAll) && CanRunHostBind("Kill All")) KillAll(); - if (bindCallMeeting != KeyCode.None && Input.GetKeyDown(bindCallMeeting) && CanRunHostBind("Call Meeting")) callMeetingPublic(); - if (bindKickAll != KeyCode.None && Input.GetKeyDown(bindKickAll) && CanRunHostBind("Kick All")) KickAll(); - if (bindFixSabotages != KeyCode.None && Input.GetKeyDown(bindFixSabotages) && CanRunHostBind("Fix Sabotages")) FixAllSabotages(); - if (bindSetAllGhost != KeyCode.None && Input.GetKeyDown(bindSetAllGhost) && CanRunHostBind("All -> Ghost")) SetAllPlayersGhost(); - if (bindSetAllGhostImp != KeyCode.None && Input.GetKeyDown(bindSetAllGhostImp) && CanRunHostBind("All -> Ghost Imp")) SetAllPlayersGhost(true); - } - - ElysiumAutoHostService.Tick(); - ElysiumAutoLobbyReturn.UpdateLogic(); - ApplyFpsLimit(); - TryAutoGhostAfterStartTick(); - TryAutoBanCustomPlatformsTick(); - TrySendDiscordLaunchStatusTick(); - TryDetectLogBurstTick(); - if (votekickEveryone) - { - TickVotekickEveryoneRun(); - } - if (stylesInited && rgbMenuMode) - { - rgbMenuHue += Time.deltaTime * 0.2f; - if (rgbMenuHue > 1f) rgbMenuHue -= 1f; - UpdateAccentColor(Color.HSVToRGB(rgbMenuHue, 1f, 1f)); - } - - if (wasShowMenu && !showMenu) SaveConfig(); - wasShowMenu = showMenu; - - if (PlayerControl.LocalPlayer != null) - { - TryHostOnlyKillAuraTick(); - TryAutoBanBrokenFriendCodeTick(); - - if (enableLocalNameSpoof && !isEditingName) - { - localNameRefreshTimer += Time.deltaTime; - if (localNameRefreshTimer >= 0.5f) - { - localNameRefreshTimer = 0f; - ApplyLocalNameSelf(customNameInput, false); - } - } - else - { - localNameRefreshTimer = 0f; - } - - if (enableLocalFriendCodeSpoof && !isEditingLocalFriendCode) - { - localFriendCodeRefreshTimer += Time.deltaTime; - if (localFriendCodeRefreshTimer >= 0.5f) - { - localFriendCodeRefreshTimer = 0f; - ApplyLocalFriendCodeSelf(localFriendCodeInput, false); - } - } - else - { - localFriendCodeRefreshTimer = 0f; - } - - if ((tpToCursor && Input.GetMouseButtonDown(1)) || - (dragToCursor && Input.GetMouseButton(2)) || - autoFollowCursor) - { - if (Camera.main != null) - { - Vector3 mPos = Camera.main.ScreenToWorldPoint(Input.mousePosition); - mPos.z = PlayerControl.LocalPlayer.transform.position.z; - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(mPos); - } - } - try - { - if (noTaskMode && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - if (GameOptionsManager.Instance != null && GameOptionsManager.Instance.CurrentGameOptions != null) - { - var opts = GameOptionsManager.Instance.CurrentGameOptions; - opts.SetInt(Int32OptionNames.NumCommonTasks, 0); - opts.SetInt(Int32OptionNames.NumLongTasks, 0); - opts.SetInt(Int32OptionNames.NumShortTasks, 0); - } - } - } - catch { } - if (autoChatEveryone && pendingAutoMeeting && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - try - { - if (PlayerControl.LocalPlayer != null && ShipStatus.Instance != null && !PlayerControl.LocalPlayer.Data.IsDead) - { - autoMeetingTimer += Time.deltaTime; - - if (autoMeetingTimer >= autoChatEveryoneDelay) - { - if (MeetingHud.Instance == null) - { - PlayerControl.LocalPlayer.CmdReportDeadBody(null); - } - else - { - MeetingHud.Instance.RpcClose(); - pendingAutoMeeting = false; - autoMeetingTimer = 0f; - ShowNotification("[CHAT EVERYONE] Игроки собраны в кафетерии!"); - } - } - } - } - catch { } - } - - if (customChatSpamEnabled) - { - customChatSpamTimer += Time.deltaTime; - if (customChatSpamTimer >= customChatSpamDelay) - { - customChatSpamTimer = 0f; - TrySendCustomChatMessage(customChatMessage); - } - } - else - { - customChatSpamTimer = 0f; - } - if (autoKickBugs && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && fortegreenTimer.Count > 0) - { - foreach (var kvp in fortegreenTimer.ToList()) - { - if (Time.time >= kvp.Value) - { - byte pid = kvp.Key; - var player = GameData.Instance.GetPlayerById(pid); - - if (player != null && !player.Disconnected && player.Object != null) - { - int currentColor = (int)player.DefaultOutfit.ColorId; - if (currentColor == 18 || currentColor >= Palette.PlayerColors.Length) - { - AmongUsClient.Instance.KickPlayer(player.ClientId, false); - ShowNotification($"[AUTO-KICK] Игрок {player.PlayerName} кикнут (Баг цвета)!"); - } - } - fortegreenTimer.Remove(pid); - } - } - } - if (PlayerControl.LocalPlayer != null) - { - try - { - if (AnimAsteroidsEnabled) - { - PlayerControl.LocalPlayer.PlayAnimation((byte)TaskTypes.ClearAsteroids); - RpcPlayAnimationMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, (byte)TaskTypes.ClearAsteroids); - AmongUsClient.Instance.LateBroadcastUnreliableMessage(Unsafe.As(rpcMessage)); - } - - if (AnimShieldsEnabled) - { - PlayerControl.LocalPlayer.PlayAnimation((byte)TaskTypes.PrimeShields); - RpcPlayAnimationMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, (byte)TaskTypes.PrimeShields); - AmongUsClient.Instance.LateBroadcastUnreliableMessage(Unsafe.As(rpcMessage)); - } - - if (IsScanning && !isScannerActiveFlag) - { - var count = ++PlayerControl.LocalPlayer.scannerCount; - PlayerControl.LocalPlayer.SetScanner(true, count); - RpcSetScannerMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, true, count); - AmongUsClient.Instance.LateBroadcastReliableMessage(Unsafe.As(rpcMessage)); - isScannerActiveFlag = true; - } - else if (!IsScanning && isScannerActiveFlag) - { - var count = ++PlayerControl.LocalPlayer.scannerCount; - PlayerControl.LocalPlayer.SetScanner(false, count); - RpcSetScannerMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, false, count); - AmongUsClient.Instance.LateBroadcastReliableMessage(Unsafe.As(rpcMessage)); - isScannerActiveFlag = false; - } - - if (ShipStatus.Instance != null) - { - if (AnimCamsInUseEnabled && !isCamsActiveFlag) - { - ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Security, 1); - isCamsActiveFlag = true; - } - else if (!AnimCamsInUseEnabled && isCamsActiveFlag) - { - ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Security, 0); - isCamsActiveFlag = false; - } - } - } - catch { } - } - try - { - if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.MyPhysics != null && PlayerControl.LocalPlayer.Data != null) - { - if (PlayerControl.LocalPlayer.Collider != null) - { - PlayerControl.LocalPlayer.Collider.enabled = !(noClip || PlayerControl.LocalPlayer.onLadder); - } - - float baseSpeed = 3f; - float targetSpeed = walkSpeed * baseSpeed; - - if (PlayerControl.LocalPlayer.Data.IsDead) - { - PlayerControl.LocalPlayer.MyPhysics.GhostSpeed = targetSpeed; - } - else - { - PlayerControl.LocalPlayer.MyPhysics.Speed = targetSpeed; - } - } - } - catch { } - - if (SpoofMenuEnabled && PlayerControl.LocalPlayer != null) - { - uiSpoofTimer += Time.deltaTime; - if (uiSpoofTimer >= rpcSpoofDelay) - { - uiSpoofTimer = 0f; - byte rpc = spoofMenuRPCs[selectedSpoofMenuIndex]; - try - { - MessageWriter msg = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, rpc, SendOption.None, -1); - AmongUsClient.Instance.FinishRpcImmediately(msg); - } - catch { } - } - } - try - { - if (autoBanEnabled && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) - { - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc.Data == null || pc.Data.Disconnected || pc == PlayerControl.LocalPlayer) continue; - - string fc = pc.Data.FriendCode; - if (!string.IsNullOrEmpty(fc)) - { - foreach (var entry in bannedEntries) - { - string[] parts = entry.Split('|'); - if (parts.Length > 0 && parts[0].Trim().ToLower() == fc.Trim().ToLower()) - { - AmongUsClient.Instance.KickPlayer(pc.OwnerId, true); - break; - } - } - } - } - } - } - catch { } - if (freecam) - { - if (!_freecamActive && Camera.main != null) - { - var cam = Camera.main.gameObject.GetComponent(); - if (cam != null) { cam.enabled = false; cam.Target = null; } - _freecamActive = true; - } - if (PlayerControl.LocalPlayer != null) PlayerControl.LocalPlayer.moveable = false; - Vector3 movement = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0.0f); - if (Camera.main != null) Camera.main.transform.position += movement * 15f * Time.deltaTime; - } - else if (_freecamActive) - { - if (PlayerControl.LocalPlayer != null) PlayerControl.LocalPlayer.moveable = true; - if (Camera.main != null) - { - var cam = Camera.main.gameObject.GetComponent(); - if (cam != null && PlayerControl.LocalPlayer != null) { cam.enabled = true; cam.SetTarget(PlayerControl.LocalPlayer); } - } - _freecamActive = false; - } - - try - { - if (cameraZoom && Camera.main != null && Input.GetAxis("Mouse ScrollWheel") != 0f) - { - if (Input.GetAxis("Mouse ScrollWheel") < 0f) Camera.main.orthographicSize += 0.5f; - else if (Input.GetAxis("Mouse ScrollWheel") > 0f && Camera.main.orthographicSize > 3f) Camera.main.orthographicSize -= 0.5f; - } - } - catch { } - - try - { - if (rainbowPlayers.Count > 0 && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) - { - colorTimer += Time.deltaTime; - if (colorTimer > 0.15f) - { - colorTimer = 0f; - currentColorId++; - if (currentColorId > 17) currentColorId = 0; - foreach (var p in PlayerControl.AllPlayerControls) - if (p != null && p.Data != null && !p.Data.Disconnected && rainbowPlayers.Contains(p.PlayerId)) - p.RpcSetColor(currentColorId); - } - } - } - catch { } - try - { - if (PlayerControl.AllPlayerControls != null) - { - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null) HandleTracer(pc, showTracers); - } - } - } - catch { } - - - - if (!isEditingLevel && uint.TryParse(spoofLevelString, out uint parsedLvl)) - { - uint targetLevel = parsedLvl > 0 ? parsedLvl - 1 : 0; - try - { - if (AmongUs.Data.DataManager.Player.stats.level != targetLevel) - { - AmongUs.Data.DataManager.Player.stats.level = targetLevel; - } - } - catch - { - try - { - if (AmongUs.Data.DataManager.Player.Stats.Level != targetLevel) - { - AmongUs.Data.DataManager.Player.Stats.Level = targetLevel; - } - } - catch { } - } - } - try - { - if (localRainbow || rainbowPlayers.Count > 0) - { - colorTimer += Time.deltaTime; - if (colorTimer > 0.15f) - { - colorTimer = 0f; - currentColorId++; - if (currentColorId > 17) currentColorId = 0; - - if (localRainbow && PlayerControl.LocalPlayer != null) - PlayerControl.LocalPlayer.CmdCheckColor(currentColorId); - - if (rainbowPlayers.Count > 0 && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) - { - foreach (var p in PlayerControl.AllPlayerControls) - { - if (p != null && p.Data != null && !p.Data.Disconnected && rainbowPlayers.Contains(p.PlayerId)) - p.RpcSetColor(currentColorId); - } - } - } - } - } - - - catch { } - - - } - } - public static void ForceSetScanner(PlayerControl player, bool toggle) - { - var count = ++player.scannerCount; - player.SetScanner(toggle, count); - RpcSetScannerMessage rpcMessage = new(player.NetId, toggle, count); - AmongUsClient.Instance.LateBroadcastReliableMessage(Unsafe.As(rpcMessage)); - } - public static void ForcePlayAnimation(byte animationType) - { - PlayerControl.LocalPlayer.PlayAnimation(animationType); - RpcPlayAnimationMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, animationType); - AmongUsClient.Instance.LateBroadcastUnreliableMessage(Unsafe.As(rpcMessage)); - } - - public void OnGUI() - { - Event e = Event.current; - - bool isTyping = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingGhostChatColor || isEditingBan; - bool isBinding = isWaitingForBind || isWaitBindMassMorph || isWaitBindSpawnLobby || isWaitBindDespawnLobby || - isWaitBindCloseMeeting || isWaitBindInstaStart || isWaitBindEndCrew || isWaitBindEndImp || - isWaitBindEndImpDC || isWaitBindEndHnsDC || isWaitBindMagnetCursor || isWaitBindToggleTracers || - isWaitBindToggleNoClip || isWaitBindToggleFreecam || isWaitBindToggleCameraZoom || - isWaitBindKillAll || isWaitBindCallMeeting || isWaitBindTogglePlayerInfo || - isWaitBindToggleSeeRoles || isWaitBindToggleSeeGhosts || isWaitBindToggleFullBright || - isWaitBindKickAll || isWaitBindFixSabotages || isWaitBindSetAllGhost || - isWaitBindSetAllGhostImp; - - if (e != null && e.isKey && e.type == EventType.KeyDown) - { - if (e.keyCode == KeyCode.Escape) - { - isEditingName = isEditingLevel = isEditingFriendCode = isEditingLocalFriendCode = isEditingGhostChatColor = isEditingBan = false; - ResetAllBindWaits(); - e.Use(); - } - else if (isBinding && e.keyCode != KeyCode.None) - { - if (isWaitingForBind) { menuToggleKey = e.keyCode; } - else if (isWaitBindMassMorph) { bindMassMorph = e.keyCode; } - else if (isWaitBindSpawnLobby) { bindSpawnLobby = e.keyCode; } - else if (isWaitBindDespawnLobby) { bindDespawnLobby = e.keyCode; } - else if (isWaitBindCloseMeeting) { bindCloseMeeting = e.keyCode; } - else if (isWaitBindInstaStart) { bindInstaStart = e.keyCode; } - else if (isWaitBindEndCrew) { bindEndCrew = e.keyCode; } - else if (isWaitBindEndImp) { bindEndImp = e.keyCode; } - else if (isWaitBindEndImpDC) { bindEndImpDC = e.keyCode; } - else if (isWaitBindEndHnsDC) { bindEndHnsDC = e.keyCode; } - else if (isWaitBindMagnetCursor) { bindMagnetCursor = e.keyCode; } - else if (isWaitBindToggleTracers) { bindToggleTracers = e.keyCode; } - else if (isWaitBindToggleNoClip) { bindToggleNoClip = e.keyCode; } - else if (isWaitBindToggleFreecam) { bindToggleFreecam = e.keyCode; } - else if (isWaitBindToggleCameraZoom) { bindToggleCameraZoom = e.keyCode; } - else if (isWaitBindKillAll) { bindKillAll = e.keyCode; } - else if (isWaitBindCallMeeting) { bindCallMeeting = e.keyCode; } - else if (isWaitBindTogglePlayerInfo) { bindTogglePlayerInfo = e.keyCode; } - else if (isWaitBindToggleSeeRoles) { bindToggleSeeRoles = e.keyCode; } - else if (isWaitBindToggleSeeGhosts) { bindToggleSeeGhosts = e.keyCode; } - else if (isWaitBindToggleFullBright) { bindToggleFullBright = e.keyCode; } - else if (isWaitBindKickAll) { bindKickAll = e.keyCode; } - else if (isWaitBindFixSabotages) { bindFixSabotages = e.keyCode; } - else if (isWaitBindSetAllGhost) { bindSetAllGhost = e.keyCode; } - else if (isWaitBindSetAllGhostImp) { bindSetAllGhostImp = e.keyCode; } - - ResetAllBindWaits(); - SaveConfig(); - e.Use(); - } - else if (isTyping) - { - if (isEditingBan && HandleClipboardShortcut(e, ref banInput)) { } - else if (isEditingName && HandleClipboardShortcut(e, ref customNameInput)) { } - else if (isEditingLevel && HandleClipboardShortcut(e, ref spoofLevelString)) { } - else if (isEditingFriendCode && HandleClipboardShortcut(e, ref spoofFriendCodeInput)) { } - else if (isEditingLocalFriendCode && HandleClipboardShortcut(e, ref localFriendCodeInput)) { } - else if (isEditingGhostChatColor && HandleClipboardShortcut(e, ref ghostChatColorHex, 7)) { ghostChatColorHex = FilterHexInput(ghostChatColorHex, 7); } - else if (e.keyCode == KeyCode.Backspace) - { - if (isEditingBan && banInput.Length > 0) { banInput = banInput.Substring(0, banInput.Length - 1); } - if (isEditingName && customNameInput.Length > 0) { customNameInput = customNameInput.Substring(0, customNameInput.Length - 1); } - if (isEditingLevel && spoofLevelString.Length > 0) { spoofLevelString = spoofLevelString.Substring(0, spoofLevelString.Length - 1); } - if (isEditingFriendCode && spoofFriendCodeInput.Length > 0) { spoofFriendCodeInput = spoofFriendCodeInput.Substring(0, spoofFriendCodeInput.Length - 1); } - if (isEditingLocalFriendCode && localFriendCodeInput.Length > 0) { localFriendCodeInput = localFriendCodeInput.Substring(0, localFriendCodeInput.Length - 1); } - if (isEditingGhostChatColor && ghostChatColorHex.Length > 0) { ghostChatColorHex = ghostChatColorHex.Substring(0, ghostChatColorHex.Length - 1); } - e.Use(); - } - else if (e.character != 0 && e.character != '\n' && e.character != '\r') - { - if (isEditingBan) { banInput += e.character; } - if (isEditingName) { customNameInput += e.character; } - if (isEditingLevel) { spoofLevelString += e.character; } - if (isEditingFriendCode) { spoofFriendCodeInput += e.character; } - if (isEditingLocalFriendCode) { localFriendCodeInput += e.character; } - if (isEditingGhostChatColor) { ghostChatColorHex = FilterHexInput((ghostChatColorHex ?? "") + e.character, 7); } - e.Use(); - } - } - } - - if (Event.current.type == EventType.Layout) - { - lockedPlayersList.Clear(); - if (PlayerControl.AllPlayerControls != null) - { - foreach (var p in PlayerControl.AllPlayerControls) - { - if (p != null && p.Data != null && !p.Data.Disconnected && p.PlayerId < 100) - lockedPlayersList.Add(p); - } - } - - if (!stylesInited) InitStyles(); - - if (showMenu) - { - windowRect = GUI.Window(0, windowRect, (Action)DrawElysiumModMenu, "", windowStyle); - } - - for (int i = screenNotifications.Count - 1; i >= 0; i--) - { - screenNotifications[i].lifetime += Time.deltaTime; - if (screenNotifications[i].HasExpired) screenNotifications.RemoveAt(i); - } - } - - try - { - if (AmongUsClient.Instance != null && (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Joined || AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started)) - { - if (PlayerControl.AllPlayerControls != null) - { - List currentIds = new List(); - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc != null && pc.Data != null && !pc.Data.Disconnected) - { - currentIds.Add(pc.PlayerId); - UpsertPlayerHistory(pc); - } - } - - foreach (var id in currentIds) - { - if (!lastPlayerIds.Contains(id) && !pendingJoinTimers.ContainsKey(id)) - { - pendingJoinTimers[id] = 1.5f; - } - } - - var keysToProcess = pendingJoinTimers.Keys.ToList(); - foreach (var id in keysToProcess) - { - pendingJoinTimers[id] -= Time.deltaTime; - if (pendingJoinTimers[id] <= 0f) - { - pendingJoinTimers.Remove(id); - - var pc = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p != null && p.PlayerId == id); - if (pc != null && pc.Data != null && !pc.Data.Disconnected) - { - if (DetailedJoinInfo) - { - int level = 1; - try - { - uint rawLevel = pc.Data.PlayerLevel; - if (rawLevel != uint.MaxValue && rawLevel < 10000) level = (int)rawLevel + 1; - } - catch { } - - string platform = GetPlatform(AmongUsClient.Instance.GetClientFromCharacter(pc)); - string fc = GetDisplayedFriendCode(pc.Data); - - ShowNotification($"[+] {pc.Data.PlayerName} joined\nLvl: {level} | {platform} | FC: {fc}"); - } - else - { - ShowNotification($"[+] {pc.Data.PlayerName} присоединился"); - } - } - } - } - - foreach (var id in lastPlayerIds) - { - if (!currentIds.Contains(id)) - { - pendingJoinTimers.Remove(id); - MarkPlayerHistoryLeft(id); - } - } - - lastPlayerIds = new List(currentIds); - } - } - else - { - foreach (var id in lastPlayerIds) - MarkPlayerHistoryLeft(id); - lastPlayerIds.Clear(); - pendingJoinTimers.Clear(); - } - } - catch { } - if (screenNotifications.Count > 0) - { - int maxNotifs = 6; - int startIdx = Mathf.Max(0, screenNotifications.Count - maxNotifs); - for (int i = startIdx; i < screenNotifications.Count; i++) - { - ElysiumNotification notif = screenNotifications[i]; - int reverseIndex = screenNotifications.Count - 1 - i; - - float slideOffset = 0f; - float animSpeed = 0.3f; - float currentAlpha = 0.95f; - - if (notif.lifetime < animSpeed) - { - float t = Mathf.Clamp01(1f - (notif.lifetime / animSpeed)); - slideOffset = t * t * 300f; - } - else if (notif.lifetime > notif.ttl - animSpeed) - { - float t = Mathf.Clamp01((notif.lifetime - (notif.ttl - animSpeed)) / animSpeed); - slideOffset = t * t * 300f; - currentAlpha = Mathf.Lerp(0.95f, 0f, t); - } - - float xPos = (float)Screen.width - notificationBoxSize.x - 15f + slideOffset; - float yPos = Screen.height - 150f - (reverseIndex * (notificationBoxSize.y + 5f)); - - GUI.color = new Color(0.12f, 0.12f, 0.12f, currentAlpha); - GUI.Box(new Rect(xPos, yPos, notificationBoxSize.x, notificationBoxSize.y), "", windowStyle); - - GUI.color = new Color(1f, 1f, 1f, currentAlpha > 0.5f ? 1f : currentAlpha * 2f); - string accentHex = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); - - GUI.Label(new Rect(xPos + 10f, yPos + 5f, notificationBoxSize.x - 20f, 20f), $"{notif.title}"); - - float timeLeft = Mathf.Max(0, notif.ttl - notif.lifetime); - GUI.Label(new Rect(xPos + 10f, yPos + 5f, notificationBoxSize.x - 20f, 20f), $"{timeLeft:F1}s", new GUIStyle(GUI.skin.label) { alignment = TextAnchor.UpperRight, fontSize = 12, richText = true }); - GUI.Label(new Rect(xPos + 10f, yPos + 25f, notificationBoxSize.x - 20f, notificationBoxSize.y - 30f), notif.message, new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 12 }); - - float progress = 1f - (notif.lifetime / notif.ttl); - GUI.color = new Color(currentAccentColor.r, currentAccentColor.g, currentAccentColor.b, currentAlpha); - GUI.Box(new Rect(xPos + 8f, yPos + notificationBoxSize.y - 6f, (notificationBoxSize.x - 16f) * progress, 2f), "", safeLineStyle); - GUI.color = Color.white; - } - } - } - - public static bool votekickEveryone = false; - public static List votekickedPlayerIds = new List(); - private static bool votekickExitQueued = false; - private static float votekickExitAt = 0f; - private const float VotekickExitDelay = 0.45f; - private Vector2 votekickScrollPosition = Vector2.zero; - - private void StartVotekickEveryoneRun() - { - votekickEveryone = true; - votekickedPlayerIds.Clear(); - votekickExitQueued = false; - votekickExitAt = 0f; - ShowNotification("[AUTO-VOTEKICK] Armed. Join a room and votes will be sent automatically."); - } - - private void StopVotekickEveryoneRun(bool clearVotes = true) - { - votekickEveryone = false; - votekickExitQueued = false; - votekickExitAt = 0f; - if (clearVotes) votekickedPlayerIds.Clear(); - } - - private void TickVotekickEveryoneRun() - { - if (!votekickEveryone) return; - - if (votekickExitQueued) - { - if (Time.unscaledTime >= votekickExitAt) - LeaveRoomAfterVotekick(); - return; - } - - if (VoteBanSystem.Instance == null || PlayerControl.AllPlayerControls == null || AmongUsClient.Instance == null) - return; - - int sent = ExecuteVotekickEveryone(true); - if (sent <= 0) return; - - votekickExitQueued = true; - votekickExitAt = Time.unscaledTime + VotekickExitDelay; - ShowNotification($"[AUTO-VOTEKICK] Votes sent: {sent}. Leaving room..."); - } - - private int ExecuteVotekickEveryone(bool rememberTargets) - { - if (VoteBanSystem.Instance == null || PlayerControl.AllPlayerControls == null) return 0; - - int votesSent = 0; - try - { - foreach (PlayerControl pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc.AmOwner || pc.Data == null || pc.Data.Disconnected) continue; - - int clientId = pc.Data.ClientId; - - if (!rememberTargets || !votekickedPlayerIds.Contains(clientId)) - { - for (int i = 0; i < 3; i++) - { - VoteBanSystem.Instance.CmdAddVote(clientId); - votesSent++; - } - - if (rememberTargets) - votekickedPlayerIds.Add(clientId); - - ShowNotification($"[VOTEKICK] 3 votes sent to {pc.Data.PlayerName}."); - } - } - } - catch (Exception) - { - } - - return votesSent; - } - - private void SendVotekickEveryoneStay() - { - int sent = ExecuteVotekickEveryone(false); - if (sent > 0) - ShowNotification($"[VOTEKICK] Sent {sent} votes. Staying in room."); - else - ShowNotification("[VOTEKICK] No valid targets or VoteBanSystem is not ready."); - } - - private void LeaveRoomAfterVotekick() - { - try - { - votekickExitQueued = false; - votekickExitAt = 0f; - votekickedPlayerIds.Clear(); - - if (AmongUsClient.Instance != null) - { - AmongUsClient.Instance.ExitGame(DisconnectReasons.ExitGame); - ShowNotification("[AUTO-VOTEKICK] Left room. Auto mode is still armed."); - } - } - catch (Exception) - { - votekickExitQueued = false; - votekickExitAt = 0f; - } - } - - public static void ExecuteVotekickTarget(PlayerControl target) - { - if (target == null || target.Data == null || VoteBanSystem.Instance == null) return; - - try - { - int targetClientId = target.Data.ClientId; - - VoteBanSystem.Instance.CmdAddVote(targetClientId); - - - if (DestroyableSingleton.Instance != null && DestroyableSingleton.Instance.Notifier != null) - { - DestroyableSingleton.Instance.Notifier.AddDisconnectMessage("Votekick sent! Leave and rejoin 2 more times."); - } - - ShowNotification($"[VOTEKICK] Vote sent to {target.Data.PlayerName}!"); - } - catch (Exception) - { - } - } - - private void DrawVotekickTab() - { - GUILayout.BeginVertical(boxStyle); - try - { - GUIStyle voteInfoStyle = new GUIStyle(toggleLabelStyle) { richText = true, wordWrap = true }; - GUILayout.Label("VOTEKICK MENU", headerStyle); - GUILayout.Label("Auto mode: sends 3 votes to every valid player, leaves the room, and stays armed until you press it again.", voteInfoStyle); - GUILayout.Space(5); - - string autoButtonText = votekickEveryone ? "STOP AUTO VOTEKICK + LEAVE" : "AUTO VOTEKICK + LEAVE"; - if (GUILayout.Button(autoButtonText, votekickEveryone ? activeTabStyle : btnStyle, GUILayout.Height(35))) - { - if (votekickEveryone) StopVotekickEveryoneRun(); - else StartVotekickEveryoneRun(); - } - - GUILayout.Space(5); - GUILayout.Label("Manual mode: sends 3 votes now and stays in the current room.", voteInfoStyle); - if (GUILayout.Button("SEND 3 VOTES + STAY", btnStyle, GUILayout.Height(32))) - { - SendVotekickEveryoneStay(); - } - } - finally { GUILayout.EndVertical(); } - - GUILayout.Space(10); - GUILayout.Label("TARGET VOTE", headerStyle); - - if (PlayerControl.AllPlayerControls != null) - { - var safePlayersList = new System.Collections.Generic.List(); - foreach (var p in PlayerControl.AllPlayerControls) safePlayersList.Add(p); - - votekickScrollPosition = GUILayout.BeginScrollView(votekickScrollPosition); - try - { - foreach (var pc in safePlayersList) - { - if (pc == null || pc.Data == null || pc.PlayerId >= 100 || pc == PlayerControl.LocalPlayer) continue; - - GUILayout.BeginHorizontal(boxStyle); - try - { - string pName = pc.Data.PlayerName ?? "Unknown"; - bool isHost = (AmongUsClient.Instance != null && AmongUsClient.Instance.GetHost()?.Character == pc); - string displayStr = isHost ? pName + " [H]" : pName; - - GUILayout.Label(displayStr, GUILayout.Width(110)); - - GUILayout.FlexibleSpace(); - - if (GUILayout.Button("Vote", btnStyle, GUILayout.Width(60), GUILayout.Height(25))) - { - ExecuteVotekickTarget(pc); - } - } - finally - { - GUILayout.EndHorizontal(); - } - GUILayout.Space(2); - } - } - finally - { - GUILayout.EndScrollView(); - } - } - } - - private void DrawElysiumModMenu(int windowID) - { - if (Event.current.type == EventType.Repaint && tabTransitionProgress < 1f) - { - tabTransitionProgress += Time.unscaledDeltaTime * 8f; - if (tabTransitionProgress >= 1f) { tabTransitionProgress = 1f; currentTab = targetTabIndex; } - } - - if (enableBackground && customMenuBg != null) - { - GUI.color = new Color(0.6f, 0.6f, 0.6f, 0.8f); - GUIStyle bgStyle = new GUIStyle() { normal = { background = customMenuBg } }; - GUI.Box(new Rect(0, 0, windowRect.width, windowRect.height), GUIContent.none, bgStyle); - GUI.color = Color.white; - } - - GUILayout.BeginHorizontal(); - GUILayout.Label(ApplyMenuShimmer("ElysiumModMenu Meowchelo & Carrot"), titleStyle); - GUILayout.FlexibleSpace(); - if (GUILayout.Button("-", new GUIStyle(btnStyle) { fixedWidth = 20, fixedHeight = 18, margin = CreateRectOffset(0, 8, 6, 0) })) showMenu = false; - GUILayout.EndHorizontal(); - - GUI.color = new Color(1f, 1f, 1f, 0.1f); - GUI.Box(new Rect(0, 30, windowRect.width, 1), "", safeLineStyle); - GUI.color = Color.white; - - GUILayout.BeginArea(new Rect(0f, 31f, 130f, windowRect.height - 31f)); - GUILayout.BeginVertical(sidebarStyle, GUILayout.ExpandHeight(true)); - GUILayout.Space(5); - for (int i = 0; i < tabNames.Length; i++) - if (GUILayout.Button(tabNames[i], i == targetTabIndex ? activeSidebarBtnStyle : sidebarBtnStyle, GUILayout.Height(24))) - if (targetTabIndex != i) { targetTabIndex = i; tabTransitionProgress = 0f; scrollPosition = Vector2.zero; } - GUILayout.EndVertical(); - GUILayout.EndArea(); - - GUI.color = new Color(1f, 1f, 1f, 0.1f); - GUI.Box(new Rect(130, 31, 1, windowRect.height), "", safeLineStyle); - GUI.color = new Color(1f, 1f, 1f, tabTransitionProgress); - - GUILayout.BeginArea(new Rect(140f, 36f + ((1f - tabTransitionProgress) * 10f), windowRect.width - 150f, windowRect.height - 46f)); - scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, false, GUIStyle.none, GUI.skin.verticalScrollbar); - int tabToDraw = (tabTransitionProgress < 1f) ? targetTabIndex : currentTab; - - if (tabToDraw == 0) DrawGeneralTab(); - else if (tabToDraw == 1) DrawSelfTab(); - else if (tabToDraw == 2) DrawVisualsTab(); - else if (tabToDraw == 3) DrawPlayersTab(); - else if (tabToDraw == 4) DrawSabotagesTab(); - else if (tabToDraw == 5) DrawHostOnlyTab(); - else if (tabToDraw == 6) DrawOutfitsTab(); - else if (tabToDraw == 7) DrawVotekickTab(); - else if (tabToDraw == 8) DrawMenuTab(); - else if (tabToDraw == 9) DrawMapsTab(); - else if (tabToDraw == 10) DrawAnimationsTab(); - - GUILayout.EndScrollView(); - GUILayout.EndArea(); - - GUI.color = Color.white; - GUI.DragWindow(new Rect(0, 0, 10000, 30)); - } - public static int punishmentMode = 1; - public static string[] punishmentNames = { "Null", "Warn", "Kick", "Ban" }; - - public static bool blockSpoofRPC = true; - public static bool blockSabotageRPC = true; - public static bool blockGameRpcInLobby = true; - public static bool blockChatFloodRpc = true; - public static bool blockMeetingFloodRpc = true; - public static bool enablePasosLimit = true; - public static int rpcSpamLimit = 80; - public static bool enableLocalPasosBan = true; - public static bool enableHostPasosBan = true; - public static bool autoBanBrokenFriendCode = false; - public static int chatRpcLimit = 1; - public static float chatRpcWindow = 1f; - public static int meetingRpcLimit = 2; - public static float meetingRpcWindow = 9999f; - - [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleAnimation))] - public static class PlayerPhysics_HandleAnimation - { - public static bool Prefix(PlayerPhysics __instance) - { - if (ElysiumModMenuGUI.moonWalk && __instance.AmOwner) - { - __instance.ResetAnimState(); - return false; - } - return true; - } - } - - [HarmonyPatch(typeof(FreeChatInputField), nameof(FreeChatInputField.UpdateCharCount))] - public static class FreeChatInputField_UpdateCharCount_Patch - { - public static void Postfix(FreeChatInputField __instance) - { - if (__instance == null || __instance.textArea == null || __instance.charCountText == null) return; - - __instance.textArea.characterLimit = 120; - - int length = __instance.textArea.text.Length; - - __instance.charCountText.SetText($"{length}/{__instance.textArea.characterLimit}"); - - if (length < 90) - { - __instance.charCountText.color = Color.white; - } - else if (length < 115) - { - __instance.charCountText.color = Color.yellow; - } - else - { - __instance.charCountText.color = Color.red; - } - } - } - - public static class ChatHistory - { - public static List sentMessages = new List(); - public static int HistoryIndex = -1; - public static string DraftBeforeHistory = ""; - public static bool BrowsingHistory = false; - - public static void Remember(string message) - { - if (string.IsNullOrWhiteSpace(message)) return; - bool isNewEntry = sentMessages.Count == 0 || sentMessages[sentMessages.Count - 1] != message; - if (isNewEntry) - { - sentMessages.Add(message); - while (sentMessages.Count > ElysiumModMenuGUI.chatHistoryLimit) - sentMessages.RemoveAt(0); - } - HistoryIndex = sentMessages.Count; - } - - public static void HandleNavigation(ChatController chat) - { - if (sentMessages.Count == 0 || chat.freeChatField == null || chat.freeChatField.textArea == null || !chat.freeChatField.textArea.hasFocus) - return; - - if (Input.GetKeyDown(KeyCode.UpArrow)) - { - if (!BrowsingHistory) - { - DraftBeforeHistory = chat.freeChatField.textArea.text; - BrowsingHistory = true; - } - if (HistoryIndex <= 0) return; - - HistoryIndex = Mathf.Clamp(HistoryIndex - 1, 0, sentMessages.Count - 1); - chat.freeChatField.textArea.SetText(sentMessages[HistoryIndex], string.Empty); - } - else if (Input.GetKeyDown(KeyCode.DownArrow)) - { - if (!BrowsingHistory) return; - - HistoryIndex += 1; - if (HistoryIndex < sentMessages.Count) - { - chat.freeChatField.textArea.SetText(sentMessages[HistoryIndex], string.Empty); - } - else - { - chat.freeChatField.textArea.SetText(DraftBeforeHistory, string.Empty); - BrowsingHistory = false; - } - } - } - } - - public static class ClipboardBridge - { - private static bool isPastingChatInput = false; - private static int currentPasteCharPos = 0; - private static int lastClipboardFrame = -1; - - public static void Run(TextBoxTMP box) - { - if (!enableClipboard) return; - if (box == null || !box.hasFocus) return; - - bool controlHeld = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); - bool shiftHeld = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); - - bool copyPressed = controlHeld && (Input.GetKeyDown(KeyCode.C) || Input.GetKeyDown(KeyCode.Insert)); - bool pastePressed = (controlHeld && Input.GetKeyDown(KeyCode.V)) || (shiftHeld && Input.GetKeyDown(KeyCode.Insert)); - bool cutPressed = controlHeld && Input.GetKeyDown(KeyCode.X); - - if (!copyPressed && !pastePressed && !cutPressed) return; - if (lastClipboardFrame == Time.frameCount) return; - lastClipboardFrame = Time.frameCount; - - if (copyPressed) - { - GUIUtility.systemCopyBuffer = box.text ?? string.Empty; - } - else if (pastePressed) - { - string paste = GUIUtility.systemCopyBuffer; - if (!string.IsNullOrEmpty(paste)) - { - string currentText = box.text ?? string.Empty; - int caretPos = Mathf.Clamp(box.caretPos, 0, currentText.Length); - string nextText = currentText.Insert(caretPos, paste); - - isPastingChatInput = true; - box.SetText(nextText, string.Empty); - isPastingChatInput = false; - } - } - else if (cutPressed) - { - GUIUtility.systemCopyBuffer = box.text ?? string.Empty; - box.SetText(string.Empty, string.Empty); - } - } - - public static bool IsCharAllowed(TextBoxTMP box, ref bool result) - { - if (box == null) return true; - - string compositionString = Input.compositionString; - if (!string.IsNullOrEmpty(compositionString)) - { - result = true; - return false; - } - - string input = isPastingChatInput ? GUIUtility.systemCopyBuffer : Input.inputString; - if (string.IsNullOrEmpty(input)) return true; - - string currentText = box.text ?? string.Empty; - int caretPos = Mathf.Clamp(box.caretPos, 0, currentText.Length); - string text = currentText.Insert(caretPos, input); - - currentPasteCharPos = Mathf.Clamp(currentPasteCharPos, 0, Mathf.Max(0, text.Length - 1)); - char currentChar = text[currentPasteCharPos]; - currentPasteCharPos = currentPasteCharPos >= text.Length - 1 ? 0 : currentPasteCharPos + 1; - - if (allowLinksAndSymbols) - { - HashSet blockedSymbols = new HashSet { '\b', '\r', '\n', '>', '<', '[' }; - result = !blockedSymbols.Contains(currentChar); - return false; - } - - return true; - } - } - - [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.Update))] - public static class AllowSymbols_TextBoxTMP_Update_Patch - { - public static void Postfix(TextBoxTMP __instance) - { - if (__instance == null) return; - __instance.allowAllCharacters = ElysiumModMenuGUI.allowLinksAndSymbols; - __instance.AllowSymbols = ElysiumModMenuGUI.allowLinksAndSymbols; - __instance.AllowEmail = ElysiumModMenuGUI.allowLinksAndSymbols; - } - } - - [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.Update))] - public static class Clipboard_TextBoxTMP_Patch - { - public static void Postfix(TextBoxTMP __instance) - { - ClipboardBridge.Run(__instance); - } - } - - [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.IsCharAllowed))] - public static class Clipboard_TextBoxTMP_IsCharAllowed_Patch - { - public static bool Prefix(TextBoxTMP __instance, ref bool __result) - { - return ClipboardBridge.IsCharAllowed(__instance, ref __result); - } - } - - [HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] - public static class ChatHistory_Update_Patch - { - public static void Postfix(ChatController __instance) - { - if (__instance != null && __instance.freeChatField != null && __instance.freeChatField.textArea != null) - { - ClipboardBridge.Run(__instance.freeChatField.textArea); - } - ChatHistory.HandleNavigation(__instance); - } - } - public static bool enableExtendedChat = true; - public static bool enableChatHistory = true; - public static bool enableClipboard = true; - public static bool AnimEmptyGarbageEnabled = false; - public static bool skipShhhAnim = false; - public static bool isManualMapSpawn = false; - private void DrawAnimationsTab() - { - GUILayout.BeginVertical(boxStyle); - - GUILayout.Label(L("LOOPED PLAYER ANIMATIONS", "ЗАЦИКЛЕННЫЕ АНИМАЦИИ ИГРОКА"), headerStyle); - - string animInfo = L("Animations are looped. They will run as long as the toggle is ON.", - "Анимации зациклены. Будут работать, пока включен тумблер."); - GUILayout.Label(animInfo, new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); - - GUILayout.Space(10); - - GUILayout.BeginHorizontal(); - AnimAsteroidsEnabled = DrawToggle(AnimAsteroidsEnabled, L("Weapons (Asteroids)", "Оружие (Астероиды)"), 250); - IsScanning = DrawToggle(IsScanning, L("Medbay Scan", "Скан в медпункте"), 250); - GUILayout.EndHorizontal(); - - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - AnimShieldsEnabled = DrawToggle(AnimShieldsEnabled, L("Turn On Shields", "Включить щиты"), 250); - AnimCamsInUseEnabled = DrawToggle(AnimCamsInUseEnabled, L("Use Cameras (Blink Red)", "Камеры (Красный индикатор)"), 250); - GUILayout.EndHorizontal(); - - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - AnimEmptyGarbageEnabled = DrawToggle(AnimEmptyGarbageEnabled, L("Empty Garbage", "Выкинуть мусор"), 250); - skipShhhAnim = DrawToggle(skipShhhAnim, L("Skip 'Shhh!' Intro", "Пропустить 'Shhh!' интро"), 250); - GUILayout.EndHorizontal(); - - GUILayout.EndVertical(); - } - public static string GetPlatform(ClientData client) - { - if (client == null || client.PlatformData == null) return "Unknown"; - - int platformId = (int)client.PlatformData.Platform; - - switch (platformId) - { - case 1: return "Epic"; - case 2: return "Steam"; - case 3: return "Mac"; - case 4: return "Microsoft"; - case 5: return "Itch"; - case 6: return "iOS"; - case 7: return "Android"; - case 8: return "Switch"; - case 9: return "Xbox"; - case 10: return "PlayStation"; - case 112: return "Starlight"; - default: return $"Unknown ({platformId})"; - } - } - - private static string CompactEspValue(string value, int maxLength = 24) - { - value = Regex.Replace(value ?? string.Empty, "<.*?>", string.Empty) - .Replace('\r', ' ') - .Replace('\n', ' ') - .Trim(); - - if (string.IsNullOrEmpty(value)) return "Hidden"; - if (value.Length > maxLength) value = value.Substring(0, maxLength - 3) + "..."; - return value; - } - - private static string NormalizeEspToken(string value) - { - return Regex.Replace(value ?? string.Empty, "<.*?>", string.Empty) - .Replace('\r', ' ') - .Replace('\n', ' ') - .Trim(); - } - - private static string FriendEspIgnoreFilePath() - { - string folder = string.IsNullOrWhiteSpace(Plugin.ElysiumFolder) - ? System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu") - : Plugin.ElysiumFolder; - return System.IO.Path.Combine(folder, "ElysiumFriendEspIgnore.txt"); - } - - private static void LoadFriendEspIgnoreTokensIfNeeded() - { - try - { - if (Time.unscaledTime < friendEspIgnoreNextLoadAt) return; - friendEspIgnoreNextLoadAt = Time.unscaledTime + 3f; - - friendEspIgnoreTokens.Clear(); - string path = FriendEspIgnoreFilePath(); - if (!System.IO.File.Exists(path)) - { - System.IO.File.WriteAllText(path, "# One nickname, Friend Code, or PUID per line. Matching players will not show ESP info.\n"); - return; - } - - foreach (string line in System.IO.File.ReadAllLines(path)) - { - string token = NormalizeEspToken(line); - if (string.IsNullOrWhiteSpace(token) || token.StartsWith("#")) continue; - friendEspIgnoreTokens.Add(token); - } - } - catch { } - } - - private static bool IsEspIgnored(NetworkedPlayerInfo info) - { - if (info == null) return false; - - LoadFriendEspIgnoreTokensIfNeeded(); - if (friendEspIgnoreTokens.Count == 0) return false; - - try - { - string name = NormalizeEspToken(info.PlayerName); - if (!string.IsNullOrEmpty(name) && friendEspIgnoreTokens.Contains(name)) return true; - - string displayedFc = NormalizeEspToken(GetDisplayedFriendCode(info, string.Empty)); - if (!string.IsNullOrEmpty(displayedFc) && friendEspIgnoreTokens.Contains(displayedFc)) return true; - - string rawFc = NormalizeEspToken(info.FriendCode); - if (!string.IsNullOrEmpty(rawFc) && friendEspIgnoreTokens.Contains(rawFc)) return true; - - ClientData client = AmongUsClient.Instance?.GetClientFromPlayerInfo(info); - string puid = client == null ? string.Empty : NormalizeEspToken(GetClientPuid(client)); - return !string.IsNullOrEmpty(puid) && friendEspIgnoreTokens.Contains(puid); - } - catch { return false; } - } - - public static string BuildESPInfoLine(NetworkedPlayerInfo info) - { - if (info == null) return string.Empty; - - int level = 0; - string platform = "Unknown"; - bool isHost = false; - - try { level = (int)info.PlayerLevel + 1; } catch { } - - try - { - var client = AmongUsClient.Instance.GetClientFromPlayerInfo(info); - if (client != null) - { - platform = GetPlatform(client); - isHost = AmongUsClient.Instance.GetHost() == client; - } - } - catch { } - - if (enablePlatformSpoof && - PlayerControl.LocalPlayer != null && - info.PlayerId == PlayerControl.LocalPlayer.PlayerId) - { - platform = $"{platform} spf"; - } - - string fc = CompactEspValue(GetDisplayedFriendCode(info)); - List parts = new List(); - if (isHost) parts.Add("Host"); - parts.Add($"Lv:{level}"); - parts.Add(platform); - if (showEspFriendCode) parts.Add(fc); - return string.Join(" - ", parts); - } - - public static Color GetRoleColor(int roleId, Color fallbackColor) - { - switch (roleId) - { - case 1: return new Color32(255, 0, 0, 255); - case 2: return new Color32(0, 0, 128, 255); - case 3: return new Color32(127, 255, 212, 255); - case 4: return new Color32(176, 196, 222, 255); - case 5: return new Color32(255, 140, 0, 255); - case 8: return new Color32(255, 105, 180, 255); - case 9: return new Color32(139, 0, 0, 255); - case 10: return new Color32(106, 90, 205, 255); - case 12: return new Color32(189, 183, 107, 255); - case 18: return new Color32(173, 255, 47, 255); - default: return fallbackColor; - } - } - - public static void HandleTracer(PlayerControl target, bool enable) - { - try - { - if (target == null || target.gameObject == null) return; - - LineRenderer lr = target.GetComponent(); - - if (!enable || PlayerControl.LocalPlayer == null || target == PlayerControl.LocalPlayer || target.Data == null || target.Data.Disconnected || IsEspIgnored(target.Data)) - { - if (lr != null) lr.enabled = false; - return; - } - - if (target.Data.IsDead && !seeGhosts && !PlayerControl.LocalPlayer.Data.IsDead) - { - if (lr != null) lr.enabled = false; - return; - } - - if (lr == null) - { - lr = target.gameObject.AddComponent(); - lr.SetVertexCount(2); - lr.SetWidth(0.02f, 0.02f); - try { if (HatManager.Instance != null) lr.material = HatManager.Instance.PlayerMaterial; } catch { } - } - - lr.enabled = true; - - Color tColor = Color.white; - try - { - if (target.Data.IsDead) - { - tColor = Color.gray; - } - else if (target.Data.Role != null) - { - tColor = GetRoleColor((int)target.Data.Role.Role, target.Data.Role.TeamColor); - } - } - catch { } - - lr.SetColors(tColor, tColor); - - lr.SetPosition(0, PlayerControl.LocalPlayer.transform.position); - lr.SetPosition(1, target.transform.position); - } - catch { } - } - - - private void DrawLobbyControls() - { - GUILayout.BeginVertical(boxStyle); - GUILayout.Label("LOBBY CONTROLS", headerStyle); - - GUILayout.BeginHorizontal(); - - GUILayout.BeginVertical(boxStyle, GUILayout.Width(292)); - GUILayout.Label("GAME RULES", headerStyle); - neverEndGame = DrawToggle(neverEndGame, "Unlimited Game", 250); - GUILayout.Space(5); - noSettingLimit = DrawToggle(noSettingLimit, "No Setting Limit", 250); - GUILayout.Space(5); - noTaskMode = DrawToggle(noTaskMode, "No Task Mode", 250); - GUILayout.Space(5); - allowDuplicateColors = DrawToggle(allowDuplicateColors, L("Allow Duplicate Colors", "Разрешить одинаковые цвета"), 250); - GUILayout.EndVertical(); - - GUILayout.Space(10); - - GUILayout.BeginVertical(boxStyle, GUILayout.Width(292)); - GUILayout.Label("CHAT MODERATION", headerStyle); - enableColorCommand = DrawToggle(enableColorCommand, "Enable /c command (Public)", 250); - GUILayout.Space(5); - blockFortegreenChat = DrawToggle(blockFortegreenChat, "Block Fortegreen Chat", 250); - GUILayout.Space(5); - blockRainbowChat = DrawToggle(blockRainbowChat, "Block Rainbow Chat", 250); - GUILayout.Space(5); - autoChatEveryone = DrawToggle(autoChatEveryone, "Chat Everyone (Auto-Meeting)", 250); - if (autoChatEveryone) - { - GUILayout.BeginHorizontal(); - GUILayout.Label($"Delay: {autoChatEveryoneDelay:0.0}s", toggleLabelStyle, GUILayout.Width(78)); - autoChatEveryoneDelay = GUILayout.HorizontalSlider(autoChatEveryoneDelay, 0f, 10f, sliderStyle, sliderThumbStyle, GUILayout.Width(170)); - GUILayout.EndHorizontal(); - } - GUILayout.EndVertical(); - - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - - GUILayout.BeginHorizontal(); - - GUILayout.BeginVertical(boxStyle, GUILayout.Width(292)); - GUILayout.Label("LOBBY ACTIONS", headerStyle); - if (GUILayout.Button("Insta Start", btnStyle, GUILayout.Height(26))) - { GameStartManager.Instance.startState = GameStartManager.StartingStates.Countdown; GameStartManager.Instance.countDownTimer = 0f; } - GUILayout.Space(5); - if (GUILayout.Button("Close Meeting", btnStyle, GUILayout.Height(26))) MeetingHud.Instance.RpcClose(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Spawn Lobby", activeTabStyle, GUILayout.Height(26))) SpawnLobby(); - GUILayout.Space(5); - if (GUILayout.Button("Despawn", btnStyle, GUILayout.Height(26))) DespawnLobby(); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Kill All", btnStyle, GUILayout.Height(26))) KillAll(); - GUILayout.Space(5); - if (GUILayout.Button("Kick All", btnStyle, GUILayout.Height(26))) KickAll(); - GUILayout.Space(5); - if (GUILayout.Button("Mass Morph", btnStyle, GUILayout.Height(26))) this.StartCoroutine(MassMorphCoroutine().WrapToIl2Cpp()); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - - GUILayout.Space(10); - - GUILayout.BeginVertical(boxStyle, GUILayout.Width(292)); - GUILayout.Label("END GAME", headerStyle); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Crewmate Win", btnStyle, GUILayout.Height(26))) SmartEndGame("CrewWin"); - GUILayout.Space(5); - if (GUILayout.Button("Impostor Win", btnStyle, GUILayout.Height(26))) SmartEndGame("ImpWin"); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Imp Disconnect", btnStyle, GUILayout.Height(26))) SmartEndGame("ImpDisconnect"); - GUILayout.Space(5); - if (GUILayout.Button("H&S Disconnect", activeTabStyle, GUILayout.Height(26))) SmartEndGame("HnsImpDisconnect"); - GUILayout.EndHorizontal(); - GUILayout.Space(5); - - if (GUILayout.Button("Force End (Impostor Disconnect)", btnStyle, GUILayout.Height(26)) && GameManager.Instance != null && AmongUsClient.Instance.AmHost) - { bool tempNeverEnd = neverEndGame; neverEndGame = false; GameManager.Instance.RpcEndGame((GameOverReason)4, false); neverEndGame = tempNeverEnd; } - GUILayout.EndVertical(); - - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.EndVertical(); - } - public static string GetESPNameTag(NetworkedPlayerInfo info, string originalName) - { - if (info == null) return originalName; - string newName = originalName; - if (enableLocalNameSpoof && - PlayerControl.LocalPlayer != null && - info.PlayerId == PlayerControl.LocalPlayer.PlayerId && - !string.IsNullOrWhiteSpace(customNameInput)) - { - newName = BuildLocalNameRenderText(customNameInput); - } - - if (seeRoles && info.Role != null) - { - string roleName = info.Role.Role.ToString(); - int roleId = (int)info.Role.Role; - if (roleId == 8) roleName = "Noisemaker"; - else if (roleId == 9) roleName = "Phantom"; - else if (roleId == 10) roleName = "Tracker"; - else if (roleId == 12) roleName = "Detective"; - else if (roleId == 18) roleName = "Viper"; - else if (roleName == "GuardianAngel") roleName = "Guardian Angel"; - Color customColor = GetRoleColor(roleId, info.Role.TeamColor); - string roleColor = ColorUtility.ToHtmlStringRGB(customColor); - newName = $"{roleName}\n{newName}"; - } - if (showPlayerInfo) - { - string accentHex = ColorUtility.ToHtmlStringRGB(GetThemeAccentColor(currentAccentColor)); - string espLine = BuildESPInfoLine(info); - if (!string.IsNullOrWhiteSpace(espLine)) - newName = $"{espLine}\n{newName}"; - } - if (seeKillCooldown && info.Role != null && info.PlayerId != PlayerControl.LocalPlayer?.PlayerId) - { - int roleId = (int)info.Role.Role; - bool isImpTeam = roleId == 1 || roleId == 5 || roleId == 9 || roleId == 18; - if (isImpTeam) - { - float rem = GetRemainingKillCooldown(info.PlayerId); - string kcdText = rem > 0.01f ? $"KCD: {rem:F1}s" : "KCD: READY"; - string kcdColor = rem > 0.01f ? "FFAA33" : "55FF77"; - newName = $"{kcdText}\n{newName}"; - } - } - return newName; - } - - private static float GetConfiguredKillCooldown() - { - try - { - object opts = GameOptionsManager.Instance?.CurrentGameOptions; - if (opts == null) return 25f; - var m = opts.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance) - .FirstOrDefault(x => x.Name == "GetFloat" && x.GetParameters().Length == 1); - if (m != null) - { - Type enumType = m.GetParameters()[0].ParameterType; - if (enumType.IsEnum) - { - foreach (var val in Enum.GetValues(enumType)) - { - string n = val.ToString().ToLower(); - if (n.Contains("kill") && n.Contains("cool")) - { - object result = m.Invoke(opts, new object[] { val }); - return Convert.ToSingle(result); - } - } - } - } - } - catch { } - return 25f; - } - - private static float GetRemainingKillCooldown(byte playerId) - { - if (!lastKillTimestamps.ContainsKey(playerId)) return 0f; - float elapsed = Time.time - lastKillTimestamps[playerId]; - float rem = GetConfiguredKillCooldown() - elapsed; - return Mathf.Max(0f, rem); - } - - private static bool IsImpostorTeamForCooldown(PlayerControl pc) - { - try - { - if (pc == null || pc.Data == null) return false; - int roleId = pc.Data.Role != null ? (int)pc.Data.Role.Role : (int)pc.Data.RoleType; - return roleId == 1 || roleId == 5 || roleId == 9 || roleId == 18; - } - catch { return false; } - } - - public static void InitializeKillCooldownOnRoundStart() - { - try - { - lastKillTimestamps.Clear(); - if (PlayerControl.AllPlayerControls == null) return; - - float now = Time.time; - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc.Data == null || pc.Data.Disconnected) continue; - if (!IsImpostorTeamForCooldown(pc)) continue; - lastKillTimestamps[pc.PlayerId] = now; - } - } - catch { } - } - - - [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] - public static class VersionShower_Start_Patch - { - public static void Postfix(VersionShower __instance) { if (__instance != null && __instance.text != null) __instance.text.text = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu Meowchelo & Carrot"); } - } - - [HarmonyPatch(typeof(PingTracker), nameof(PingTracker.Update))] - public static class PingTracker_Watermark_Patch - { - private static float _smoothFps = 0f; - private static int _smoothPing = 0; - private static float _updateTimer = 0f; - public static void Postfix(PingTracker __instance) - { - try - { - _updateTimer += Time.deltaTime; - if (_updateTimer >= 0.5f) { _smoothFps = 1f / Time.deltaTime; if (AmongUsClient.Instance != null) _smoothPing = AmongUsClient.Instance.Ping; _updateTimer = 0f; } - int num = Mathf.RoundToInt(_smoothFps); - string pingColor = ((_smoothPing < 80) ? "#00FF00" : ((_smoothPing < 400) ? "#FFFF00" : "#FF0000")); - - string finalString = $"PING: {_smoothPing} ms • FPS: {num}"; - - if (ElysiumModMenuGUI.showWatermark) - { - string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.4"); - finalString = $"{shimmerTitle} • " + finalString; - } - - if (AmongUsClient.Instance != null) - { - ClientData host = AmongUsClient.Instance.GetHost(); - if (host != null && host.Character != null) - { - string hostName = host.Character.Data.PlayerName ?? "Unknown"; - string shimmerHostName = ElysiumModMenuGUI.ApplyMenuShimmer(hostName); - finalString += $" • Host: {shimmerHostName}"; - if (AmongUsClient.Instance.AmHost) finalString += " (You)"; - } - } - __instance.text.text = finalString; - __instance.text.alignment = TMPro.TextAlignmentOptions.Center; - __instance.aspectPosition.enabled = false; - float zPos = MeetingHud.Instance != null && MeetingHud.Instance.gameObject.activeInHierarchy ? -100f : -10f; - __instance.transform.localPosition = new Vector3(0f, -2.3f, zPos); - } - catch { } - } - } - - [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Update))] - public static class GameStartManager_Update_Patch - { - public static void Postfix(GameStartManager __instance) - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; - if (ElysiumModMenuGUI.fakeStartCounterTroll) - { - try { sbyte[] arr = { -123, -111, -100, -69, -67, -52, -42, 0, 42, 52, 67, 69, 100, 111, 123 }; sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; PlayerControl.LocalPlayer.RpcSetStartCounter(b); __instance.SetStartCounter(b); } catch { } - } - else if (ElysiumModMenuGUI.fakeStartCounterCustom && int.TryParse(ElysiumModMenuGUI.fakeStartInput, out int custom)) - { - try { PlayerControl.LocalPlayer.RpcSetStartCounter(custom); __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); } catch { } - } - } - } - - [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] - public static class InfiniteGamePatch { public static bool Prefix() { try { if (ElysiumModMenuGUI.neverEndGame && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) return false; } catch { } return true; } } - - [HarmonyPatch(typeof(IntroCutscene), "CoBegin")] - public static class IntroCutscene_CoBegin_Patch - { - public static void Prefix() - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; - if (ElysiumModMenuGUI.enablePreGameRoleForce) - { - foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) - { var target = GameData.Instance.GetPlayerById(kvp.Key)?.Object; if (target != null && target.Data.RoleType != kvp.Value) target.RpcSetRole(kvp.Value); } - foreach (byte impId in ElysiumModMenuGUI.forcedImpostors) - { var target = GameData.Instance.GetPlayerById(impId)?.Object; if (target != null && target.Data.Role != null && !target.Data.Role.IsImpostor) target.RpcSetRole(RoleTypes.Impostor); } - } - } - } - - [HarmonyPatch(typeof(LogicRoleSelectionNormal), "AssignRolesForTeam")] - public static class RoleSelectionNormal_Patch - { - public static bool Prefix(Il2CppSystem.Collections.Generic.List players, IGameOptions opts, RoleTeamTypes team, ref int teamMax) - { - if (!ElysiumModMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; - try - { - if ((int)team == 1) - { - int numImps = opts.GetInt((Int32OptionNames)1); - var impRoleTypes = new HashSet { 1, 5, 9, 18 }; - List allForced = new List(ElysiumModMenuGUI.forcedImpostors); - foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) if (impRoleTypes.Contains((int)kvp.Value) && !allForced.Contains(kvp.Key)) allForced.Add(kvp.Key); - if (allForced.Count > 0) numImps = allForced.Count; - else { if (numImps >= players.Count) numImps = players.Count - 1; if (numImps < 1) numImps = 1; } - int assigned = 0; - foreach (byte impId in allForced) - { - if (players.Count == 0 || assigned >= numImps) break; - var targetInfo = players.ToArray().FirstOrDefault(p => p.PlayerId == impId); - if (targetInfo != null && targetInfo.Object != null) - { - RoleTypes role = ElysiumModMenuGUI.forcedPreGameRoles.ContainsKey(impId) ? ElysiumModMenuGUI.forcedPreGameRoles[impId] : RoleTypes.Impostor; - targetInfo.Object.RpcSetRole(role, false); - players.Remove(targetInfo); - assigned++; - } - } - while (assigned < numImps && players.Count > 0) - { - int idx = UnityEngine.Random.Range(0, players.Count); - players[idx].Object.RpcSetRole(RoleTypes.Impostor, false); - players.RemoveAt(idx); - assigned++; - } - return false; - } - else if ((int)team == 0) - { - var crewRoleTypes = new HashSet { 0, 2, 3, 4, 8, 10, 12 }; - for (int i = players.Count - 1; i >= 0; i--) - { - var p = players[i]; - if (p != null && p.Object != null) - { - RoleTypes role = RoleTypes.Crewmate; - if (ElysiumModMenuGUI.forcedPreGameRoles.ContainsKey(p.PlayerId) && crewRoleTypes.Contains((int)ElysiumModMenuGUI.forcedPreGameRoles[p.PlayerId])) - role = ElysiumModMenuGUI.forcedPreGameRoles[p.PlayerId]; - p.Object.RpcSetRole(role, false); - players.RemoveAt(i); - } - } - return false; - } - return true; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(LogicRoleSelectionHnS), "AssignRolesForTeam")] - public static class RoleSelectionHnS_Patch - { - public static bool Prefix(Il2CppSystem.Collections.Generic.List players, IGameOptions opts, RoleTeamTypes team, ref int teamMax) - { - if (!ElysiumModMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; - if ((int)team != 1) return true; - try - { - int numImps = opts.GetInt((Int32OptionNames)1); - var impRoleTypes = new HashSet { 1, 5, 9, 18 }; - List allForced = new List(ElysiumModMenuGUI.forcedImpostors); - foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) if (impRoleTypes.Contains((int)kvp.Value) && !allForced.Contains(kvp.Key)) allForced.Add(kvp.Key); - if (allForced.Count > 0) numImps = allForced.Count; - else { if (numImps >= players.Count) numImps = players.Count - 1; if (numImps < 1) numImps = 1; } - int assigned = 0; - foreach (byte impId in allForced) - { - if (players.Count == 0 || assigned >= numImps) break; - var targetInfo = players.ToArray().FirstOrDefault(p => p.PlayerId == impId); - if (targetInfo != null) { targetInfo.Object.RpcSetRole((RoleTypes)1, false); players.Remove(targetInfo); assigned++; } - } - while (assigned < numImps && players.Count > 0) - { - int idx = UnityEngine.Random.Range(0, players.Count); - players[idx].Object.RpcSetRole((RoleTypes)1, false); - players.RemoveAt(idx); - assigned++; - } - return false; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SelectRoles))] - public static class RoleManager_SelectRoles_Patch - { - public static bool Prefix(RoleManager __instance) - { - if (!ElysiumModMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; - try - { - var allPlayers = PlayerControl.AllPlayerControls.ToArray().Where(p => p != null && p.Data != null && !p.Data.Disconnected && !p.Data.IsDead).ToList(); - int numImps = 1; - try { numImps = GameOptionsManager.Instance.CurrentGameOptions.GetInt((Int32OptionNames)1); } catch { } - var impRoleTypes = new HashSet { 1, 5, 9, 18 }; - List impostors = new List(); - foreach (var p in allPlayers) - if (ElysiumModMenuGUI.forcedImpostors.Contains(p.PlayerId) || (ElysiumModMenuGUI.forcedPreGameRoles.ContainsKey(p.PlayerId) && impRoleTypes.Contains((int)ElysiumModMenuGUI.forcedPreGameRoles[p.PlayerId]))) - impostors.Add(p); - if (impostors.Count > 0) numImps = impostors.Count; - else { if (numImps >= allPlayers.Count) numImps = allPlayers.Count - 1; if (numImps < 1) numImps = 1; } - System.Random rand = new System.Random(); - while (impostors.Count < numImps && allPlayers.Count > impostors.Count) - { - var available = allPlayers.Where(p => !impostors.Contains(p)).ToList(); - impostors.Add(available[rand.Next(available.Count)]); - } - List crewmates = allPlayers.Where(p => !impostors.Contains(p)).ToList(); - var impData = new Il2CppSystem.Collections.Generic.List(); - foreach (var i in impostors) impData.Add(i.Data); - var crewData = new Il2CppSystem.Collections.Generic.List(); - foreach (var c in crewmates) crewData.Add(c.Data); - IGameOptions opts = GameOptionsManager.Instance.CurrentGameOptions; - GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(impData, opts, (RoleTeamTypes)1, int.MaxValue, new Il2CppSystem.Nullable()); - GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(crewData, opts, (RoleTeamTypes)0, int.MaxValue, new Il2CppSystem.Nullable((RoleTypes)0)); - foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) - { - if (kvp.Value != RoleTypes.Crewmate && kvp.Value != RoleTypes.Impostor) - { - var pc = allPlayers.FirstOrDefault(p => p.PlayerId == kvp.Key); - if (pc != null) RoleManager.Instance.SetRole(pc, kvp.Value); - } - } - foreach (var pc in allPlayers) if (pc.Data.Role != null) pc.Data.Role.Initialize(pc); - return false; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.TurnOnProtection))] - public static class PlayerControl_TurnOnProtection_Patch - { - public static void Prefix(ref bool visible) - { - if (ElysiumModMenuGUI.seeGhosts || ElysiumModMenuGUI.seeProtections) visible = true; - } - } - - [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.LateUpdate))] - public static class PlayerVisuals_LateUpdate_Patch - { - public static void Postfix(PlayerPhysics __instance) - { - if (__instance == null || __instance.myPlayer == null || __instance.myPlayer.Data == null) return; - try - { - if (ElysiumModMenuGUI.seeGhosts && __instance.myPlayer.Data.IsDead && PlayerControl.LocalPlayer != null && !PlayerControl.LocalPlayer.Data.IsDead) - { - __instance.myPlayer.Visible = true; - var rend = __instance.myPlayer.GetComponent(); - if (rend != null) { Color c = rend.color; rend.color = new Color(c.r, c.g, c.b, 0.4f); } - } - var cosmetics = __instance.myPlayer.cosmetics; - var outfit = __instance.myPlayer.CurrentOutfit; - if (cosmetics != null && cosmetics.nameText != null && outfit != null) - { - cosmetics.SetName(ElysiumModMenuGUI.GetESPNameTag(__instance.myPlayer.Data, outfit.PlayerName)); - if (ElysiumModMenuGUI.seeRoles && ElysiumModMenuGUI.showPlayerInfo) cosmetics.nameText.transform.localPosition = new Vector3(0f, 0.186f, 0f); - else if (ElysiumModMenuGUI.seeRoles || ElysiumModMenuGUI.showPlayerInfo) cosmetics.nameText.transform.localPosition = new Vector3(0f, 0.093f, 0f); - else cosmetics.nameText.transform.localPosition = new Vector3(0f, 0f, 0f); - } - } - catch { } - } - } - - [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Update))] - public static class ESP_MeetingHud - { - public static void Postfix(MeetingHud __instance) - { - try - { - if (__instance.playerStates == null) return; - foreach (var state in __instance.playerStates) - { - if (state == null) continue; - var data = GameData.Instance.GetPlayerById(state.TargetPlayerId); - if (data != null && !data.Disconnected && data.DefaultOutfit != null && state.NameText != null) - { - string espName = ElysiumModMenuGUI.GetESPNameTag(data, data.DefaultOutfit.PlayerName ?? "???"); - if (!ElysiumModMenuGUI.seeRoles && ElysiumModMenuGUI.revealMeetingRoles && data.Role != null) - { - string roleName = data.Role.Role.ToString(); - int roleId = (int)data.Role.Role; - if (roleId == 8) roleName = "Noisemaker"; - else if (roleId == 9) roleName = "Phantom"; - else if (roleId == 10) roleName = "Tracker"; - else if (roleId == 12) roleName = "Detective"; - else if (roleId == 18) roleName = "Viper"; - else if (roleName == "GuardianAngel") roleName = "Guardian Angel"; - Color customColor = ElysiumModMenuGUI.GetRoleColor(roleId, data.Role.TeamColor); - string roleColor = ColorUtility.ToHtmlStringRGB(customColor); - espName = $"{roleName}\n{espName}"; - } - state.NameText.text = espName; - bool showingExtra = ElysiumModMenuGUI.seeRoles || ElysiumModMenuGUI.revealMeetingRoles; - if (showingExtra && ElysiumModMenuGUI.showPlayerInfo) { state.NameText.transform.localPosition = new Vector3(0.33f, 0.08f, 0f); state.NameText.transform.localScale = new Vector3(0.75f, 0.75f, 0.75f); } - else if (showingExtra || ElysiumModMenuGUI.showPlayerInfo) { state.NameText.transform.localPosition = new Vector3(0.3384f, 0.1125f, -0.1f); state.NameText.transform.localScale = new Vector3(0.9f, 1f, 1f); } - else { state.NameText.transform.localPosition = new Vector3(0.3384f, 0.0311f, -0.1f); state.NameText.transform.localScale = new Vector3(0.9f, 1f, 1f); } - } - } - } - catch { } - } - } - [HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetName))] - public static class ChatBubble_SetName_Patch - { - public static void Postfix(ChatBubble __instance) - { - if (!ElysiumModMenuGUI.showPlayerInfo || __instance.playerInfo == null) return; - try - { - string accentHex = ColorUtility.ToHtmlStringRGB(ElysiumModMenuGUI.currentAccentColor); - string espLine = ElysiumModMenuGUI.BuildESPInfoLine(__instance.playerInfo); - if (string.IsNullOrWhiteSpace(espLine)) return; - string extra = $" {espLine}"; - - if (!__instance.NameText.text.Contains("Lv:")) __instance.NameText.text += extra; - } - catch { } - } - } - - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcMurderPlayer))] - public static class KillCooldownTrackerPatch - { - public static void Prefix(PlayerControl __instance, PlayerControl target, bool didSucceed) - { - try - { - if (!didSucceed || __instance == null || __instance.Data == null) return; - ElysiumModMenuGUI.lastKillTimestamps[__instance.PlayerId] = Time.time; - } - catch { } - } - } - - [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] - public static class FullBright_Patch - { - public static void Postfix(HudManager __instance) - { - try - { - if (__instance == null || __instance.ShadowQuad == null || __instance.ShadowQuad.gameObject == null) return; - __instance.ShadowQuad.gameObject.SetActive(!ElysiumModMenuGUI.fullBright); - } - catch { } - } - } - - [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] - public static class HudManager_Update_Patch - { - public static void Postfix(HudManager __instance) - { - try - { - if (ElysiumModMenuGUI.alwaysChat && __instance.Chat != null) - __instance.Chat.gameObject.SetActive(true); - } - catch { } - } - } - - [HarmonyPatch(typeof(PlatformSpecificData), nameof(PlatformSpecificData.Serialize))] - public static class PlatformSpooferPatch { public static void Prefix(PlatformSpecificData __instance) { try { if (ElysiumModMenuGUI.enablePlatformSpoof && __instance != null) __instance.Platform = ElysiumModMenuGUI.platformValues[ElysiumModMenuGUI.currentPlatformIndex]; } catch { } } } - - [HarmonyPatch(typeof(FullAccount), nameof(FullAccount.CanSetCustomName))] - public static class FullAccount_CanSetCustomName_Patch { public static void Prefix(ref bool canSetName) { try { if (ElysiumModMenuGUI.unlockFeatures) canSetName = true; } catch { } } } - - [HarmonyPatch(typeof(AccountManager), nameof(AccountManager.CanPlayOnline))] - public static class AccountManager_CanPlayOnline_Patch { public static void Postfix(ref bool __result) { try { if (ElysiumModMenuGUI.unlockFeatures) __result = true; } catch { } } } - - [HarmonyPatch(typeof(EngineerRole), "FixedUpdate")] - public static class EngineerCheatsPatch - { - public static void Postfix(EngineerRole __instance) - { - if (__instance.Player != PlayerControl.LocalPlayer) return; - if (ElysiumModMenuGUI.endlessVentTime) __instance.inVentTimeRemaining = float.MaxValue; - if (ElysiumModMenuGUI.noVentCooldown && __instance.cooldownSecondsRemaining > 0f) - { - __instance.cooldownSecondsRemaining = 0f; - var btn = DestroyableSingleton.Instance?.AbilityButton; - if (btn != null) { btn.ResetCoolDown(); btn.SetCooldownFill(0f); } - } - } - } - - private static bool TrySetCooldownMember(object target, float value) - { - if (target == null) return false; - - string[] names = { "CoolDown", "_CoolDown_k__BackingField", "k__BackingField", "coolDown", "cooldown" }; - const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - - try - { - Type type = target.GetType(); - foreach (string name in names) - { - PropertyInfo property = type.GetProperty(name, flags); - if (property != null && property.CanWrite) - { - property.SetValue(target, value, null); - return true; - } - - FieldInfo field = type.GetField(name, flags); - if (field != null) - { - field.SetValue(target, value); - return true; - } - } - } - catch { } - - return false; - } - - [HarmonyPatch(typeof(Ladder), "SetDestinationCooldown")] - public static class Ladder_SetDestinationCooldown_Patch - { - public static bool Prefix(Ladder __instance) - { - try - { - if (!ElysiumModMenuGUI.noMapCooldowns) return true; - TrySetCooldownMember(__instance, 0f); - return false; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(ZiplineConsole), "Update")] - public static class ZiplineConsole_Update_Patch - { - public static void Postfix(ZiplineConsole __instance) - { - try - { - if (!ElysiumModMenuGUI.noMapCooldowns) return; - TrySetCooldownMember(__instance, 0f); - } - catch { } - } - } - - [HarmonyPatch(typeof(PlayerControl), "MurderPlayer")] - public static class KillCooldownTrackerPatch2 - { - public static void Prefix(PlayerControl __instance, PlayerControl target) - { - try - { - if (__instance == null || __instance.Data == null) return; - ElysiumModMenuGUI.lastKillTimestamps[__instance.PlayerId] = Time.time; - - if (!ElysiumModMenuGUI.spamReportBodies) return; - if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null || PlayerControl.LocalPlayer.Data.IsDead) return; - if (target == null || target.Data == null || !target.Data.IsDead) return; - - PlayerControl.LocalPlayer.CmdReportDeadBody(target.Data); - } - catch { } - } - } - - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetKillTimer))] - public static class KillAuraNoKillCooldownPatch - { - public static void Prefix(PlayerControl __instance, ref float time) - { - try - { - if (!ElysiumModMenuGUI.noKillCooldownHostOnly) return; - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; - if (__instance != PlayerControl.LocalPlayer) return; - time = 0f; - } - catch { } - } - } - - [HarmonyPatch(typeof(ScientistRole), "Update")] - public static class ScientistCheatsPatch - { - public static void Postfix(ScientistRole __instance) - { - if (__instance.Player != PlayerControl.LocalPlayer) return; - if (ElysiumModMenuGUI.noVitalsCooldown) __instance.currentCooldown = 0f; - if (ElysiumModMenuGUI.endlessBattery) __instance.currentCharge = float.MaxValue; - } - } - - [HarmonyPatch(typeof(ShapeshifterRole), "FixedUpdate")] - public static class ShapeshifterDurationPatch - { - public static void Postfix(ShapeshifterRole __instance) { if (__instance.Player == PlayerControl.LocalPlayer && ElysiumModMenuGUI.endlessSsDuration) __instance.durationSecondsRemaining = float.MaxValue; } - } - - [HarmonyPatch(typeof(ImpostorRole), "FindClosestTarget")] - public static class ImpostorRangePatch - { - public static bool Prefix(ImpostorRole __instance, ref PlayerControl __result) - { - if (!ElysiumModMenuGUI.killReach) return true; - try - { - var target = PlayerControl.AllPlayerControls.ToArray() - .Where(p => p != null && __instance.IsValidTarget(p.Data) && !p.Data.IsDead && !p.Data.Disconnected) - .OrderBy(p => Vector2.Distance(p.transform.position, PlayerControl.LocalPlayer.transform.position)) - .FirstOrDefault(); - if (target != null) __result = target; - return false; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(ImpostorRole), "IsValidTarget")] - public static class ImpostorKillAnyonePatch - { - public static void Postfix(NetworkedPlayerInfo target, ref bool __result) { try { if (ElysiumModMenuGUI.killAnyone && target != null && target.PlayerId != PlayerControl.LocalPlayer.PlayerId && !target.IsDead) __result = true; } catch { } } - } - - private void teleportToPlayer(PlayerControl t) - { - if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.NetTransform == null || t == null) return; - PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(t.transform.position); - } - - [HarmonyPatch(typeof(DetectiveRole), "FindClosestTarget")] - public static class DetectiveRangePatch - { - public static bool Prefix(DetectiveRole __instance, ref PlayerControl __result) - { - if (!ElysiumModMenuGUI.UnlimitedInterrogateRange) return true; - try - { - var target = PlayerControl.AllPlayerControls.ToArray() - .Where(p => p != null && __instance.IsValidTarget(p.Data) && !p.Data.IsDead && !p.Data.Disconnected) - .OrderBy(p => Vector2.Distance(p.transform.position, PlayerControl.LocalPlayer.transform.position)) - .FirstOrDefault(); - if (target != null) __result = target; - return false; - } - catch { return true; } - } - } - - [HarmonyPatch(typeof(DoorBreakerGame), nameof(DoorBreakerGame.Start))] - public static class DoorBreakerGame_Start_Patch - { - public static bool Prefix(DoorBreakerGame __instance) - { - if (!ElysiumModMenuGUI.autoOpenDoors) return true; - try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(__instance.MyDoor.Id | 64)); } catch { } - __instance.MyDoor.SetDoorway(true); __instance.Close(); - return false; - } - } - [HarmonyPatch(typeof(DoorCardSwipeGame), nameof(DoorCardSwipeGame.Begin))] - public static class DoorCardSwipeGame_Begin_Patch - { - public static bool Prefix(DoorCardSwipeGame __instance) - { - if (!ElysiumModMenuGUI.autoOpenDoors) return true; - try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(__instance.MyDoor.Id | 64)); } catch { } - __instance.MyDoor.SetDoorway(true); __instance.Close(); - return false; - } - } - [HarmonyPatch(typeof(MushroomDoorSabotageMinigame), nameof(MushroomDoorSabotageMinigame.Begin))] - public static class MushroomDoorSabotageMinigame_Begin_Patch - { - public static bool Prefix(MushroomDoorSabotageMinigame __instance) { if (ElysiumModMenuGUI.autoOpenDoors) { __instance.FixDoorAndCloseMinigame(); return false; } return true; } - } - - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetTasks))] - public static class NoTaskMode_Patch { public static bool Prefix(PlayerControl __instance) { if (ElysiumModMenuGUI.noTaskMode) return false; return true; } } - [HarmonyPatch(typeof(ChatController), nameof(ChatController.SendChat))] - public static class ChatController_SendChat_Patch - { - public static bool Prefix(ChatController __instance) - { - if (__instance.freeChatField == null || __instance.freeChatField.textArea == null) return true; - string text = __instance.freeChatField.textArea.text; - if (string.IsNullOrWhiteSpace(text)) return true; - - if (ElysiumModMenuGUI.enableChatHistory) - { - ChatHistory.Remember(text); - } - - ElysiumModMenuGUI.TrySpellCheckNotify(text); - - string lowerChat = text.ToLower().Trim(); - - if (ElysiumModMenuGUI.enableColorCommand) - { - if (lowerChat == "/rainbow" || lowerChat == "!rainbow" || lowerChat == "/lgbt" || lowerChat == "!lgbt") - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - if (ElysiumModMenuGUI.rainbowPlayers.Contains(PlayerControl.LocalPlayer.PlayerId)) - { - ElysiumModMenuGUI.rainbowPlayers.Remove(PlayerControl.LocalPlayer.PlayerId); - ElysiumModMenuGUI.ShowNotification("[SERVER] Ваша радуга ВЫКЛ."); - } - else - { - ElysiumModMenuGUI.rainbowPlayers.Add(PlayerControl.LocalPlayer.PlayerId); - ElysiumModMenuGUI.ShowNotification("[SERVER] Ваша радуга ВКЛ."); - } - } - else - { - if (HudManager.Instance?.Chat != null) - HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Эта команда только для Хоста!"); - } - __instance.freeChatField.textArea.SetText("", ""); - return false; - } - - if (lowerChat.StartsWith("/color ") || lowerChat.StartsWith("/c ") || lowerChat.StartsWith("/col ") || - lowerChat.StartsWith("!color ") || lowerChat.StartsWith("!c ") || lowerChat.StartsWith("!col ")) - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - string arg = lowerChat.Substring(lowerChat.IndexOf(' ') + 1).Trim(); - int colorId = -1; - - if (int.TryParse(arg, out int parsed)) colorId = parsed; - else colorId = ElysiumModMenuGUI.GetColorIdByName(arg); - - if (colorId >= 0 && colorId <= 18 && PlayerControl.LocalPlayer != null) - { - PlayerControl.LocalPlayer.RpcSetColor((byte)colorId); - } - else if (HudManager.Instance?.Chat != null) - { - HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Используйте ID (0-18) или названия (красн, син, зел...)"); - } - } - else - { - if (HudManager.Instance?.Chat != null) - HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Смена цвета доступна только Хосту!"); - } - __instance.freeChatField.textArea.SetText("", ""); - return false; - } - } - - if (lowerChat.StartsWith("/w ") || lowerChat.StartsWith("/pm ") || - lowerChat.StartsWith("/msg ") || lowerChat.StartsWith("/am ")) - { - string[] parts = text.Split(new char[] { ' ' }, 3); - if (parts.Length >= 3) - { - - string targetInput = parts[1].ToLower().Trim(); - string message = parts[2]; - PlayerControl target = null; - - if (byte.TryParse(targetInput, out byte pid)) - { - target = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p.PlayerId == pid); - } - - if (target == null && PlayerControl.AllPlayerControls != null) - { - PlayerControl exactMatch = null; - PlayerControl partialMatch = null; - - foreach (var pc in PlayerControl.AllPlayerControls) - { - if (pc == null || pc.Data == null || pc.Data.Disconnected || pc == PlayerControl.LocalPlayer) continue; - - string rawName = Regex.Replace(pc.Data.PlayerName, "<.*?>", string.Empty).ToLower().Trim(); - int cId = (int)pc.Data.DefaultOutfit.ColorId; - int targetColorId = ElysiumModMenuGUI.GetColorIdByName(targetInput); - - if (rawName == targetInput || (targetColorId != -1 && cId == targetColorId)) - { - exactMatch = pc; - break; - } - if (rawName.StartsWith(targetInput)) - { - if (partialMatch == null) partialMatch = pc; - } - } - target = exactMatch ?? partialMatch; - } - - if (target != null && target != PlayerControl.LocalPlayer) - { - string safeMessage = Regex.Replace(message, "<.*?>", string.Empty).Replace("<", "").Replace(">", ""); - string networkMsg = $"шепчет вам:\n{safeMessage}"; - - if (AmongUsClient.Instance != null && PlayerControl.LocalPlayer != null) - { - MessageWriter msgWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, 13, Hazel.SendOption.Reliable, target.OwnerId); - msgWriter.Write(networkMsg); - AmongUsClient.Instance.FinishRpcImmediately(msgWriter); - } - - string targetClean = Regex.Replace(target.Data.PlayerName, "<.*?>", string.Empty); - if (HudManager.Instance?.Chat != null) - HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, $"Вы шепчете {targetClean}:\n{safeMessage}"); - } - else if (HudManager.Instance?.Chat != null) - { - HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Игрок не найден! Введите ID, Цвет или Имя."); - } - } - __instance.freeChatField.textArea.SetText("", ""); - return false; - } - - return true; - } - } - - public static void Postfix(GameStartManager __instance) - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; - if (ElysiumModMenuGUI.customStartTimer > 0f) return; - - if (ElysiumModMenuGUI.fakeStartCounterTroll) - { - try - { - sbyte[] arr = { -123, -100, -69, -42, 0, 42, 69, 100, 123 }; - sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; - PlayerControl.LocalPlayer.RpcSetStartCounter((int)b); - __instance.SetStartCounter(b); - } - catch { } - } - else if (ElysiumModMenuGUI.fakeStartCounterCustom && int.TryParse(ElysiumModMenuGUI.fakeStartInput, out int custom)) - { - try - { - PlayerControl.LocalPlayer.RpcSetStartCounter(custom); - __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); - } - catch { } - } - } - } -} - - -[HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] -public static class ChatController_Update_Patch -{ - public static void Postfix(ChatController __instance) - { - try - { - if (!ElysiumModMenuGUI.enableChatDarkMode) return; - - if (__instance.freeChatField != null && __instance.freeChatField.background != null) - { - __instance.freeChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); - if (__instance.freeChatField.textArea != null && __instance.freeChatField.textArea.outputText != null) - __instance.freeChatField.textArea.outputText.color = Color.white; - } - if (__instance.quickChatField != null && __instance.quickChatField.background != null) - { - __instance.quickChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); - if (__instance.quickChatField.text != null) - __instance.quickChatField.text.color = Color.white; - } - } - catch { } - } -} - -[HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetText))] -public static class DarkMode_ChatBubblePatch -{ - public static void Postfix(ChatBubble __instance) - { - try - { - if (!ElysiumModMenuGUI.enableChatDarkMode) return; - - Transform bg = __instance.transform.Find("Background"); - if (bg != null) - { - var sr = bg.GetComponent(); - if (sr != null) sr.color = new Color32(35, 35, 35, 255); - } - if (__instance.TextArea != null) - __instance.TextArea.color = Color.white; - } - catch { } - } -} - -[HarmonyPatch(typeof(GameManager), nameof(GameManager.CheckTaskCompletion))] -public static class GameManager_CheckTaskCompletion_Patch -{ - public static bool Prefix(ref bool __result) - { - try - { - if (!ElysiumModMenuGUI.neverEndGame) return true; - __result = false; return false; - } - catch { return true; } - } -} - -[HarmonyPatch(typeof(ChatController), nameof(ChatController.SetVisible))] -public static class ChatController_SetVisible_Patch -{ - public static void Prefix(ref bool visible) - { - if (ElysiumModMenuGUI.alwaysChat) visible = true; - } -} - -[HarmonyPatch(typeof(MeetingHud), "Update")] -public static class RevealVotesPatch -{ - internal static List _votedPlayers = new List(); - public static void Prefix(MeetingHud __instance) - { - if (!ElysiumModMenuGUI.RevealVotesEnabled) return; - try - { - if ((int)__instance.state >= 4) return; - foreach (var item in __instance.playerStates) - { - if (item == null) continue; - var playerById = GameData.Instance.GetPlayerById(item.TargetPlayerId); - if (playerById == null || playerById.Disconnected || item.VotedFor == PlayerVoteArea.HasNotVoted || - item.VotedFor == PlayerVoteArea.MissedVote || item.VotedFor == PlayerVoteArea.DeadVote || _votedPlayers.Contains(item.TargetPlayerId)) continue; - _votedPlayers.Add(item.TargetPlayerId); - if (item.VotedFor != PlayerVoteArea.SkippedVote) - { - foreach (var item2 in __instance.playerStates) if (item2.TargetPlayerId == item.VotedFor) { __instance.BloopAVoteIcon(playerById, 0, item2.transform); break; } - } - else if (__instance.SkippedVoting != null) __instance.BloopAVoteIcon(playerById, 0, __instance.SkippedVoting.transform); - } - foreach (var item3 in __instance.playerStates) - { - if (item3 == null) continue; - var component = item3.transform.GetComponent(); - if (component != null) foreach (var sprite in component.Votes) sprite.gameObject.SetActive(true); - } - if (__instance.SkippedVoting != null) __instance.SkippedVoting.SetActive(true); - } - catch { } - } -} -[HarmonyPatch(typeof(MeetingHud), "PopulateResults")] -public static class RevealVotesCleanupPatch -{ - public static void Prefix(MeetingHud __instance) - { - if (!ElysiumModMenuGUI.RevealVotesEnabled) return; - try - { - foreach (var item in __instance.playerStates) - { - if (item == null) continue; - var component = item.transform.GetComponent(); - if (component != null && component.Votes.Count != 0) - { - foreach (var sprite in component.Votes) Object.DestroyImmediate(sprite.gameObject); - component.Votes.Clear(); - } - } - RevealVotesPatch._votedPlayers.Clear(); - } - catch { } - } -} - -[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increase))] -public static class NumberOption_Increase_Patch -{ - public static bool Prefix(NumberOption __instance) - { - try - { - if (!ElysiumModMenuGUI.noSettingLimit) return true; - if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && - (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) - return true; - __instance.Value += __instance.Increment; - __instance.UpdateValue(); - __instance.OnValueChanged.Invoke(__instance); - __instance.AdjustButtonsActiveState(); - return false; - } - catch { return true; } - } -} - -[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Decrease))] -public static class NumberOption_Decrease_Patch -{ - public static bool Prefix(NumberOption __instance) - { - try - { - if (!ElysiumModMenuGUI.noSettingLimit) return true; - if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && - (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) - return true; - __instance.Value -= __instance.Increment; - __instance.UpdateValue(); - __instance.OnValueChanged.Invoke(__instance); - __instance.AdjustButtonsActiveState(); - return false; - } - catch { return true; } - } -} - -[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Initialize))] -public static class NumberOption_Initialize_Patch -{ - public static void Postfix(NumberOption __instance) - { - try - { - if (!ElysiumModMenuGUI.noSettingLimit) return; - if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && - (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) - return; - __instance.ValidRange = new FloatRange(-999f, 999f); - } - catch { } - } -} - -[HarmonyPatch(typeof(IGameOptionsExtensions), nameof(IGameOptionsExtensions.GetAdjustedNumImpostors))] -public static class IGameOptionsExtensions_GetAdjustedNumImpostors_Patch -{ - public static bool Prefix(IGameOptions __instance, ref int __result) - { - try - { - if (!ElysiumModMenuGUI.noSettingLimit) return true; - __result = GameOptionsManager.Instance.CurrentGameOptions.NumImpostors; - return false; - } - catch { return true; } - } -} - -[HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.Start))] -public static class ExtendedLobbyListPatch -{ - public static Scroller scroller; - - public static bool Prefix(FindAGameManager __instance) - { - if (!ElysiumModMenuGUI.extendedLobby) return true; - try - { - if (__instance.gameContainers == null || __instance.gameContainers.Count == 0) return true; - if (__instance.gameContainers.Count > 10) return true; - - GameContainer prefab = __instance.gameContainers[0]; - GameObject holder = new GameObject("ExtendedLobbyScroller"); - holder.transform.SetParent(prefab.transform.parent); - - scroller = holder.AddComponent(); - scroller.Inner = holder.transform; - scroller.MouseMustBeOverToScroll = true; - scroller.allowY = true; - scroller.ScrollWheelSpeed = 0.4f; - scroller.SetYBoundsMin(0f); - scroller.SetYBoundsMax(4f); - - BoxCollider2D collider = prefab.transform.parent.gameObject.AddComponent(); - collider.size = new Vector2(100f, 100f); - scroller.ClickMask = collider; - - var list = new System.Collections.Generic.List(); - foreach (var gc in __instance.gameContainers) - { - gc.transform.SetParent(holder.transform); - gc.transform.localPosition = new Vector3(gc.transform.localPosition.x, gc.transform.localPosition.y, 25f); - list.Add(gc); - } - - for (int i = 0; i < 15; i++) - { - GameContainer newGc = UnityEngine.Object.Instantiate(prefab, holder.transform); - newGc.transform.localPosition = new Vector3(newGc.transform.localPosition.x, newGc.transform.localPosition.y - 0.75f * list.Count, 25f); - list.Add(newGc); - } - - __instance.gameContainers = new Il2CppReferenceArray(list.ToArray()); - return true; - } - catch { return true; } - } -} - -[HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.RefreshList))] -public static class ExtendedLobbyRefreshPatch -{ - public static void Postfix() - { - try { if (ElysiumModMenuGUI.extendedLobby && ExtendedLobbyListPatch.scroller != null) ExtendedLobbyListPatch.scroller.ScrollRelative(new Vector2(0f, -100f)); } catch { } - } -} - - -[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.FixedUpdate))] -public static class InvertControls_Patch -{ - private static void SeePlayerVent(PlayerPhysics player) - { -#pragma warning disable CS8632 - if (GameManager.Instance.IsHideAndSeek() && player.myPlayer.Data.RoleType == RoleTypes.Impostor || player == null || - AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started) - return; - if (!SeePlayersInVent) - { - if (player.myPlayer.invisibilityAlpha == 0.3f) - { - PhantomRole? role = player.myPlayer.Data.Role as PhantomRole; - if (role != null) - { - player.myPlayer.SetInvisibility(role.isInvisible); - return; - } - else - { - player.myPlayer.cosmetics.SetPhantomRoleAlpha(1f); - player.myPlayer.invisibilityAlpha = 1; - if (player.myPlayer.inVent) - { - player.myPlayer.Visible = false; - } - } - } - return; - } - - if (player.myPlayer.inVent && player.NetId != PlayerControl.LocalPlayer.MyPhysics.NetId) - { - player.myPlayer.Visible = true; - player.myPlayer.invisibilityAlpha = 0.3f; - player.myPlayer.cosmetics.SetPhantomRoleAlpha(0.3f); - } - else - { - PhantomRole? role = player.myPlayer.Data.Role as PhantomRole; - if (role != null) - { - player.myPlayer.SetInvisibility(role.isInvisible); - } - else - { - player.myPlayer.cosmetics.SetPhantomRoleAlpha(1f); - player.myPlayer.invisibilityAlpha = 1; - } - } - } - - public static void Postfix(PlayerPhysics __instance) - { - if (__instance.AmOwner && ElysiumModMenuGUI.invertControls && __instance.body != null) - { - __instance.body.velocity = -__instance.body.velocity; - } - - SeePlayerVent(__instance); - } -} -[HarmonyPatch(typeof(LobbyBehaviour), nameof(LobbyBehaviour.Start))] -public static class LobbyStart_ApplyLevelSpoof -{ - public static void Postfix() - { - if (!ElysiumModMenuGUI.isEditingLevel && uint.TryParse(ElysiumModMenuGUI.spoofLevelString, out uint parsedLvl)) - { - uint targetLevel = parsedLvl > 0 ? parsedLvl - 1 : 0; - try { AmongUs.Data.DataManager.Player.stats.level = targetLevel; } - catch { try { AmongUs.Data.DataManager.Player.Stats.Level = targetLevel; } catch { } } - AmongUs.Data.DataManager.Player.Save(); - } - } -} - -[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] -public static class RPCSniffer_Patch -{ - private static readonly HashSet VanillaRPCs = ElysiumModMenuGUI.VanillaRpcIds; - - private static readonly Dictionary KnownMods = new Dictionary - { - { 157, ("RockStar", "#800000") }, - { 121, ("RockStar / Chocoo", "#800000") }, - { 167, ("TuffMenu", "#008000") }, - { 164, ("Hydra / Sicko", "#FF0000") }, - { 176, ("HostGuard / TOH", "#008000") }, - { 195, ("Polar Client", "#FFFF00") }, - { 204, ("Polar Client", "#FFFF00") }, - { 154, ("GNC", "#FF0000") }, - { 85, ("KillNet (Base)", "#FF0000") }, - { 150, ("KillNet (V2)", "#FF0000") }, - { 162, ("KNM", "#FF0000") }, - { 250, ("KillNet (Alt)", "#FF0000") }, - { 212, ("BanMod", "#008000") }, - { 213, ("BanMod", "#008000") }, - { 214, ("BanMod", "#008000") }, - { 215, ("BanMod", "#008000") }, - { 216, ("BanMod", "#008000") }, - { 217, ("BanMod", "#008000") }, - { 218, ("BanMod", "#008000") }, - { 219, ("BanMod", "#008000") }, - { 144, ("Gaff Menu", "#FF0000") }, - { 145, ("Gaff Menu", "#FF0000") }, - { 188, ("GMM", "#FF0000") }, - { 189, ("GMM", "#FF0000") }, - { 169, ("Malum", "#FF0000") }, - { 210, ("Eclipse", "#FFFF00") }, - { 173, ("Private Client", "#FF0000") }, - { 151, ("Better Among Us", "#008000") }, - { 152, ("Better Among Us", "#008000") }, - { 255, ("CrewMod", "#FFFF00") }, - { 111, ("AUM (BitCrackers)", "#FF0000") }, - { 231, ("SentinelAU", "#FF0000") }, - { 133, ("Lunar / ElysiumModMenu", "#00FFFF") }, - { 89, ("ElysiumModMenu Old", "#008000") } - }; - - public static bool Prefix(PlayerControl __instance, byte callId, MessageReader reader) - { - if (__instance == null) return true; - - - if (PlayerControl.LocalPlayer != null && __instance == PlayerControl.LocalPlayer) return true; - - ElysiumModMenuGUI.RecordPlayerRpc(__instance, callId); - - if (ElysiumModMenuGUI.LogAllRPCs) - { - - if (!VanillaRPCs.Contains(callId)) - { - string pNameSniff = (__instance.Data != null && !string.IsNullOrEmpty(__instance.Data.PlayerName)) ? __instance.Data.PlayerName : $"Player_{__instance.PlayerId}"; - - - if (KnownMods.TryGetValue(callId, out var modInfo)) - { - ElysiumModMenuGUI.ShowNotification($"[СНИФФЕР] {pNameSniff}: {modInfo.Name} ({callId})"); - } - else - { - ElysiumModMenuGUI.ShowNotification($"[СНИФФЕР] {pNameSniff} кинул неизвестный RPC: {callId}"); - } - } - } - return true; - } -} - -[HarmonyPatch(typeof(HatManager), nameof(HatManager.Initialize))] -public static class UnlockCosmetics_HatManager_Initialize_Postfix -{ - public static void Postfix(HatManager __instance) - { - if (!ElysiumModMenuGUI.unlockCosmetics) return; - - foreach (var bundle in __instance.allBundles) bundle.Free = true; - foreach (var hat in __instance.allHats) hat.Free = true; - foreach (var nameplate in __instance.allNamePlates) nameplate.Free = true; - foreach (var pet in __instance.allPets) pet.Free = true; - foreach (var skin in __instance.allSkins) skin.Free = true; - foreach (var visor in __instance.allVisors) visor.Free = true; - foreach (var starBundle in __instance.allStarBundles) starBundle.price = 0; - } -} - -[HarmonyPatch(typeof(PlayerPurchasesData), nameof(PlayerPurchasesData.GetPurchase))] -public static class UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix -{ - public static bool Prefix(ref bool __result) - { - if (!ElysiumModMenuGUI.unlockCosmetics) return true; - __result = true; - return false; - } -} -[HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.Start))] -public static class AutoChatEveryone_Start_Patch -{ - public static void Postfix() - { - ElysiumModMenuGUI.InitializeKillCooldownOnRoundStart(); - - if (ElysiumModMenuGUI.autoChatEveryone && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - ElysiumModMenuGUI.pendingAutoMeeting = true; - ElysiumModMenuGUI.autoMeetingTimer = 0f; - } - } -} -[HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] -public static class ChatController_AddChat_Patch -{ - public static bool Prefix(PlayerControl sourcePlayer, ref string chatText, bool censor, ChatController __instance) - { - if (string.IsNullOrEmpty(chatText)) return true; - string lowerText = chatText.ToLower().Trim(); - - if (ElysiumModMenuGUI.enableColorCommand && sourcePlayer != null) - { - string[] colorCommands = { "/color ", "!color ", "/col ", "!col ", "/c ", "!c " }; - string usedCmd = colorCommands.FirstOrDefault(cmd => lowerText.StartsWith(cmd)); - - if (usedCmd != null) - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - string colorInput = lowerText.Substring(usedCmd.Length).Trim(); - int colorId = -1; - - if (int.TryParse(colorInput, out int parsedId)) { if (parsedId >= 0 && parsedId <= 18) colorId = parsedId; } - else colorId = ElysiumModMenuGUI.GetColorIdByName(colorInput); - - if (colorId != -1) - { - if (colorId == 18 && ElysiumModMenuGUI.blockFortegreenChat) - { - if (HudManager.Instance?.Chat != null) - HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Цвет Fortegreen запрещен хостом!"); - } - else - { - sourcePlayer.RpcSetColor((byte)colorId); - } - } - else if (sourcePlayer == PlayerControl.LocalPlayer) - { - __instance.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Неверный цвет."); - } - } - return false; - } - - if (lowerText == "/rainbow" || lowerText == "!rainbow" || lowerText == "/lgbt" || lowerText == "!lgbt") - { - if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - if (ElysiumModMenuGUI.blockRainbowChat) - { - if (HudManager.Instance?.Chat != null) - HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Радуга запрещена хостом!"); - } - else - { - if (ElysiumModMenuGUI.rainbowPlayers.Contains(sourcePlayer.PlayerId)) - { - ElysiumModMenuGUI.rainbowPlayers.Remove(sourcePlayer.PlayerId); - ElysiumModMenuGUI.ShowNotification("[SERVER] Радуга ВЫКЛ."); - } - else - { - ElysiumModMenuGUI.rainbowPlayers.Add(sourcePlayer.PlayerId); - ElysiumModMenuGUI.ShowNotification("[SERVER] Радуга ВКЛ."); - } - } - } - return false; - } - } - - if (ShouldShowGhostMessage(sourcePlayer)) - { - return ShowGhostMessage(sourcePlayer, chatText, censor, __instance); - } - - return true; - } - - private static bool ShouldShowGhostMessage(PlayerControl sourcePlayer) - { - try - { - if (!ElysiumModMenuGUI.readGhostChat && !ElysiumModMenuGUI.seeGhosts) return false; - if (sourcePlayer == null || sourcePlayer.Data == null) return false; - if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) return false; - if (PlayerControl.LocalPlayer.Data.IsDead) return false; - - return sourcePlayer.Data.IsDead; - } - catch { return false; } - } - - private static bool ShowGhostMessage(PlayerControl sourcePlayer, string chatText, bool censor, ChatController chat) - { - if (chat == null) return true; - - ChatBubble pooledBubble = null; - try - { - NetworkedPlayerInfo sourceData = sourcePlayer.Data; - if (sourceData == null) return true; - - pooledBubble = chat.GetPooledBubble(); - pooledBubble.transform.SetParent(chat.scroller.Inner); - pooledBubble.transform.localScale = Vector3.one; - - bool isLocal = sourcePlayer == PlayerControl.LocalPlayer; - if (isLocal) pooledBubble.SetRight(); - else pooledBubble.SetLeft(); - - bool didVote = MeetingHud.Instance != null && MeetingHud.Instance.DidVote(sourcePlayer.PlayerId); - pooledBubble.SetCosmetics(sourceData); - chat.SetChatBubbleName(pooledBubble, sourceData, sourceData.IsDead, didVote, PlayerNameColor.Get(sourceData), null); - - if (censor && AmongUs.Data.DataManager.Settings.Multiplayer.CensorChat) - { - chatText = BlockedWords.CensorWords(chatText, false); - } - - pooledBubble.SetText($"{chatText}"); - pooledBubble.AlignChildren(); - chat.AlignAllBubbles(); - - if (!chat.IsOpenOrOpening && chat.notificationRoutine == null) - { - chat.notificationRoutine = chat.StartCoroutine(chat.BounceDot()); - } - - if (!isLocal && !chat.IsOpenOrOpening) - { - SoundManager.Instance.PlaySound(chat.messageSound, false).pitch = 0.5f + sourcePlayer.PlayerId / 15f; - chat.chatNotification.SetUp(sourcePlayer, chatText); - } - - return false; - } - catch - { - try - { - if (pooledBubble != null) chat.chatBubblePool.Reclaim(pooledBubble); - } - catch { } - return true; - } - } - - - - public static void Postfix(GameStartManager __instance) - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; - if (ElysiumModMenuGUI.customStartTimer > 0f) return; - - if (ElysiumModMenuGUI.fakeStartCounterTroll) - { - try - { - sbyte[] arr = { -123, -100, -69, -42, 0, 42, 69, 100, 123 }; - sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; - PlayerControl.LocalPlayer.RpcSetStartCounter((int)b); - __instance.SetStartCounter(b); - } - catch { } - } - else if (ElysiumModMenuGUI.fakeStartCounterCustom && int.TryParse(ElysiumModMenuGUI.fakeStartInput, out int custom)) - { - try - { - PlayerControl.LocalPlayer.RpcSetStartCounter(custom); - __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); - } - catch { } - } - } -} - -[HarmonyPatch(typeof(GameContainer), nameof(GameContainer.SetupGameInfo))] -public static class MoreLobbyInfo_GameContainer_SetupGameInfo_Postfix -{ - public static void Postfix(GameContainer __instance) - { - if (!ElysiumModMenuGUI.moreLobbyInfo) return; - - var trueHostName = __instance.gameListing.TrueHostName; - const string separator = "<#0000>000000000000000"; - var age = __instance.gameListing.Age; - var lobbyTime = $"Age: {age / 60}:{(age % 60 < 10 ? "0" : "")}{age % 60}"; - - - int platId = (int)__instance.gameListing.Platform; - string platformStr = platId switch - { - 1 => "Epic", - 2 => "Steam", - 3 => "Mac", - 4 => "Microsoft Store", - 5 => "Itch.io", - 6 => "iOS", - 7 => "Android", - 8 => "Nintendo Switch", - 9 => "Xbox", - 10 => "PlayStation", - 112 => "Starlight", - _ => "Unknown" - }; - - string hexColor = ColorUtility.ToHtmlStringRGB(ElysiumModMenuGUI.currentAccentColor); - - __instance.capacity.text = $"{separator}\n{trueHostName}\n{__instance.capacity.text}\n" + - $"{GameCode.IntToGameName(__instance.gameListing.GameId)}\n" + - $"{platformStr}\n{lobbyTime}\n{separator}"; - } -} - -[HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.HandleList))] -public static class MoreLobbyInfo_FindAGameManager_HandleList_Postfix -{ - public static void Postfix(HttpMatchmakerManager.FindGamesListFilteredResponse response, FindAGameManager __instance) - { - if (!ElysiumModMenuGUI.moreLobbyInfo) return; - - __instance.TotalText.text = response.Metadata.AllGamesCount.ToString(); - } -} -[HarmonyPatch(typeof(PlatformSpecificData), nameof(PlatformSpecificData.Serialize))] -public static class PlatformSpooferPatch -{ - public static void Prefix(PlatformSpecificData __instance) - { - try - { - if (__instance != null) - { - if (!ElysiumModMenuGUI.enablePlatformSpoof) return; - - __instance.Platform = ElysiumModMenuGUI.platformValues[ElysiumModMenuGUI.currentPlatformIndex]; - __instance.PlatformName = "ElysiumModMenu by Meowchelo (and one silly guy :p) https://github.com/meowchelo/ElysiumModMenu"; - } - } - catch { } - } -} - -[HarmonyPatch(typeof(NetworkedPlayerInfo), nameof(NetworkedPlayerInfo.Serialize))] -public static class FriendCodeSpooferPatch -{ - private static string serializeRestoreValue = null; - - public static void Prefix(NetworkedPlayerInfo __instance) - { - try - { - serializeRestoreValue = null; - if (ElysiumModMenuGUI.PrepareLocalFriendCodeForSerialize(__instance, out serializeRestoreValue)) return; - if (!ElysiumModMenuGUI.enableFriendCodeSpoof) return; - if (__instance == null || PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) return; - if (__instance.PlayerId != PlayerControl.LocalPlayer.PlayerId) return; - - string input = ElysiumModMenuGUI.spoofFriendCodeInput ?? ""; - string clean = ""; - foreach (char c in input.ToLowerInvariant()) - { - if (char.IsWhiteSpace(c)) break; - if (char.IsLetterOrDigit(c)) clean += c; - if (clean.Length >= 10) break; - } - - if (string.IsNullOrWhiteSpace(clean)) return; - __instance.FriendCode = clean; - } - catch { } - } - - public static void Postfix(NetworkedPlayerInfo __instance) - { - ElysiumModMenuGUI.RestoreLocalFriendCodeAfterSerialize(__instance, serializeRestoreValue); - serializeRestoreValue = null; - } -} -[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.KickPlayer))] -public static class AmongUsClient_KickPlayer_BanList_Patch -{ - public static void Prefix(InnerNetClient __instance, int clientId, bool ban) - { - if (ban && PlayerControl.AllPlayerControls != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) - { - try - { - var pc = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p.OwnerId == clientId); - if (pc != null && pc.Data != null) - { - string fc = string.IsNullOrEmpty(pc.Data.FriendCode) ? "Unknown" : pc.Data.FriendCode; - string name = pc.Data.PlayerName ?? "Unknown"; - string puid = "Unknown"; - - try - { - var client = AmongUsClient.Instance.GetClientFromCharacter(pc); - if (client != null) puid = ElysiumModMenuGUI.GetClientPuid(client); - } - catch { } - ElysiumModMenuGUI.AddToBanList(fc, puid, name, "Host ban"); - ElysiumModMenuGUI.ShowNotification($"[BAN] {name} занесен в черный список!"); - } - } - catch { } - } - } -} diff --git a/ElysiumModMenu.csproj b/ElysiumModMenu.csproj index cddb71c..0e4ecef 100644 --- a/ElysiumModMenu.csproj +++ b/ElysiumModMenu.csproj @@ -13,6 +13,15 @@ C:\Program Files (x86)\Steam\steamapps\common\Among Us + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\AddressablesPlayAssetDelivery.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\AmongUsCaching.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Assembly-CSharp-firstpass.dll + $(AmongUsDir)\BepInEx\core\BepInEx.Core.dll @@ -22,6 +31,9 @@ $(AmongUsDir)\BepInEx\core\0Harmony.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\CsvHelper.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Hazel.dll @@ -31,44 +43,347 @@ $(AmongUsDir)\BepInEx\interop\Assembly-CSharp.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppMono.Security.dll + $(AmongUsDir)\BepInEx\interop\Il2Cppmscorlib.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Configuration.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Core.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Data.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Drawing.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Net.Http.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Numerics.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Runtime.Serialization.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Xml.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Xml.Linq.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\io.sentry.unity.runtime.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Newtonsoft.Json.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\QRCoder.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Rewired_Core.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Rewired_Windows.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Rewired_Windows_Functions.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.Microsoft.Bcl.AsyncInterfaces.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Buffers.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Collections.Immutable.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Memory.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Numerics.Vectors.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Reflection.Metadata.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Runtime.CompilerServices.Unsafe.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Text.Encodings.Web.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Text.Json.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Threading.Tasks.Extensions.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.Unity.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Addressables.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.LevelPlay.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.ProBuilder.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.ResourceManager.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Configuration.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Device.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Environments.Internal.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Internal.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Registration.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Scheduler.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Telemetry.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Threading.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.TextMeshPro.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AccessibilityModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AIModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AndroidJNIModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AnimationModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AssetBundleModule.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AudioModule.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ClothModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ClusterInputModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ClusterRendererModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ContentLoadModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.CoreModule.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.CrashReportingModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.DirectorModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.DSPGraphModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.GameCenterModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.GIModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.GridModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.HotReloadModule.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ImageConversionModule.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.IMGUIModule.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.InputModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.JSONSerializeModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.LocalizationModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ParticleSystemModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.PerformanceReportingModule.dll + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.Physics2DModule.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.PhysicsModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ProfilerModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.PropertiesModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.Purchasing.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.Purchasing.SecurityCore.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ScreenCaptureModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.SharedInternalsModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.SpriteMaskModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.SpriteShapeModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.StreamingModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.SubstanceModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.SubsystemsModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TerrainModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TerrainPhysicsModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TextCoreFontEngineModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TextCoreTextEngineModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.TextRenderingModule.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.InputLegacyModule.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TilemapModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TLSModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UI.dll + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UIElementsModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UIModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UmbraModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityAnalyticsCommonModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityAnalyticsModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityConnectModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityCurlModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityTestProtocolModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityWebRequestAssetBundleModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityWebRequestAudioModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityWebRequestModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityWebRequestTextureModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityWebRequestWWWModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.VehiclesModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.VFXModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.VideoModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.VirtualTexturingModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.VRModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.WindModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.XRModule.dll + + + ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\__Generated.dll + diff --git a/Network.cs b/Network.cs new file mode 100644 index 0000000..1bfe2d1 --- /dev/null +++ b/Network.cs @@ -0,0 +1,768 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { +public class ModPlayer : MonoBehaviour + + { + public PlayerControl player; + + public float LastTask; + + public float JoinTime; + + public bool NameApply = false; + + private Dictionary> rpcCallTimestamps = new Dictionary>(); + + private const float DefaultTimeWindow = 1f; + + private const int DefaultRpcLimitPerWindow = 10; + + private bool isMarkedAsSpamRpc; + + private bool isMarkedAsModUser; + + public static ModPlayer LocalPlayer => PlayerControl.LocalPlayer.Mod(); + + public static readonly HashSet normalCustomCallId = new HashSet { 6, 80, 78, 70, 210, 81, 176, 169 }; + + public readonly HashSet SpammableCrashRpc = new HashSet { 49, 50, 7, 4, 18, 31 }; + + public static readonly HashSet excludedCallIdsForTargetClient = new HashSet { 5, 6, 7, 13, 44, 51, 54, 62, 64, 55 }; + + public static readonly HashSet ImmediatelyRPCs = new HashSet + { + 51, 54, 5, 7, 14, 47, 48, 12, 52, 53, 54, + 45, 46, 62, 64, 55, 56, 2, 63, 65, 21, 49 + }; + + public static readonly HashSet excludedCallIdsForLobby = new HashSet + { + 5, 6, 7, 9, 10, 13, 17, 18, 21, 33, + 36, 37, 38, 39, 40, 41, 42, 43, 44, 49, 50, + 60, 61, 80, 78, 70, 210, 81, 176 + }; + + public static readonly IReadOnlyDictionary StandartALotRPCs = new Dictionary + { + { + 31, + (10, 1f) + }, + { + 18, + (25, 1f) + }, + { + 49, + (50, 0.1f) + }, + { + 44, + (50, 0.1f) + }, + { + 50, + (100, 0.1f) + }, + { + 8, + (30, 0.1f) + }, + { + 6, + (30, 0.1f) + }, + { + 39, + (30, 0.1f) + }, + { + 40, + (30, 0.1f) + }, + { + 42, + (30, 0.1f) + }, + { + 41, + (30, 0.1f) + }, + { + 33, + (1, 1f) + }, + { + 54, + (5, 1f) + }, + { + 7, + (100, 0.1f) + } + }; + + static bool RpcCrash; + + public static readonly HashSet excludedNumMsgCallIds = new HashSet { 12, 41, 39, 40, 42, 43, 38, 49 }; + + public static readonly HashSet SusRPCs = new HashSet { 101, 164, 154, 85, 219, 81, 176, 204, 216, 121, 119, 167 }; + + + public ModPlayer(IntPtr ptr) + : base(ptr) + { + } + + public void Awake() + { + player = this.GetComponent(); + } + + public void FixedUpdate() + { + JoinTime += Time.fixedDeltaTime; + LateTask.Update(Time.deltaTime); + } + + public bool RpcCheck(byte callId, int targetClientId, SendOption sendOption, int numData) + { + if (PlayerControl.LocalPlayer == null) return true; + RpcCalls b = (RpcCalls)callId; + if (!CheckSpam(callId)) + { + return false; + } + + if (AmongUsClient.Instance.AmHost) + { + if (targetClientId >= 0 && !excludedCallIdsForTargetClient.Contains(callId) && !(player.Data.ClientId == AmongUsClient.Instance.ClientId)) + { + + if (!RpcCrash) + { + new LateTask(delegate + { + DestroyableSingleton.Instance.Notifier.AddDisconnectMessage($"Received Rpc from {player.Data.PlayerName}, {b}\nthat shouldn't be got like that way"); + if (enablePasosLimit) + { + AmongUsClient.Instance.KickPlayer(player.OwnerId, true); + } + }, 2f); + _ = new LateTask(delegate + { + RpcCrash = false; + }, 15f); + RpcCrash = true; + } + return false; + + } + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Joined && !excludedCallIdsForLobby.Contains(callId) && callId < 66 && player != PlayerControl.LocalPlayer) + { + if (!RpcCrash) + { + new LateTask(delegate + { + DestroyableSingleton.Instance.Notifier.AddDisconnectMessage($"Received Rpc in lobby from {player.Data.PlayerName}, {b}\nthat shouldn't be used there"); + + if (enablePasosLimit) + { + AmongUsClient.Instance.KickPlayer(player.OwnerId, true); + } + }, 2f); + _ = new LateTask(delegate + { + RpcCrash = false; + }, 10f); + RpcCrash = true; + } + return false; + } + } + if (callId > 65) + { + if (isMarkedAsModUser) return false; + if (!RpcCrash) + { + new LateTask(delegate + { + isMarkedAsModUser = true; + }, 2f); + _ = new LateTask(delegate + { + RpcCrash = false; + }, 10f); + RpcCrash = true; + } + return true; + } + if (!excludedNumMsgCallIds.Contains(callId) && ((numData > 1 && ImmediatelyRPCs.Contains(callId)) && !AmongUsClient.Instance.AmHost || numData > 25) && player != PlayerControl.LocalPlayer) + { + if (!RpcCrash) + { + new LateTask(delegate + { + DestroyableSingleton.Instance.Notifier.AddDisconnectMessage($"Received Too many Rpc from {player.name}\nfor this type of Rpc - {b} "); + }, 2f); + _ = new LateTask(delegate + { + RpcCrash = false; + }, 10f); + RpcCrash = true; + } + return targetClientId <= 0; + + } + if (sendOption == SendOption.None && callId != 0 && player != PlayerControl.LocalPlayer) + { + if (!RpcCrash && !isMarkedAsModUser) + { + if (isMarkedAsModUser) return targetClientId <= 0; + isMarkedAsModUser = true; + _ = new LateTask(delegate + { + DestroyableSingleton.Instance.Notifier.AddDisconnectMessage($"Received SendOption None Rpc (Modded) from {player.name}, {b}"); + }, 2f); + _ = new LateTask(delegate + { + RpcCrash = false; + }, 10f); + RpcCrash = true; + } + return targetClientId <= 0; + } + return true; + } + + public bool CheckSpam(byte callId) + { + + if (!rpcCallTimestamps.ContainsKey(callId)) + { + rpcCallTimestamps[callId] = new Queue(); + } + Queue queue = rpcCallTimestamps[callId]; + float fixedTime = Time.fixedTime; + float num = 1f; + int num2 = 10; + if (StandartALotRPCs.TryGetValue(callId, out var value)) + { + num2 = value.Item1; + num = value.Item2; + } + while (queue.Count > 0 && queue.Peek() < fixedTime - num) + { + queue.Dequeue(); + } + queue.Enqueue(fixedTime); + if ((queue.Count > num2) && (byte)RpcCalls.SnapTo != callId) + { + if (!RpcCrash) + { + new LateTask(delegate + { + if (enablePasosLimit) + { + AmongUsClient.Instance.KickPlayer(player.OwnerId, true); + } + if (!isMarkedAsSpamRpc && player != PlayerControl.LocalPlayer) + { + isMarkedAsSpamRpc = true; + } + DestroyableSingleton.Instance.Notifier.AddDisconnectMessage($"Rpc Spam from {player.name}\nwith {(RpcCalls)callId}"); + }, 2f); + new LateTask(delegate + { + RpcCrash = false; + + }, 10f); + RpcCrash = true; + } + return false; + } + return true; + } + + public static bool operator ==(ModPlayer a, PlayerControl b) + { + return a.player == b; + } + + public static bool operator ==(PlayerControl a, ModPlayer b) + { + return a == b.player; + } + + public static bool operator !=(ModPlayer a, PlayerControl b) + { + return a.player != b; + } + + public static bool operator !=(PlayerControl a, ModPlayer b) + { + return a != b.player; + } + + public override bool Equals(object o) + { + return base.Equals(o); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } + +[HarmonyPatch(typeof(InnerNetClient), "HandleMessage")] + internal class HandleMessage + { + public static List blockedSenders = new(); + public static Dictionary msgCount = new(); + public static float blockedtimer; + public static float timer; + + public static void HandleTimer() + { + GoingTimer(); + resetingDataLimit += Time.deltaTime; + timer -= Time.deltaTime; + blockedtimer -= Time.deltaTime; + + if (resetingDataLimit >= 1) + { + resetingDataLimit = 0; + } + + if (timer <= 0) + { + timer = 1; + msgCount.Clear(); + } + if (blockedtimer <= 0) + { + blockedtimer = 15; + blockedSenders.Clear(); + } + } + + public static bool Crashed; + public static Dictionary LastJoin = new Dictionary(); + + public static void GoingTimer() + { + foreach (var item in LastJoin) + { + LastJoin[item.Key] += Time.deltaTime; + } + } + + public static void Crash(string reason) + { + if (!Crashed) + { + Debug.LogError("WARNING - " + reason); + _ = new LateTask(delegate + { + DestroyableSingleton.Instance.Notifier.AddDisconnectMessage("<#ffff00>" + L("Got crash attempt - " + reason, "Произошла попытка краша - " + reason)); + if (banMalformedPacketSender) + { + KeyValuePair keyValuePair = HandleMessage.LastJoin.OrderBy((KeyValuePair pair) => pair.Value).FirstOrDefault(); + AmongUsClient.Instance.KickPlayer(keyValuePair.Key, ban: true); + } + }, 0.1f); + _ = new LateTask(delegate + { + Crashed = false; + }, 10f); + Crashed = true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CheckDataMessage(MessageReader message, int TargetClientId, SendOption sendOption) + { + if (PlayerControl.LocalPlayer == null) return true; + int num = 0; + InnerNetObject innerNetObject = default; + while (message.Position < message.Length) + { + + if (num > 75) + { + Crash("Spam of Data"); + return false; + } + + MessageReader messageReader = message.ReadMessage(); + if (messageReader.Tag != 207 && messageReader.Tag > 8 || messageReader.Tag == 3 || messageReader.Tag == 0) + { + Crash("Bad Tag - " + messageReader.Tag); + return false; + } + if (messageReader.Tag == 1) + { + uint num2 = messageReader.ReadPackedUInt32(); + try + { + if (!AmongUsClient.Instance.allObjects.allObjectsFast.ContainsKey(num2) && !AmongUsClient.Instance.DestroyedObjects.Contains(num2) && AmongUsClient.Instance.AmHost || num2 > AmongUsClient.Instance.NetIdCnt + 30 || num2 == 0) + { + Crash("Null Data - " + num2); + return false; + } + } + catch { } + } + else + { + if (messageReader.Tag != 2) + { + continue; + } + uint num3 = messageReader.ReadPackedUInt32(); + byte callId = messageReader.ReadByte(); + AmongUsClient instance = AmongUsClient.Instance; + lock (instance.allObjects) + { + if (instance.allObjects.allObjectsFast.TryGetValue(num3, out innerNetObject)) + { + if (innerNetObject is PlayerControl pc) + { + if (!pc.Mod().RpcCheck(callId, TargetClientId, sendOption, num)) + { + return false; + } + } + else if (innerNetObject is PlayerPhysics playerPhysics) + { + if (!playerPhysics.myPlayer.Mod().RpcCheck(callId, TargetClientId, sendOption, num)) + { + return false; + } + } + else if (innerNetObject is CustomNetworkTransform customNetworkTransform && !customNetworkTransform.myPlayer.Mod().RpcCheck(callId, TargetClientId, sendOption, num)) + { + return false; + } + } + } + } + } + return true; + } + + public static void Dispatcher(InnerNetClient __instance, System.Action action) + { + __instance.Dispatcher.Add(action); + } + + public static void PreDispatcher(InnerNetClient __instance, System.Action action) + { + __instance.PreSpawnDispatcher.Add(action); + } + + public static bool Prefix(InnerNetClient __instance, MessageReader reader, SendOption sendOption) + { + if (enableMalformedPacketGuard == false) return true; + + switch (reader.Tag) + { + case 1: + { + int num2 = reader.ReadInt32(); + int num3 = 0; + ClientData clientData = null; + bool flag = false; + if (__instance.GameId == num2) + { + num3 = reader.ReadInt32(); + flag = __instance.AmHost; + __instance.HostId = reader.ReadInt32(); + string playerName = reader.ReadString(); + PlatformSpecificData platformSpecificData = new PlatformSpecificData(); + MessageReader messageReader = reader.ReadMessage(); + platformSpecificData.Platform = (Platforms)messageReader.Tag; + string platformName = messageReader.ReadString(); + platformSpecificData.PlatformName = platformName; + switch (platformSpecificData.Platform) + { + case Platforms.StandaloneWin10: + case Platforms.Xbox: + platformSpecificData.XboxPlatformId = messageReader.ReadUInt64(); + break; + case Platforms.Playstation: + platformSpecificData.PsnPlatformId = messageReader.ReadUInt64(); + break; + } + uint playerLevel = reader.ReadPackedUInt32(); + string productUserId = reader.ReadString(); + string friendCode = reader.ReadString(); + clientData = new ClientData(num3, playerName, platformSpecificData, playerLevel, productUserId, friendCode); + LastJoin[num3] = 0f; + ClientData client = __instance.GetOrCreateClient(clientData); + Debug.Log($"Player {num3} joined. Name - {clientData.PlayerName} with FC {clientData.FriendCode}"); + LastJoin[num3] = 0f; + lock (__instance.Dispatcher) + { + Dispatcher(__instance, delegate + { + __instance.OnPlayerJoined(client); + }); + } + if (!__instance.AmHost || flag) + { + break; + } + lock (__instance.Dispatcher) + { + Dispatcher(__instance, delegate + { + __instance.OnBecomeHost(); + }); + } + } + else + { + __instance.EnqueueDisconnect(DisconnectReasons.IncorrectGame); + } + break; + } + case 5: + case 6: + { + int num = reader.ReadInt32(); + int TargetClientId = 0; + if (__instance.GameId != num) + { + break; + } + if (reader.Tag == 6) + { + try + { + TargetClientId = reader.ReadPackedInt32(); + } + catch + { + break; + } + if (__instance.ClientId != TargetClientId) + { + Debug.Log($"Got data meant for {TargetClientId} for some unknown reason"); + break; + } + } + else + { + TargetClientId = -1; + } + if (__instance.InOnlineScene) + { + MessageReader subReader2 = MessageReader.Get(reader); + lock (__instance.Dispatcher) + { + Dispatcher(__instance, delegate + { + + int num4 = 0; + num4 = subReader2.Position; + if (!CheckDataMessage(subReader2, TargetClientId, sendOption)) + { + subReader2.Recycle(); + } + else + { + subReader2.Position = num4; + HandleGameData.HandleData(__instance, subReader2, TargetClientId, sendOption); + } + }); + } + } + else + { + if (sendOption == SendOption.None) + { + break; + } + MessageReader subReader3 = MessageReader.Get(reader); + lock (__instance.PreSpawnDispatcher) + { + PreDispatcher(__instance, delegate + { + int num4 = 0; + num4 = subReader3.Position; + if (CheckDataMessage(subReader3, TargetClientId, sendOption)) + { + subReader3.Position = num4; + HandleGameData.HandleData(__instance, subReader3, TargetClientId, sendOption); + } + }); + } + } + break; + } + default: + return true; + } + return false; + } + } + +[HarmonyPatch(typeof(InnerNetClient), "HandleGameData")] + internal class HandleGameData + { + + public static bool Prefix(InnerNetClient __instance, MessageReader parentReader) + { + return !enableMalformedPacketGuard; + } + + public static void HandleData(InnerNetClient __instance, MessageReader parentReader, int TargetClientId, SendOption sendOption) + { + try + { + int num = 0; + while (parentReader.Position < parentReader.Length) + { + num++; + MessageReader reader = parentReader.ReadMessageAsNewBuffer(); + int msgNum = __instance.msgNum; + __instance.msgNum = msgNum + 1; + __instance.StartCoroutine(BepInEx.Unity.IL2CPP.Utils.Collections.CollectionExtensions.WrapToIl2Cpp(Handle(__instance, reader, msgNum, TargetClientId, sendOption, num))); + } + } + finally + { + parentReader.Recycle(); + } + } + + public static IEnumerator Handle(InnerNetClient __instance, MessageReader reader, int msgNum, int TargetClientId, SendOption sendOption, int numData) + { + int cnt = 0; + reader.Position = 0; + + switch ((GameDataTypes)reader.Tag) + { + case GameDataTypes.SceneChangeFlag: + { + int num3 = reader.ReadPackedInt32(); + ClientData clientData2 = __instance.FindClientById(num3); + string text = reader.ReadString(); + if (clientData2 != null && !string.IsNullOrWhiteSpace(text)) + { + MonoBehaviourExtensions.StartCoroutine(__instance, AmongUsClientUtils.CoOnPlayerChangedScene(__instance, clientData2, text)); + Debug.Log($"SceneChangeFlag for {num3} to {text}"); + break; + } + Debug.Log($"(SceneChangeFlag) Couldn't find client {num3} to change scene to {text}"); + reader.Recycle(); + break; + } + case GameDataTypes.RpcFlag: + try + { + InnerNetObject value = default; + while (true) + { + uint num2; + try + { + num2 = reader.ReadPackedUInt32(); + } + + catch (Exception) + { + throw; + } + byte b = reader.ReadByte(); + RpcCalls rpcCalls = (RpcCalls)b; + try + { + if (PlayerControl.LocalPlayer) Debug.Log($"RpcFlag ({sendOption}) - ({__instance.FindObjectByNetId(num2).name}({__instance.FindObjectByNetId(num2).NetId})RPC:" + rpcCalls); + } + catch { if (PlayerControl.LocalPlayer) HandleMessage.Crash("Unknown object sent RPC - " + rpcCalls); } + + lock (__instance.allObjects) + { + if (__instance.allObjects.AllObjectsFast.TryGetValue(num2, out value)) + { + value.HandleRpc(b, reader); + goto IL_03b6; + } + if (num2 == uint.MaxValue || __instance.DestroyedObjects.Contains(num2)) + { + goto IL_03b6; + } + if (cnt++ <= 10) + { + reader.Position = 0; + yield return Effects.Wait(0.1f); + continue; + } + break; + IL_03b6: + value = null; + break; + } + } + break; + } + finally + { + reader.Recycle(); + } + default: + // Removed per-packet Debug.Log here. It fired on every unrecognized + // data flag and Unity's Debug.Log is expensive (synchronous write + + // stack trace capture), which caused log spam and micro-stutters. + yield return __instance.HandleGameDataInner(reader, __instance.msgNum); + break; + } + } + } + } +} diff --git a/Plugin.cs b/Plugin.cs new file mode 100644 index 0000000..6b14f34 --- /dev/null +++ b/Plugin.cs @@ -0,0 +1,179 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.5.1")] + public class Plugin : BasePlugin + { + public static ModPlayer modClass; + + public static Plugin Instance { get; private set; } = null!; + public static string ElysiumFolder = ""; + public static ConfigFile MenuConfig; + public static ConfigEntry RpcSpoofDelayConfig; + public static ConfigEntry MenuKeybind; + public static ConfigEntry SpoofedLevel; + public static ConfigEntry EnableLevelSpoofConfig; + public static ConfigEntry EnableFriendCodeSpoofConfig; + public static ConfigEntry SpoofFriendCodeConfig; + public static ConfigEntry EnablePlatformSpoof; + public static ConfigEntry AutoBanBrokenFriendCodeConfig; + public static ConfigEntry PlatformIndex; + public static ConfigEntry ShowWatermarkConfig; + public static ConfigEntry MenuColorIndexConfig; + public static ConfigEntry RgbMenuModeConfig; + public static ConfigEntry RgbMenuTextConfig; + public static ConfigEntry BoldMenuTextConfig; + public static ConfigEntry UnlockCosmeticsConfig; + public static ConfigEntry MoreLobbyInfoConfig; + public static ConfigEntry EnableChatDarkModeConfig; + public static ConfigEntry GhostChatColorConfig; + public static ConfigEntry EnableAnomalyLogReportsConfig; + public static ConfigEntry ShowEspFriendCodeConfig; + + public override void Load() + { + Instance = this; + + ElysiumFolder = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu"); + if (!System.IO.Directory.Exists(ElysiumFolder)) + { + System.IO.Directory.CreateDirectory(ElysiumFolder); + } + + string banFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumModMenuBanList.txt"); + if (!System.IO.File.Exists(banFile)) + { + System.IO.File.Create(banFile).Dispose(); + } + + string platformBanFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumPlatformBanList.txt"); + if (!System.IO.File.Exists(platformBanFile)) + { + System.IO.File.WriteAllText(platformBanFile, "# One custom platform token per line. Matching PlatformName values are host-banned when enabled.\n# Example: github\n"); + } + + string friendEspFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumFriendEspIgnore.txt"); + if (!System.IO.File.Exists(friendEspFile)) + { + System.IO.File.WriteAllText(friendEspFile, "# One nickname, Friend Code, or PUID per line. Matching players will not show ESP info.\n"); + } + + string botBanFile = System.IO.Path.Combine(ElysiumFolder, "ElysiumBotBanList.txt"); + if (!System.IO.File.Exists(botBanFile)) + { + System.IO.File.WriteAllText(botBanFile, "# Auto bot ban list. Format: FriendCode|PUID|Nickname|Date|Reason\n# You can also add one nickname, Friend Code, or PUID per line to always ban matching players.\n"); + } + + string configPath = System.IO.Path.Combine(ElysiumFolder, "ElysiumModMenu.cfg"); + RemoveLegacyPlaintextWebhookConfig(configPath); + MenuConfig = new ConfigFile(configPath, true); + RpcSpoofDelayConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "RpcDelay", 4f, ""); + MenuKeybind = MenuConfig.Bind("ElysiumModMenu.GUI", "Keybind", KeyCode.Insert, ""); + SpoofedLevel = MenuConfig.Bind("ElysiumModMenu.Spoofing", "Level", "100", ""); + EnableLevelSpoofConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "EnableLevelSpoof", true, ""); + EnableFriendCodeSpoofConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "EnableFriendCodeSpoof", false, ""); + SpoofFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "FriendCode", "crewmate01", ""); + EnablePlatformSpoof = MenuConfig.Bind("ElysiumModMenu.Spoofing", "EnablePlatformSpoof", true, ""); + AutoBanBrokenFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Anticheat", "AutoBanBrokenFriendCode", false, ""); + PlatformIndex = MenuConfig.Bind("ElysiumModMenu.Spoofing", "PlatformIndex", 1, ""); + ShowWatermarkConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "ShowWatermark", true, ""); + MenuColorIndexConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "MenuColorIndex", 10, ""); + RgbMenuModeConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "RgbMenuMode", false, ""); + RgbMenuTextConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "RgbMenuText", false, "When true, RGB Menu Mode also recolors menu text."); + BoldMenuTextConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "BoldMenuText", true, "When true, menu text is drawn bold."); + UnlockCosmeticsConfig = MenuConfig.Bind("ElysiumModMenu.General", "UnlockCosmetics", true, ""); + MoreLobbyInfoConfig = MenuConfig.Bind("ElysiumModMenu.Visuals", "MoreLobbyInfo", true, ""); + EnableChatDarkModeConfig = MenuConfig.Bind("ElysiumModMenu.Chat", "EnableChatDarkMode", true, "Turns the custom dark chat input and bubble colors on/off."); + GhostChatColorConfig = MenuConfig.Bind("ElysiumModMenu.Chat", "GhostChatColor", "#D7B8FF", "Hex color for visible ghost chat messages."); + EnableAnomalyLogReportsConfig = MenuConfig.Bind("ElysiumModMenu.Diagnostics", "EnableAnomalyLogReports", true, "Yes/No: sending freeze/overload logs to the mod author. Note: this does not affect your performance, nor does it steal your data or anything like that. It is strictly needed for quick anti-cheat fixes."); + ShowEspFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Visuals", "ShowEspFriendCode", true, "Show Friend Code in ESP player info."); + ClassInjector.RegisterTypeInIl2Cpp(); + ClassInjector.RegisterTypeInIl2Cpp(); + ClassInjector.RegisterTypeInIl2Cpp(); + + var guiObject = new GameObject("ElysiumModMenu_Object"); + UnityEngine.Object.DontDestroyOnLoad(guiObject); + guiObject.hideFlags = HideFlags.HideAndDontSave; + guiObject.AddComponent(); + guiObject.AddComponent(); + + modClass = AddComponent(); + + var harmony = new Harmony("com.elysiummodmenu.harmony"); + harmony.PatchAll(); + } + + private static void RemoveLegacyPlaintextWebhookConfig(string configPath) + { + try + { + if (string.IsNullOrWhiteSpace(configPath) || !System.IO.File.Exists(configPath)) return; + + List lines = System.IO.File.ReadAllLines(configPath).ToList(); + string[] legacyKeys = { "AnomalyLogWebhookUrl", "EnableDiagnostics", "EndpointUrl", "AuthKey", "IncludePuid" }; + bool changed = false; + + for (int i = lines.Count - 1; i >= 0; i--) + { + string trimmed = lines[i].TrimStart(); + if (!legacyKeys.Any(key => trimmed.StartsWith(key, StringComparison.OrdinalIgnoreCase))) continue; + + int start = i; + while (start > 0) + { + string previous = lines[start - 1].TrimStart(); + if (!previous.StartsWith("#") && previous.Length != 0) break; + start--; + if (previous.Length == 0) break; + } + + lines.RemoveRange(start, i - start + 1); + changed = true; + } + + if (!changed) return; + System.IO.File.WriteAllLines(configPath, lines.ToArray()); + } + catch { } + } + } +} diff --git a/Utilities.cs b/Utilities.cs new file mode 100644 index 0000000..bfc06b7 --- /dev/null +++ b/Utilities.cs @@ -0,0 +1,53 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +public static class Extra +{ + public static ModPlayer Mod(this PlayerControl pc) + { + return pc.gameObject.EnsureComponent(); + } + + public static T EnsureComponent(this GameObject obj) where T : Component + { + return obj.GetComponent() ?? obj.AddComponent(); + } + +} diff --git a/anticheat/Acov/AcovProfiler.cs b/anticheat/Acov/AcovProfiler.cs new file mode 100644 index 0000000..2a6c19e --- /dev/null +++ b/anticheat/Acov/AcovProfiler.cs @@ -0,0 +1,48 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +namespace Acov +{ + internal static class AcovProfiler + { + internal static System.IDisposable Sample(string label) { return null; } + } +} diff --git a/anticheat/Acov/Patches/AcovAccessLists.cs b/anticheat/Acov/Patches/AcovAccessLists.cs new file mode 100644 index 0000000..b982252 --- /dev/null +++ b/anticheat/Acov/Patches/AcovAccessLists.cs @@ -0,0 +1,92 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class AcovAccessLists +{ + internal static bool AddBanClient(ClientData target, string reason) + { + try + { + if (target == null) return false; + string fc = target.FriendCode; + if (string.IsNullOrEmpty(fc)) return false; + ElysiumModMenu.ElysiumModMenuGUI.AddToBanList(fc, string.IsNullOrEmpty(target.ProductUserId) ? "Unknown" : target.ProductUserId, string.IsNullOrEmpty(target.PlayerName) ? "Unknown" : target.PlayerName, reason); + return true; + } + catch { return false; } + } + internal static void AddBanIdentity(AcovClientIdentity identity, string reason) + { + try + { + string fc = identity.FriendCode; + if (string.IsNullOrEmpty(fc)) fc = identity.ProductUserId; + if (string.IsNullOrEmpty(fc)) return; + ElysiumModMenu.ElysiumModMenuGUI.AddToBanList(fc, string.IsNullOrEmpty(identity.ProductUserId) ? "Unknown" : identity.ProductUserId, string.IsNullOrEmpty(identity.PlayerName) ? "Unknown" : identity.PlayerName, reason); + } + catch { } + } + internal static string ClientDisplayName(ClientData data, int clientId) + { + try { return data != null && !string.IsNullOrEmpty(data.PlayerName) ? data.PlayerName : ("Client " + clientId); } + catch { return "Client " + clientId; } + } + internal static bool TryHandleJoinMessage(InnerNetClient client, MessageReader reader) { return false; } + internal static void UpdateNickBanChecks() { } +} +} diff --git a/anticheat/Acov/Patches/AcovBugColorGuard.cs b/anticheat/Acov/Patches/AcovBugColorGuard.cs new file mode 100644 index 0000000..e76097d --- /dev/null +++ b/anticheat/Acov/Patches/AcovBugColorGuard.cs @@ -0,0 +1,63 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class AcovBugColorGuard +{ + internal static void Update() { } +} +} diff --git a/anticheat/Acov/Patches/AcovClientIdentity.cs b/anticheat/Acov/Patches/AcovClientIdentity.cs new file mode 100644 index 0000000..2221fec --- /dev/null +++ b/anticheat/Acov/Patches/AcovClientIdentity.cs @@ -0,0 +1,73 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal struct AcovClientIdentity +{ + internal int ClientId; + internal string PlayerName; + internal string FriendCode; + internal string ProductUserId; + internal AcovClientIdentity(int clientId, string playerName, string friendCode, string productUserId) + { + ClientId = clientId; + PlayerName = playerName; + FriendCode = friendCode; + ProductUserId = productUserId; + } +} +} diff --git a/anticheat/Acov/Patches/AcovFakeMapLobby.cs b/anticheat/Acov/Patches/AcovFakeMapLobby.cs new file mode 100644 index 0000000..d7bd531 --- /dev/null +++ b/anticheat/Acov/Patches/AcovFakeMapLobby.cs @@ -0,0 +1,63 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class AcovFakeMapLobby +{ + internal static bool Active { get { return false; } } +} +} diff --git a/anticheat/Acov/Patches/AcovNetPacketMonitor.cs b/anticheat/Acov/Patches/AcovNetPacketMonitor.cs new file mode 100644 index 0000000..8ed4130 --- /dev/null +++ b/anticheat/Acov/Patches/AcovNetPacketMonitor.cs @@ -0,0 +1,64 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class AcovNetPacketMonitor +{ + internal static bool Enabled { get { return false; } } + internal static void RecordInboundPacket(int clientId, MessageReader message) { } +} +} diff --git a/anticheat/Acov/Patches/AcovNotifications.cs b/anticheat/Acov/Patches/AcovNotifications.cs new file mode 100644 index 0000000..c981709 --- /dev/null +++ b/anticheat/Acov/Patches/AcovNotifications.cs @@ -0,0 +1,66 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class AcovNotifications +{ + internal static void Show(string title, string detail, float duration) + { + try { ElysiumModMenu.ElysiumModMenuGUI.ShowNotification("[" + title + "] " + detail); } catch { } + } +} +} diff --git a/anticheat/Acov/Patches/AcovOption.cs b/anticheat/Acov/Patches/AcovOption.cs new file mode 100644 index 0000000..a318d60 --- /dev/null +++ b/anticheat/Acov/Patches/AcovOption.cs @@ -0,0 +1,65 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal sealed class AcovOption +{ + private readonly Func getter; + internal AcovOption(Func getter) { this.getter = getter; } + internal T Value { get { return getter(); } } +} +} diff --git a/anticheat/Acov/Patches/AcovPlayerLevels.cs b/anticheat/Acov/Patches/AcovPlayerLevels.cs new file mode 100644 index 0000000..d76066d --- /dev/null +++ b/anticheat/Acov/Patches/AcovPlayerLevels.cs @@ -0,0 +1,76 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class AcovPlayerLevels +{ + private static readonly Dictionary byClient = new Dictionary(); + internal static void Remember(PlayerControl player, uint level) + { + try { if (player != null) byClient[player.OwnerId] = level; } catch { } + } + internal static bool TryGetDisplayLevel(int clientId, out uint level) + { + return byClient.TryGetValue(clientId, out level); + } + internal static bool TryGetDisplayLevel(PlayerControl player, out uint level) + { + level = 0u; + try { return player != null && byClient.TryGetValue(player.OwnerId, out level); } catch { return false; } + } +} +} diff --git a/anticheat/Acov/Patches/AcovPlayerLoadInfo.cs b/anticheat/Acov/Patches/AcovPlayerLoadInfo.cs new file mode 100644 index 0000000..597cae1 --- /dev/null +++ b/anticheat/Acov/Patches/AcovPlayerLoadInfo.cs @@ -0,0 +1,64 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal struct AcovPlayerLoadInfo +{ + internal bool Connected; + internal bool Ready; +} +} diff --git a/anticheat/Acov/Patches/AcovPlayerLoadStates.cs b/anticheat/Acov/Patches/AcovPlayerLoadStates.cs new file mode 100644 index 0000000..e848f1d --- /dev/null +++ b/anticheat/Acov/Patches/AcovPlayerLoadStates.cs @@ -0,0 +1,68 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class AcovPlayerLoadStates +{ + internal static AcovPlayerLoadInfo Evaluate(ClientData client) + { + AcovPlayerLoadInfo info = default(AcovPlayerLoadInfo); + try { info.Connected = client != null; info.Ready = client != null && client.Character != null; } catch { } + return info; + } +} +} diff --git a/anticheat/Acov/Patches/AcovPlugin.cs b/anticheat/Acov/Patches/AcovPlugin.cs new file mode 100644 index 0000000..3103215 --- /dev/null +++ b/anticheat/Acov/Patches/AcovPlugin.cs @@ -0,0 +1,63 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class AcovPlugin +{ + internal static readonly BepInEx.Logging.ManualLogSource Logger = BepInEx.Logging.Logger.CreateLogSource("AntiCheatOldVersion"); +} +} diff --git a/anticheat/Acov/Patches/AcovSecurityNotifications.cs b/anticheat/Acov/Patches/AcovSecurityNotifications.cs new file mode 100644 index 0000000..3e7af27 --- /dev/null +++ b/anticheat/Acov/Patches/AcovSecurityNotifications.cs @@ -0,0 +1,71 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class AcovSecurityNotifications +{ + internal static void Show(string action, string name, string title, string detail, int clientId) + { + try + { + string who = string.IsNullOrEmpty(name) ? "" : (" " + name); + ElysiumModMenu.ElysiumModMenuGUI.ShowNotification("[" + title + "]" + who + ": " + detail); + } + catch { } + } +} +} diff --git a/anticheat/Acov/Patches/AcovText.cs b/anticheat/Acov/Patches/AcovText.cs new file mode 100644 index 0000000..47ae05c --- /dev/null +++ b/anticheat/Acov/Patches/AcovText.cs @@ -0,0 +1,63 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class AcovText +{ + internal static string T(string rus, string eng) { return ElysiumModMenu.ElysiumModMenuGUI.L(eng, rus); } +} +} diff --git a/anticheat/Acov/Patches/CntPositionSanityPatch.cs b/anticheat/Acov/Patches/CntPositionSanityPatch.cs new file mode 100644 index 0000000..19b5f49 --- /dev/null +++ b/anticheat/Acov/Patches/CntPositionSanityPatch.cs @@ -0,0 +1,102 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +[HarmonyPatch(typeof(CustomNetworkTransform), "Deserialize")] +internal static class CntPositionSanityPatch +{ + private const float MaxCoord = 1000f; + + public static void Postfix(CustomNetworkTransform __instance) + { + if (!ElysiumModMenu.ElysiumModMenuGUI.oldAntiCheatVersion) return; + try + { + if (__instance == null) + { + return; + } + + PlayerControl pl = __instance.myPlayer; + Transform t = pl != null ? ((Component)pl).transform : ((Component)__instance).transform; + if (t == null) + { + return; + } + + Vector3 p = t.position; + if (float.IsNaN(p.x) || float.IsNaN(p.y) || float.IsNaN(p.z) || + float.IsInfinity(p.x) || float.IsInfinity(p.y) || float.IsInfinity(p.z)) + { + t.position = Vector3.zero; + return; + } + + if (Mathf.Abs(p.x) > MaxCoord || Mathf.Abs(p.y) > MaxCoord || Mathf.Abs(p.z) > MaxCoord) + { + t.position = new Vector3( + Mathf.Clamp(p.x, -MaxCoord, MaxCoord), + Mathf.Clamp(p.y, -MaxCoord, MaxCoord), + Mathf.Clamp(p.z, -MaxCoord, MaxCoord)); + } + } + catch + { + } + } +} +} diff --git a/anticheat/Acov/Patches/HarmonyControl.cs b/anticheat/Acov/Patches/HarmonyControl.cs new file mode 100644 index 0000000..979d026 --- /dev/null +++ b/anticheat/Acov/Patches/HarmonyControl.cs @@ -0,0 +1,64 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class HarmonyControl +{ + internal const bool Continue = true; + internal const bool SkipOriginal = false; +} +} diff --git a/anticheat/Acov/Patches/KnownModRpcCatalog.cs b/anticheat/Acov/Patches/KnownModRpcCatalog.cs new file mode 100644 index 0000000..0e94973 --- /dev/null +++ b/anticheat/Acov/Patches/KnownModRpcCatalog.cs @@ -0,0 +1,68 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class KnownModRpcCatalog +{ + internal static bool TryFind(byte rpcByte, out int ruleIndex, out KnownModRpcRule rule) + { + ruleIndex = -1; + rule = default(KnownModRpcRule); + return false; + } +} +} diff --git a/anticheat/Acov/Patches/KnownModRpcRule.cs b/anticheat/Acov/Patches/KnownModRpcRule.cs new file mode 100644 index 0000000..13a1138 --- /dev/null +++ b/anticheat/Acov/Patches/KnownModRpcRule.cs @@ -0,0 +1,63 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal struct KnownModRpcRule +{ + internal string DisplayName; +} +} diff --git a/anticheat/Acov/Patches/LobbyTeleportPositionTracker.cs b/anticheat/Acov/Patches/LobbyTeleportPositionTracker.cs new file mode 100644 index 0000000..3c2baa7 --- /dev/null +++ b/anticheat/Acov/Patches/LobbyTeleportPositionTracker.cs @@ -0,0 +1,130 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class LobbyTeleportPositionTracker +{ + private const float TeleportThreshold = 5f; + private const float SpawnGraceSeconds = 3f; + private static readonly Dictionary lastPos = new Dictionary(); + private static readonly Dictionary firstSeenAt = new Dictionary(); + private static int trackedGameId = -1; + + internal static void Reset() + { + lastPos.Clear(); + firstSeenAt.Clear(); + trackedGameId = -1; + } + + internal static void ForgetPlayer(byte playerId) + { + lastPos.Remove(playerId); + firstSeenAt.Remove(playerId); + } + + internal static void CheckPlayer(PlayerControl player) + { + try + { + if (player == null || player == PlayerControl.LocalPlayer) return; + if (LobbyBehaviour.Instance == null) return; + if (ModOptions.LobbyTeleportDetection == null || !ModOptions.LobbyTeleportDetection.Value) return; + + int gameId = AmongUsClient.Instance != null ? ((InnerNetClient)AmongUsClient.Instance).GameId : -1; + if (gameId != trackedGameId) + { + trackedGameId = gameId; + lastPos.Clear(); + firstSeenAt.Clear(); + } + + float now = Time.realtimeSinceStartup; + byte pid = player.PlayerId; + Vector2 cur = ((Component)player).transform.position; + + if (!firstSeenAt.ContainsKey(pid)) + { + firstSeenAt[pid] = now; + lastPos[pid] = cur; + return; + } + + if (now - firstSeenAt[pid] < SpawnGraceSeconds) + { + lastPos[pid] = cur; + return; + } + + if (!lastPos.TryGetValue(pid, out Vector2 prev)) + { + lastPos[pid] = cur; + return; + } + + lastPos[pid] = cur; + + float dist = Vector2.Distance(prev, cur); + if (dist < TeleportThreshold) return; + + int clientId = NetworkProtectionGuard.ResolvePlayerClientId(player); + NetworkProtectionGuard.FlagLobbyTeleport(player, clientId, dist); + } + catch { } + } +} +} diff --git a/anticheat/Acov/Patches/MinLevelKickGuard.cs b/anticheat/Acov/Patches/MinLevelKickGuard.cs new file mode 100644 index 0000000..267c355 --- /dev/null +++ b/anticheat/Acov/Patches/MinLevelKickGuard.cs @@ -0,0 +1,64 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class MinLevelKickGuard +{ + internal static void Check(PlayerControl player, uint level) { } + internal static void Update() { } +} +} diff --git a/anticheat/Acov/Patches/ModOptions.cs b/anticheat/Acov/Patches/ModOptions.cs new file mode 100644 index 0000000..874129a --- /dev/null +++ b/anticheat/Acov/Patches/ModOptions.cs @@ -0,0 +1,96 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class ModOptions +{ + internal static readonly AcovOption NetworkProtection = new AcovOption(() => ElysiumModMenu.ElysiumModMenuGUI.oldAntiCheatVersion); + internal static readonly AcovOption FloodDropNonHost = new AcovOption(() => true); + internal static readonly AcovOption NonHostDataDrop = new AcovOption(() => true); + internal static readonly AcovOption LobbyTeleportDetection = new AcovOption(() => true); + internal static readonly AcovOption NetworkProtectionAction = null; + internal static readonly AcovOption BanBrokenFriendCode = null; + internal static readonly AcovOption LevelRpcProtection = null; + internal static readonly AcovOption LevelRpcAction = null; + internal static readonly AcovOption IdenticalNetIdProtection = null; + internal static readonly AcovOption ChatSpoofProtection = null; + internal static readonly AcovOption ChatSpoofAction = null; + internal static readonly AcovOption CosmeticSpoofProtection = null; + internal static readonly AcovOption CosmeticSpoofAction = null; + internal static readonly AcovOption MalformedDataAction = null; + internal static readonly AcovOption QuickChatAction = null; + internal static readonly AcovOption LobbyTeleportAction = null; + internal static readonly AcovOption SnapToAction = null; + internal static readonly AcovOption VentGuard = null; + internal static readonly AcovOption SpawnFloodGuard = null; + internal static readonly AcovOption MaxAllowedLevelRpc = null; + internal static readonly AcovOption SuspiciousLevelList = null; + internal static ConfigEntry[] KnownModRpcActions { get { return null; } } + internal static string NormalizeNetworkProtectionAction(string action) + { + if (string.IsNullOrWhiteSpace(action)) return "Warn"; + switch (action.Trim().ToLowerInvariant()) + { + case "null": case "none": case "off": case "ignore": return "Null"; + case "warn": case "notify": return "Warn"; + case "kick": return "Kick"; + case "ban": return "Ban"; + default: return "Warn"; + } + } +} +} diff --git a/anticheat/Acov/Patches/NetworkInboundSenderPatch.cs b/anticheat/Acov/Patches/NetworkInboundSenderPatch.cs new file mode 100644 index 0000000..65f7c8c --- /dev/null +++ b/anticheat/Acov/Patches/NetworkInboundSenderPatch.cs @@ -0,0 +1,69 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +[HarmonyPatch(typeof(InnerNetClient), "OnMessageReceived")] +internal static class NetworkInboundSenderPatch +{ + public static void Prefix(InnerNetClient __instance, [HarmonyArgument(0)] DataReceivedEventArgs e) + { + if (!ElysiumModMenu.ElysiumModMenuGUI.oldAntiCheatVersion) return; + using (Acov.AcovProfiler.Sample("Net.TrackSender")) + NetworkProtectionGuard.TrackInboundSender(__instance, e); + } +} +} diff --git a/anticheat/Acov/Patches/NetworkJoinHygienePatch.cs b/anticheat/Acov/Patches/NetworkJoinHygienePatch.cs new file mode 100644 index 0000000..2830d79 --- /dev/null +++ b/anticheat/Acov/Patches/NetworkJoinHygienePatch.cs @@ -0,0 +1,68 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +[HarmonyPatch(typeof(AmongUsClient), "OnPlayerJoined")] +internal static class NetworkJoinHygienePatch +{ + public static void Postfix([HarmonyArgument(0)] ClientData client) + { + if (!ElysiumModMenu.ElysiumModMenuGUI.oldAntiCheatVersion) return; + NetworkProtectionGuard.TrackClientJoined(client); + } +} +} diff --git a/anticheat/Acov/Patches/NetworkMessageProtectionPatch.cs b/anticheat/Acov/Patches/NetworkMessageProtectionPatch.cs new file mode 100644 index 0000000..4126059 --- /dev/null +++ b/anticheat/Acov/Patches/NetworkMessageProtectionPatch.cs @@ -0,0 +1,69 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +[HarmonyPatch(typeof(InnerNetClient), "HandleMessage")] +internal static class NetworkMessageProtectionPatch +{ + public static bool Prefix(InnerNetClient __instance, MessageReader reader, SendOption sendOption) + { + if (!ElysiumModMenu.ElysiumModMenuGUI.oldAntiCheatVersion) return true; + using (Acov.AcovProfiler.Sample("Net.CheckMessage")) + return NetworkProtectionGuard.CheckMessage(__instance, reader, sendOption); + } +} +} diff --git a/anticheat/Acov/Patches/NetworkProtectionCleanupDriver.cs b/anticheat/Acov/Patches/NetworkProtectionCleanupDriver.cs new file mode 100644 index 0000000..8f4fddd --- /dev/null +++ b/anticheat/Acov/Patches/NetworkProtectionCleanupDriver.cs @@ -0,0 +1,74 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +public sealed class NetworkProtectionCleanupDriver : MonoBehaviour +{ + public void Update() + { + if (!ElysiumModMenu.ElysiumModMenuGUI.oldAntiCheatVersion) return; + using (Acov.AcovProfiler.Sample("Net.Cleanup")) + { + NetworkProtectionGuard.UpdateLocalCleanup(); + NetworkProtectionGuard.UpdateJoinIntegrityChecks(); + MinLevelKickGuard.Update(); + AcovBugColorGuard.Update(); + AcovAccessLists.UpdateNickBanChecks(); + } + } +} +} diff --git a/anticheat/Acov/Patches/NetworkProtectionGuard.Floods.cs b/anticheat/Acov/Patches/NetworkProtectionGuard.Floods.cs new file mode 100644 index 0000000..a79a03d --- /dev/null +++ b/anticheat/Acov/Patches/NetworkProtectionGuard.Floods.cs @@ -0,0 +1,1459 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static partial class NetworkProtectionGuard +{ + +private static bool IsLobbyJoinSyncGrace(PlayerControl player = null, int clientId = -1) + { + UpdateLobbySyncState(); + if (LobbyBehaviour.Instance == null) + { + return false; + } + + float now = Time.realtimeSinceStartup; + if (now - lastLobbyEnteredAt < LobbyEnteredSyncGraceSeconds) + { + return true; + } + + if (now - lastLobbyJoinAt < LobbyRecentJoinSyncGraceSeconds) + { + return true; + } + + if (clientId >= 0 && ClientJoinTimeAt.TryGetValue(clientId, out float joinedAt) && now - joinedAt < LobbyJoinWarmupSeconds) + { + return true; + } + + ClientData client = clientId >= 0 ? GetClientById(clientId) : GetClient(player); + if (client == null || ClientIdentityReady(client)) + { + return false; + } + + if (ClientJoinTimeAt.TryGetValue(client.Id, out joinedAt)) + { + return now - joinedAt < LobbyJoinWarmupSeconds; + } + + return now - lastLobbyJoinAt < LobbyJoinWarmupSeconds; + } + + private static void UpdateLobbySyncState() + { + InnerNetClient client = (InnerNetClient)AmongUsClient.Instance; + if (client == null) + { + lastObservedLobbyGameState = -1; + lastLobbyEnteredAt = -1000f; + return; + } + + int gameState = (int)client.GameState; + if (gameState == lastObservedLobbyGameState) + { + return; + } + + lastObservedLobbyGameState = gameState; + if (gameState == 1) + { + lastLobbyEnteredAt = Time.realtimeSinceStartup; + } + } + + private static bool ShouldBlockPlayerSemanticRpc(PlayerControl player, byte rpcByte, int clientId, MessageReader reader) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || player == null) + { + return false; + } + + if (NameStringRpcIds.Contains(rpcByte)) + { + try + { + int size = reader != null ? reader.Length : 0; + if (size > MaxRpcStringBytes) + { + BlockRpc(player, clientId, "Oversized name RPC", $"{size} bytes (limit {MaxRpcStringBytes}).", "Null"); + return true; + } + } + catch + { + } + } + + if (rpcByte == 38 && TryReadLevel(reader, out uint level)) + { + if (LevelRpcProtectionEnabled() && IsClearlyInvalidLevel(level)) + { + BlockRpc(player, clientId, "Invalid level RPC", $"Level {level}, max {MaxAllowedLevelRpc()}.", LevelRpcProtectionAction()); + return true; + } + + AcovPlayerLevels.Remember(player, level); + MinLevelKickGuard.Check(player, level); + } + + if (rpcByte == 1) + { + float now = Time.fixedTime; + if (LastTaskRpcAt.TryGetValue(player.PlayerId, out float lastTask) && now - lastTask < 0.7f) + { + BlockRpc(player, clientId, "Task spam", $"{now - lastTask:0.00}s between tasks."); + return true; + } + + if (MeetingOrExileActive()) + { + BlockRpc(player, clientId, "Task during meeting", "CompleteTask was blocked."); + return true; + } + + if (IsImpostor(player)) + { + BlockRpc(player, clientId, "Impostor task RPC", "CompleteTask was blocked."); + return true; + } + + LastTaskRpcAt[player.PlayerId] = now; + return false; + } + + if (rpcByte == 11) + { + if (MeetingOrExileActive()) + { + AcovPlugin.Logger?.LogWarning((object)$"Network protection ignored duplicate meeting/report RPC from {PlayerName(player)} (client {clientId}) while meeting UI is active."); + return false; + } + + if (TryReadReportedPlayerId(reader, out byte reportedPlayerId)) + { + if (reportedPlayerId == byte.MaxValue) + { + try + { + if (player.RemainingEmergencies <= 0) + { + BlockRpc(player, clientId, "Invalid emergency RPC", $"Remaining emergencies: {player.RemainingEmergencies}."); + return true; + } + } + catch + { + } + } + else if (!PlayerExists(reportedPlayerId)) + { + BlockRpc(player, clientId, "Invalid body report", $"PlayerId {reportedPlayerId} does not exist."); + return true; + } + } + } + + if (rpcByte == 12 && ShouldBlockMurderRpc(player, clientId, reader)) + { + return true; + } + + if (rpcByte == 13) + { + return ShouldBlockChatRpc(player, clientId, reader); + } + + if (CosmeticSpoofProtectionEnabled() && !SenderAttributionAmbiguous() && CosmeticMutationRpcIds.Contains(rpcByte)) + { + int cosmeticOwnerClientId = GetPlayerClientId(player); + if (clientId >= 0 && cosmeticOwnerClientId >= 0 && clientId != cosmeticOwnerClientId && !IsLobbyJoinSyncGrace(player, clientId)) + { + BlockRpc(player, clientId, "Cosmetic spoofing", $"client {clientId} set {RpcName(rpcByte)} on {PlayerName(player)} (owner {cosmeticOwnerClientId}).", CosmeticSpoofProtectionAction()); + return true; + } + } + + if (CosmeticMutationRpcIds.Contains(rpcByte) && InActiveMatch() && !AcovFakeMapLobby.Active && !MeetingOrExileActive() && IntroCutscene.Instance == null) + { + BlockRpc(player, clientId, "Match cosmetic RPC", $"{RpcName(rpcByte)} while match is running."); + return true; + } + + if (rpcByte == 45 && ShouldBlockProtectRpc(player, clientId, reader)) + { + return true; + } + + if ((rpcByte == 46 || rpcByte == 55) && ShouldBlockShapeshiftRpc(player, rpcByte, clientId, reader)) + { + return true; + } + + if ((rpcByte == 63 || rpcByte == 65) && MeetingOrExileActive()) + { + BlockRpc(player, clientId, "Meeting invisibility RPC", $"{RpcName(rpcByte)} was blocked."); + return true; + } + + return false; + } + + private static bool ShouldBlockMurderRpc(PlayerControl player, int clientId, MessageReader reader) + { + if (MeetingOrExileActive()) + { + BlockRpc(player, clientId, "Kill during meeting", "MurderPlayer was blocked."); + return true; + } + + if (!TryReadPlayerObject(reader, out PlayerControl target) || target == null) + { + return false; + } + + if (SamePlayer(player, target)) + { + BlockRpc(player, clientId, "Self kill RPC", "MurderPlayer target equals sender."); + return true; + } + + try + { + if (player.Data?.Role is PhantomRole phantom && phantom.IsInvisible) + { + BlockRpc(player, clientId, "Invisible kill RPC", "MurderPlayer was sent while phantom is invisible."); + return true; + } + } + catch + { + } + + if (IsImpostor(target)) + { + BlockRpc(player, clientId, "Kill impostor RPC", $"{PlayerName(target)} is impostor."); + return true; + } + + return false; + } + + private static bool ShouldBlockProtectRpc(PlayerControl player, int clientId, MessageReader reader) + { + if (MeetingOrExileActive()) + { + BlockRpc(player, clientId, "Protect during meeting", "ProtectPlayer was blocked."); + return true; + } + + if (TryReadPlayerObject(reader, out PlayerControl target) && target != null && SamePlayer(player, target)) + { + BlockRpc(player, clientId, "Self protect RPC", "ProtectPlayer target equals sender."); + return true; + } + + try + { + if (player.Data?.Role == null || (int)player.Data.Role.Role != 4) + { + BlockRpc(player, clientId, "Invalid protect RPC", "ProtectPlayer was sent by a non-guardian role."); + return true; + } + } + catch + { + } + + return false; + } + + private static bool ShouldBlockShapeshiftRpc(PlayerControl player, byte rpcByte, int clientId, MessageReader reader) + { + if (!TryReadPlayerObject(reader, out PlayerControl target) || target == null) + { + return false; + } + + bool animated = true; + try + { + MessageReader copy = MessageReader.Get(reader); + MessageExtensions.ReadNetObject(copy); + animated = copy.ReadBoolean(); + } + catch + { + } + + if (MeetingOrExileActive() && !SamePlayer(player, target)) + { + BlockRpc(player, clientId, "Shapeshift during meeting", "Targeted shapeshift was blocked."); + return true; + } + + if (!animated && (rpcByte == 46 || !SamePlayer(player, target))) + { + BlockRpc(player, clientId, "Silent shapeshift", $"{PlayerName(target)} without animation."); + return true; + } + + return false; + } + + private static bool ShouldBlockCrashRpc(PlayerControl player, byte rpcByte, int clientId) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + return false; + } + + if (rpcByte == 4 && MeetingHud.Instance == null && !((InnerNetObject)player).AmOwner) + { + BlockRpc(player, clientId, "Exiled вне встречи", "RPC заблокирован."); + return true; + } + + return false; + } + + private static bool ShouldBlockChatRpc(PlayerControl player, int clientId, MessageReader reader) + { + if (ChatSpoofProtectionEnabled()) + { + int ownerClientId = GetPlayerClientId(player); + if (clientId >= 0 && ownerClientId >= 0 && clientId != ownerClientId && !IsLobbyJoinSyncGrace(player, clientId)) + { + BlockRpc(player, clientId, "Chat spoofing", $"client {clientId} sent chat as {PlayerName(player)} (owner {ownerClientId}).", ChatSpoofProtectionAction()); + return true; + } + } + + if (clientId >= 0 && HitLimit(ChatByClient, clientId, Time.realtimeSinceStartup, ChatWindowSeconds, MaxChatMessagesPerWindow, out int chatCount)) + { + BlockRpc(player, clientId, "Chat flood", $"{chatCount}/{MaxChatMessagesPerWindow} за {ChatWindowSeconds:0.0}с."); + return true; + } + + try + { + MessageReader copy = MessageReader.Get(reader); + string text = copy.ReadString(); + if (IsDangerousChat(text)) + { + BlockRpc(player, clientId, "Опасный чат", "Форматирование сообщения заблокировано."); + return true; + } + } + catch (Exception error) + { + BlockRpc(player, clientId, "Поврежденный чат", error.Message); + return true; + } + + return false; + } + + private static bool TryReadLevel(MessageReader reader, out uint level) + { + level = 0; + if (reader == null) + { + return false; + } + + try + { + MessageReader copy = MessageReader.Get(reader); + level = copy.ReadPackedUInt32(); + return true; + } + catch + { + return false; + } + } + + private static string suspiciousLevelsRaw; + +private static HashSet suspiciousLevels; + +private static bool IsPopularSpoofLevel(uint level) + { + string raw = ModOptions.SuspiciousLevelList?.Value ?? string.Empty; + if (suspiciousLevels == null || !string.Equals(raw, suspiciousLevelsRaw, StringComparison.Ordinal)) + { + suspiciousLevelsRaw = raw; + suspiciousLevels = ParseSuspiciousLevels(raw); + } + + return suspiciousLevels.Contains(level); + } + +private static HashSet ParseSuspiciousLevels(string raw) + { + HashSet set = new HashSet(); + if (string.IsNullOrWhiteSpace(raw)) + { + return set; + } + + string[] parts = raw.Split(','); + for (int i = 0; i < parts.Length; i++) + { + if (uint.TryParse(parts[i].Trim(), out uint value)) + { + set.Add(value); + } + } + + return set; + } + +private static bool IsClearlyInvalidLevel(uint level) + { + return level > MaxAllowedLevelRpc() || IsPopularSpoofLevel(level); + } + +private static uint MaxAllowedLevelRpc() + { + try + { + return (uint)Mathf.Clamp(ModOptions.MaxAllowedLevelRpc?.Value ?? 10000, 100, 10000); + } + catch + { + return 10000u; + } + } + +private static bool TryReadReportedPlayerId(MessageReader reader, out byte playerId) + { + playerId = byte.MaxValue; + if (reader == null) + { + return false; + } + + try + { + MessageReader copy = MessageReader.Get(reader); + playerId = copy.ReadByte(); + return true; + } + catch + { + return false; + } + } + +private static bool TryReadPlayerObject(MessageReader reader, out PlayerControl player) + { + player = null; + if (reader == null) + { + return false; + } + + try + { + MessageReader copy = MessageReader.Get(reader); + player = MessageExtensions.ReadNetObject(copy); + return player != null; + } + catch + { + return false; + } + } + +private static bool SamePlayer(PlayerControl left, PlayerControl right) + { + return left != null && right != null && left.PlayerId == right.PlayerId; + } + +private static bool MeetingOrExileActive() + { + try + { + if (MeetingHud.Instance != null && (int)MeetingHud.Instance.state != 0) + { + return true; + } + } + catch + { + if (MeetingHud.Instance != null) + { + return true; + } + } + + return ExileController.Instance != null; + } + +private static void NoteMeetingRpc() + { + lastMeetingRpcAt = Time.realtimeSinceStartup; + } + +internal static bool IsMeetingTransitionGrace() + { + return MeetingOrExileActive() || Time.realtimeSinceStartup - lastMeetingRpcAt < MeetingTransitionGraceSeconds; + } + +private static bool InActiveMatch() + { + return ShipStatus.Instance != null && LobbyBehaviour.Instance == null; + } + +private static bool IsImpostor(PlayerControl player) + { + try + { + return player != null && player.Data != null && player.Data.Role != null && player.Data.Role.IsImpostor; + } + catch + { + return false; + } + } + +private static bool IsDead(PlayerControl player) + { + try + { + return player != null && player.Data != null && player.Data.IsDead; + } + catch + { + return false; + } + } + +private static bool PlayerExists(byte playerId) + { + try + { + if (GameData.Instance != null && GameData.Instance.AllPlayers != null) + { + var infos = GameData.Instance.AllPlayers.GetEnumerator(); + while (infos.MoveNext()) + { + NetworkedPlayerInfo info = infos.Current; + if (info != null && info.PlayerId == playerId) + { + return true; + } + } + } + } + catch + { + } + + if (PlayerControl.AllPlayerControls == null) + { + return false; + } + + try + { + var players = PlayerControl.AllPlayerControls.GetEnumerator(); + while (players.MoveNext()) + { + PlayerControl player = players.Current; + if (player != null && player.PlayerId == playerId && player.Data != null && !player.Data.Disconnected) + { + return true; + } + } + } + catch + { + } + + return false; + } + +private static void BlockSystemRpc(PlayerControl actor, int clientId, string title, string detail) + { + if (actor != null) + { + BlockRpc(actor, clientId, title, detail); + } + else + { + BlockMessage(clientId, title, detail); + } + } + +private static int CurrentMapId() + { + try + { + return GameOptionsManager.Instance.CurrentGameOptions.MapId; + } + catch + { + return -1; + } + } + +private static bool SabotagePayloadFitsMap(int systemId, byte amount, int mapId) + { + switch (systemId) + { + case 3: + return mapId != 2 && mapId != 4 && IsStandardSabotageAmount(amount); + case 7: + return mapId < 5 && amount < 5; + case 8: + return mapId != 2 && mapId < 4 && IsReducedSabotageAmount(amount); + case 14: + if (amount == 0) + { + return mapId != 1 && mapId < 5; + } + + return (mapId == 1 || mapId >= 5) && IsExtendedSabotageAmount(amount); + case 21: + return mapId == 2 && IsStandardSabotageAmount(amount); + case 57: + return false; + case 58: + return mapId == 4 && IsExtendedSabotageAmount(amount); + default: + return true; + } + } + +private static bool IsCrewSabotagePayload(int systemId, byte amount) + { + if (systemId != 17) + { + return false; + } + + switch (amount) + { + case 14: + case 21: + case 57: + case 58: + case 3: + case 7: + case 8: + return true; + default: + return false; + } + } + +private static bool IsReducedSabotageAmount(byte amount) + { + return amount == 64 || amount == 65; + } + +private static bool IsStandardSabotageAmount(byte amount) + { + return amount == 64 || amount == 65 || amount == 32 || amount == 33; + } + +private static bool IsExtendedSabotageAmount(byte amount) + { + return IsStandardSabotageAmount(amount) || amount == 16 || amount == 17; + } + +private static bool HasBrokenFriendCode(ClientData client) + { + string friendCode = null; + try + { + friendCode = client?.FriendCode; + } + catch + { + } + + if (string.IsNullOrWhiteSpace(friendCode) || friendCode.Length < 7 || friendCode.Length > 32) + { + return true; + } + + int hashIndex = friendCode.IndexOf('#'); + if (hashIndex <= 0 || hashIndex != friendCode.LastIndexOf('#') || hashIndex >= friendCode.Length - 1) + { + return true; + } + + string prefix = friendCode.Substring(0, hashIndex); + for (int i = 0; i < prefix.Length; i++) + { + char ch = prefix[i]; + if (!char.IsLetter(ch) && ch != '_' && ch != '-') + { + return true; + } + } + + return false; + } + +private static bool FriendCodeReady(ClientData client) + { + try + { + return !string.IsNullOrWhiteSpace(client?.FriendCode); + } + catch + { + return false; + } + } + +private static bool ClientIdentityReady(ClientData client) + { + try + { + AcovPlayerLoadInfo info = AcovPlayerLoadStates.Evaluate(client); + return info.Connected && info.Ready; + } + catch + { + return true; + } + } + +private static (int Limit, float Window) RpcSpamLimit(int callId) + { + if (HighVolumeRpcLimits.TryGetValue(callId, out (int Limit, float Window) rule)) + { + return rule; + } + + return (DefaultRpcLimitPerWindow, DefaultRpcWindowSeconds); + } + +private static bool HitLimit(Dictionary> store, int key, float now, float windowSeconds, int limit, out int count) + { + if (!store.TryGetValue(key, out Queue hits)) + { + hits = new Queue(); + store[key] = hits; + } + + while (hits.Count > 0 && now - hits.Peek() > windowSeconds) + { + hits.Dequeue(); + } + + hits.Enqueue(now); + count = hits.Count; + return count > limit; + } + +private static bool HitSameRpcLimit(int clientId, int callId, float now, float windowSeconds, int limit, out int count) + { + if (!SameRpcByClient.TryGetValue(clientId, out Dictionary> byRpc)) + { + byRpc = new Dictionary>(); + SameRpcByClient[clientId] = byRpc; + } + + if (!byRpc.TryGetValue(callId, out Queue hits)) + { + hits = new Queue(); + byRpc[callId] = hits; + } + + while (hits.Count > 0 && now - hits.Peek() > windowSeconds) + { + hits.Dequeue(); + } + + hits.Enqueue(now); + count = hits.Count; + return count > limit; + } + +private static bool ReaderLooksSane(MessageReader reader) + { + return reader != null && reader.Position >= 0 && reader.Position <= reader.Length; + } + +private static bool Enabled() + { + return (ModOptions.NetworkProtection == null || ModOptions.NetworkProtection.Value) + && AmongUsClient.Instance != null + && AmongUsClient.Instance.AmHost; + } + +private static bool EnabledNonHostFloodDrop() + { + return ModOptions.FloodDropNonHost != null + && ModOptions.FloodDropNonHost.Value + && AmongUsClient.Instance != null + && !AmongUsClient.Instance.AmHost; + } + +private static bool BrokenFriendCodeBanEnabled() + { + return ModOptions.BanBrokenFriendCode == null || ModOptions.BanBrokenFriendCode.Value; + } + +private static bool LevelRpcProtectionEnabled() + { + return ModOptions.LevelRpcProtection == null || ModOptions.LevelRpcProtection.Value; + } + +private static bool IdenticalNetIdProtectionEnabled() + { + return ModOptions.IdenticalNetIdProtection == null || ModOptions.IdenticalNetIdProtection.Value; + } + +private static bool ChatSpoofProtectionEnabled() + { + return ModOptions.ChatSpoofProtection == null || ModOptions.ChatSpoofProtection.Value; + } + +private static string ChatSpoofProtectionAction() + { + return ModOptions.ChatSpoofAction == null ? "Kick" : ModOptions.NormalizeNetworkProtectionAction(ModOptions.ChatSpoofAction.Value); + } + +private static bool CosmeticSpoofProtectionEnabled() + { + return ModOptions.CosmeticSpoofProtection == null || ModOptions.CosmeticSpoofProtection.Value; + } + +private static string CosmeticSpoofProtectionAction() + { + return ModOptions.CosmeticSpoofAction == null ? "Warn" : ModOptions.NormalizeNetworkProtectionAction(ModOptions.CosmeticSpoofAction.Value); + } + +private static bool SenderAttributionAmbiguous() + { + return string.IsNullOrEmpty(activeInboundConnectionKey) + || AmbiguousConnectionKeys.Contains(activeInboundConnectionKey); + } + +private static bool TryHandleKnownModRpc(PlayerControl player, byte rpcByte, int clientId, out bool skipOriginal) + { + skipOriginal = false; + + if (!KnownModRpcCatalog.TryFind(rpcByte, out int ruleIndex, out KnownModRpcRule rule)) + { + return false; + } + + string action = KnownModRpcAction(ruleIndex); + if (action == "Null") + { + return true; + } + + string warnKey = clientId >= 0 ? $"{clientId}:{rule.DisplayName}" : null; + bool firstTime = warnKey == null || WarnedModRpcOnce.Add(warnKey); + if (!firstTime && action == "Warn") + { + skipOriginal = true; + return true; + } + + BlockRpc(player, clientId, "Mod CallRpc", $"{rule.DisplayName}: RPC {rpcByte}.", action); + skipOriginal = true; + return true; + } + +private static string KnownModRpcAction(int ruleIndex) + { + try + { + ConfigEntry[] actions = ModOptions.KnownModRpcActions; + if (actions != null && ruleIndex >= 0 && ruleIndex < actions.Length && actions[ruleIndex] != null) + { + return ModOptions.NormalizeNetworkProtectionAction(actions[ruleIndex].Value); + } + } + catch + { + } + + return "Warn"; + } + +private static string ProtectionAction() + { + return ModOptions.NetworkProtectionAction == null ? "Ban" : ModOptions.NormalizeNetworkProtectionAction(ModOptions.NetworkProtectionAction.Value); + } + +private static string LevelRpcProtectionAction() + { + return ModOptions.LevelRpcAction == null ? "Ban" : ModOptions.NormalizeNetworkProtectionAction(ModOptions.LevelRpcAction.Value); + } + +private static void BlockMessage(int clientId, string title, string detail) + { + BlockMessage(clientId, title, detail, null); + } + +private static void BlockMessage(int clientId, string title, string detail, string actionOverride) + { + clientId = ResolveBestActiveClientId(clientId); + string action = string.IsNullOrWhiteSpace(actionOverride) ? ProtectionAction() : ModOptions.NormalizeNetworkProtectionAction(actionOverride); + bool shouldNotify = ShouldShowProtectionNotice(clientId, title, detail); + if (shouldNotify) + { + AcovPlugin.Logger?.LogWarning((object)$"Network protection blocked message from client {clientId}: {title}. {detail}"); + } + + if (shouldNotify && action != "Null") + { + AcovSecurityNotifications.Show(action, ClientName(clientId), title, detail, clientId); + } + + ApplyProtectionAction(clientId, title, detail, action); + } + +private static bool ShouldShowProtectionNotice(int clientId, string title, string detail) + { + float now = Time.realtimeSinceStartup; + PruneProtectionNotices(now); + string key = ProtectionNoticeKey(clientId, title); + if (ProtectionNoticeSeenAt.TryGetValue(key, out float lastSeen) && now - lastSeen < ProtectionNoticeRepeatSeconds) + { + return false; + } + + ProtectionNoticeSeenAt[key] = now; + return true; + } + +private static string ProtectionNoticeKey(int clientId, string title) + { + string source = clientId >= 0 + ? "c:" + clientId.ToString() + : "k:" + TrimConnectionKeyForLog(activeInboundConnectionKey); + return source + ":" + (title ?? string.Empty); + } + +private static void PruneProtectionNotices(float now) + { + if (ProtectionNoticeSeenAt.Count == 0) + { + return; + } + + ScratchConnectionKeys.Clear(); + foreach (KeyValuePair pair in ProtectionNoticeSeenAt) + { + if (string.IsNullOrWhiteSpace(pair.Key) || now - pair.Value > ProtectionNoticeRepeatSeconds * 4f) + { + ScratchConnectionKeys.Add(pair.Key); + } + } + + for (int i = 0; i < ScratchConnectionKeys.Count; i++) + { + string key = ScratchConnectionKeys[i]; + if (!string.IsNullOrWhiteSpace(key)) + { + ProtectionNoticeSeenAt.Remove(key); + } + } + + ScratchConnectionKeys.Clear(); + } + +private static void BlockRpc(PlayerControl player, int clientId, string title, string detail) + { + BlockRpc(player, clientId, title, detail, null); + } + +private static void BlockRpc(PlayerControl player, int clientId, string title, string detail, string actionOverride) + { + int playerClientId = GetPlayerClientId(player); + string name = clientId >= 0 && clientId != playerClientId ? ClientName(clientId) : PlayerName(player); + AcovPlugin.Logger?.LogWarning((object)$"Network protection blocked RPC from {name} (client {clientId}): {title}. {detail}"); + string action = string.IsNullOrWhiteSpace(actionOverride) ? ProtectionAction() : ModOptions.NormalizeNetworkProtectionAction(actionOverride); + if (action != "Null") + { + AcovSecurityNotifications.Show(action, name, title, detail, clientId); + } + + ApplyProtectionAction(clientId, title, detail, action); + } + +internal static void ReportQuickChatFlood(PlayerControl sender, int ownerId, string detail) + { + try + { + string action = ModOptions.NormalizeNetworkProtectionAction( + ModOptions.QuickChatAction != null ? ModOptions.QuickChatAction.Value : "Kick"); + + if (action != "Null") + { + string name = ownerId >= 0 ? ClientName(ownerId) : PlayerName(sender); + AcovSecurityNotifications.Show(action, name, "QuickChat flood", detail, ownerId); + } + + ApplyProtectionAction(ownerId, "QuickChat flood", detail, action); + } + catch + { + } + } + +private static void ApplyProtectionAction(int clientId, string attackType, string detail) + { + ApplyProtectionAction(clientId, attackType, detail, ProtectionAction()); + } + +private static void ApplyProtectionAction(int clientId, string attackType, string detail, string action) + { + switch (ModOptions.NormalizeNetworkProtectionAction(action)) + { + case "Ban": + DisconnectIfHost(clientId, true, attackType, detail); + return; + case "Kick": + DisconnectIfHost(clientId, false, attackType, detail); + return; + default: + return; + } + } + +private static void DisconnectIfHost(int clientId, bool ban, string attackType, string detail) + { + if (clientId < 0 || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + return; + } + + InnerNetClient client = (InnerNetClient)AmongUsClient.Instance; + if (clientId == client.ClientId || clientId == client.HostId) + { + AcovPlugin.Logger?.LogWarning((object)$"Network protection refused to {(ban ? "ban" : "kick")} local/host client id {clientId}."); + return; + } + + float now = Time.realtimeSinceStartup; + if (LastClientActionAt.TryGetValue(clientId, out float lastAction) && now - lastAction < 5f) + { + return; + } + + LastClientActionAt[clientId] = now; + RecentJoinSenderCandidates.Remove(clientId); + try + { + if (ban) + { + ClientData target = GetClientById(clientId) ?? GetRecentClient(client, clientId); + if (!AcovAccessLists.AddBanClient(target, $"{attackType}: {detail}") && TryGetSnapshotIdentity(clientId, out AcovClientIdentity identity)) + { + AcovAccessLists.AddBanIdentity(identity, $"{attackType}: {detail}"); + } + } + + client.KickPlayer(clientId, ban); + AcovPlugin.Logger?.LogWarning((object)$"Network protection sent {(ban ? "ban" : "kick")} for client {clientId}."); + QueueLocalCleanup(clientId, 0f); + } + catch (Exception error) + { + AcovPlugin.Logger?.LogWarning((object)$"Network protection {(ban ? "ban" : "kick")} failed for client {clientId}: {error.Message}"); + QueueLocalCleanup(clientId, 0.15f); + } + } + +internal static void UpdateLocalCleanup() + { + if (PendingLocalCleanupAt.Count == 0) + { + return; + } + + float now = Time.realtimeSinceStartup; + List completed = null; + List ids = new List(PendingLocalCleanupAt.Keys); + for (int i = 0; i < ids.Count; i++) + { + int clientId = ids[i]; + if (!PendingLocalCleanupAt.TryGetValue(clientId, out float runAt) || now < runAt) + { + continue; + } + + if (ForceLocalClientLeave(clientId)) + { + if (completed == null) + { + completed = new List(); + } + + completed.Add(clientId); + } + else + { + PendingLocalCleanupAt[clientId] = now + 0.35f; + } + } + + if (completed == null) + { + return; + } + + for (int i = 0; i < completed.Count; i++) + { + PendingLocalCleanupAt.Remove(completed[i]); + } + } + +private static void QueueLocalCleanup(int clientId, float delay) + { + if (clientId < 0) + { + return; + } + + PendingLocalCleanupAt[clientId] = Time.realtimeSinceStartup + Mathf.Max(0f, delay); + } + +private static bool ForceLocalClientLeave(int clientId) + { + if (AmongUsClient.Instance == null) + { + return true; + } + + InnerNetClient client = (InnerNetClient)AmongUsClient.Instance; + if (!client.AmHost || clientId == client.ClientId || clientId == client.HostId) + { + return true; + } + + ClientData target = GetClientById(clientId) ?? GetRecentClient(client, clientId); + if (target == null) + { + CleanupDisconnectedGameData(); + return true; + } + + PlayerControl character = null; + try + { + character = target.Character; + } + catch + { + } + + MarkDisconnected(target, character); + RunNormalPlayerLeft(target); + RemoveClientFromActiveList(client, target, clientId); + DestroyVisibleCharacter(character); + CleanupDisconnectedGameData(); + + bool stillListed = GetClientById(clientId) != null; + if (!stillListed) + { + AcovPlugin.Logger?.LogWarning((object)$"Network protection cleaned removed client {clientId} from lobby."); + } + + return !stillListed; + } + +private static void MarkDisconnected(ClientData client, PlayerControl character) + { + try + { + client.InScene = false; + } + catch + { + } + + try + { + if (character != null && character.Data != null) + { + character.Data.Disconnected = true; + } + } + catch + { + } + } + +private static void RunNormalPlayerLeft(ClientData client) + { + if (client == null || AmongUsClient.Instance == null) + { + return; + } + + try + { + AmongUsClient.Instance.OnPlayerLeft(client, (DisconnectReasons)0); + } + catch (Exception error) + { + AcovPlugin.Logger?.LogWarning((object)$"Network protection OnPlayerLeft cleanup failed for client {client.Id}: {error.Message}"); + } + } + +private static void RemoveClientFromActiveList(InnerNetClient client, ClientData target, int clientId) + { + if (client == null || target == null) + { + return; + } + + try + { + lock (client.allClients) + { + for (int i = client.allClients.Count - 1; i >= 0; i--) + { + ClientData current = client.allClients[i]; + if (current != null && (current.Id == clientId || current == target)) + { + client.allClients.RemoveAt(i); + } + } + } + } + catch (Exception error) + { + AcovPlugin.Logger?.LogWarning((object)$"Network protection active client cleanup failed for {clientId}: {error.Message}"); + } + } + +private static void DestroyVisibleCharacter(PlayerControl character) + { + if (character == null) + { + return; + } + + try + { + UnityEngine.Object.Destroy(((Component)character).gameObject); + } + catch + { + } + } + +private static void CleanupDisconnectedGameData() + { + try + { + GameData.Instance?.RemoveDisconnectedPlayers(); + } + catch + { + } + } + +private static void RememberAllClients() + { + if (AmongUsClient.Instance == null) + { + return; + } + + int frame = Time.frameCount; + if (frame == lastRememberedClientsFrame) + { + return; + } + lastRememberedClientsFrame = frame; + + PruneClientSnapshots(); + PruneRecentJoinSenderCandidates(); + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + try + { + var clients = inner.allClients.GetEnumerator(); + while (clients.MoveNext()) + { + RememberClient(clients.Current); + } + } + catch + { + } + + try + { + Il2CppSystem.Collections.Generic.List clients = new Il2CppSystem.Collections.Generic.List(); + inner.GetAllClients(clients); + var cursor = clients.GetEnumerator(); + while (cursor.MoveNext()) + { + RememberClient(cursor.Current); + } + } + catch + { + } + } + +private static void RememberClient(ClientData client) + { + if (client == null || client.Id < 0) + { + return; + } + + InnerNetClient inner = AmongUsClient.Instance == null ? null : (InnerNetClient)AmongUsClient.Instance; + if (inner != null && (client.Id == inner.ClientId || client.Id == inner.HostId)) + { + return; + } + + float now = Time.realtimeSinceStartup; + ClientSnapshot snapshot; + if (!ClientSnapshotsById.TryGetValue(client.Id, out snapshot)) + { + snapshot.PlayerId = byte.MaxValue; + snapshot.OwnerId = -1; + } + snapshot.ClientId = client.Id; + snapshot.PlayerName = ReadClientName(client, snapshot.PlayerName); + snapshot.FriendCode = SafeString(client.FriendCode); + snapshot.ProductUserId = SafeString(client.ProductUserId); + snapshot.LastSeenAt = now; + if (snapshot.JoinedAt <= 0f) + { + snapshot.JoinedAt = ClientJoinTimeAt.TryGetValue(client.Id, out float joinedAt) ? joinedAt : now; + } + + PlayerControl character = null; + try + { + character = client.Character; + } + catch + { + } + + if (character != null) + { + snapshot.PlayerId = character.PlayerId; + try + { + int ownerId = ((InnerNetObject)character).OwnerId; + if (ownerId >= 0) + { + snapshot.OwnerId = ownerId; + } + } + catch + { + } + } + else if (snapshot.PlayerId == 0) + { + snapshot.PlayerId = byte.MaxValue; + } + + ClientSnapshotsById[client.Id] = snapshot; + if (snapshot.PlayerId < 100) + { + ClientIdByPlayerId[snapshot.PlayerId] = client.Id; + } + + if (snapshot.OwnerId >= 0) + { + ClientIdByOwnerId[snapshot.OwnerId] = client.Id; + } + } + +private static void PruneClientSnapshots() + { + if (ClientSnapshotsById.Count == 0) + { + return; + } + + float now = Time.realtimeSinceStartup; + ScratchClientSnapshotIds.Clear(); + foreach (KeyValuePair pair in ClientSnapshotsById) + { + if (now - pair.Value.LastSeenAt > ClientSnapshotTtlSeconds) + { + ScratchClientSnapshotIds.Add(pair.Key); + } + } + + for (int i = 0; i < ScratchClientSnapshotIds.Count; i++) + { + ForgetClientSnapshot(ScratchClientSnapshotIds[i]); + } + } +} +} diff --git a/anticheat/Acov/Patches/NetworkProtectionGuard.Messages.cs b/anticheat/Acov/Patches/NetworkProtectionGuard.Messages.cs new file mode 100644 index 0000000..a3610ae --- /dev/null +++ b/anticheat/Acov/Patches/NetworkProtectionGuard.Messages.cs @@ -0,0 +1,1184 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static partial class NetworkProtectionGuard +{ + +private static bool NameRpcStringTooLong(MessageReader part, byte callId) + { + if (!NameStringRpcIds.Contains(callId)) + { + return false; + } + + try + { + return part != null && part.Length > MaxRpcStringBytes; + } + catch + { + return false; + } + } + +private static bool VentRpcOutOfRange(MessageReader part, byte callId) + { + if (callId != 19 && callId != 20) + { + return false; + } + + if (ModOptions.VentGuard != null && !ModOptions.VentGuard.Value) + { + return false; + } + + try + { + MessageReader copy = MessageReader.Get(part); + copy.ReadPackedUInt32(); + copy.ReadByte(); + int ventId = copy.ReadPackedInt32(); + return !IsRealVentId(ventId); + } + catch + { + return false; + } + } + +internal static bool IsRealVentId(int ventId) + { + try + { + ShipStatus ship = ShipStatus.Instance; + if (ship == null || ship.AllVents == null) + { + return true; + } + + var vents = ship.AllVents; + int count = vents.Count; + if (count <= 0) + { + return true; + } + + for (int i = 0; i < count; i++) + { + var vent = vents[i]; + if (vent != null && vent.Id == ventId) + { + return true; + } + } + + return false; + } + catch + { + return true; + } + } + +private static bool SnapToAggregateExceeded() + { + float now = Time.realtimeSinceStartup; + while (SnapToAggregate.Count > 0 && now - SnapToAggregate.Peek() > 1f) + { + SnapToAggregate.Dequeue(); + } + SnapToAggregate.Enqueue(now); + return SnapToAggregate.Count > MaxSnapToAggregatePerSec; + } + +private static void CleanupNonHostRpc(float now) + { + if (NonHostRpcByKey.Count == 0) return; + List dead = null; + foreach (KeyValuePair> kv in NonHostRpcByKey) + { + Queue q = kv.Value; + while (q.Count > 0 && now - q.Peek() > 6f) + { + q.Dequeue(); + } + if (q.Count == 0) + { + (dead ??= new List()).Add(kv.Key); + } + } + if (dead != null) + { + for (int i = 0; i < dead.Count; i++) NonHostRpcByKey.Remove(dead[i]); + } + } + +private static void ResetInboundEnvelope(SendOption sendOption) + { + activeInboundTargetClientId = -1; + activeInboundGameDataParts = 0; + activeInboundHasInvalidPartTag = false; + activeInboundHasSpawnExploit = false; + activeInboundSpawnExploitDetail = null; + activeInboundHasDataFlood = false; + activeInboundDataFloodDetail = null; + activeInboundPhantomFlood = false; + activeInboundHasPhantomNetId = false; + } + +private static bool PhantomDataRateExceeded() + { + long now = Environment.TickCount64; + if (now - phantomDataWindowMs >= 1000) + { + phantomDataWindowMs = now; + phantomDataInWindow = 0; + } + phantomDataInWindow++; + return phantomDataInWindow > MaxPhantomDataPerSec; + } + +private static bool IsPhantomNetId(uint nid) + { + long now = Environment.TickCount64; + if (now - lastPendingUnknownCleanupMs > 4000) + { + lastPendingUnknownCleanupMs = now; + CleanupPendingUnknownNetIds(now); + } + + if (!pendingUnknownNetId.TryGetValue(nid, out long firstSeen)) + { + pendingUnknownNetId[nid] = now; + return false; + } + + return now - firstSeen > UnknownNetIdGraceMs; + } + +private static void CleanupPendingUnknownNetIds(long now) + { + if (pendingUnknownNetId.Count == 0) return; + InnerNetClient inner = AmongUsClient.Instance != null ? (InnerNetClient)AmongUsClient.Instance : null; + List dead = null; + foreach (KeyValuePair kv in pendingUnknownNetId) + { + bool live = inner != null && inner.allObjects.AllObjectsFast.ContainsKey(kv.Key); + if (live || now - kv.Value > 10000) + { + (dead ??= new List()).Add(kv.Key); + } + } + if (dead != null) + { + for (int i = 0; i < dead.Count; i++) pendingUnknownNetId.Remove(dead[i]); + } + } + +private static bool GameDataFramingValid(MessageReader reader, byte tag) + { + try + { + MessageReader copy = MessageReader.Get(reader); + copy.ReadInt32(); + if (tag == 6) + { + copy.ReadPackedInt32(); + } + + int parts = 0; + while (copy.Position < copy.Length && parts < MaxTrackedGameDataParts) + { + MessageReader part = copy.ReadMessage(); + if (part == null) + { + break; + } + + parts++; + byte pt = part.Tag; + if (pt == 0 || pt == 3 || pt > 8) + { + return false; + } + + { + MessageReader idc = MessageReader.Get(part); + idc.ReadPackedUInt32(); + if (pt == 2) + { + idc.ReadByte(); + } + else if (pt == 6) + { + idc.ReadString(); + } + } + } + + return true; + } + catch + { + return false; + } + } + +private static void CaptureGameDataEnvelope(MessageReader reader, byte tag, SendOption sendOption) + { + activeInboundTargetClientId = -1; + activeInboundGameDataParts = 0; + activeInboundHasInvalidPartTag = false; + activeInboundHasSpawnExploit = false; + activeInboundSpawnExploitDetail = null; + activeInboundHasDataFlood = false; + activeInboundDataFloodDetail = null; + ScratchSpawnNetIds.Clear(); + if (reader == null) + { + return; + } + + int originalPosition = reader.Position; + try + { + int senderClientId = GetActiveInboundSenderClientId(); + MessageReader copy = MessageReader.Get(reader); + copy.ReadInt32(); + if (tag == 6) + { + activeInboundTargetClientId = copy.ReadPackedInt32(); + } + + int safety = 0; + List rpcContexts = null; + while (copy.Position < copy.Length && safety++ < MaxTrackedGameDataParts) + { + MessageReader part = copy.ReadMessage(); + if (part == null) + { + break; + } + + activeInboundGameDataParts++; + + byte partTag = part.Tag; + if (partTag == 0 || partTag == 3 || partTag > 8) + { + activeInboundHasInvalidPartTag = true; + break; + } + + if (partTag == 1 && AmongUsClient.Instance != null) + { + try + { + MessageReader dataCopy = MessageReader.Get(part); + uint dataNetId = dataCopy.ReadPackedUInt32(); + InnerNetClient innerCheck = (InnerNetClient)AmongUsClient.Instance; + if (!innerCheck.allObjects.AllObjectsFast.ContainsKey(dataNetId) && + !innerCheck.DestroyedObjects.Contains(dataNetId)) + { + if (PhantomDataRateExceeded()) + { + activeInboundPhantomFlood = true; + } + if (IsPhantomNetId(dataNetId)) + { + activeInboundHasPhantomNetId = true; + } + } + + int dataBytes = dataCopy.Length - dataCopy.Position; + if (!IsLobbyJoinSyncGrace() && ShouldDropDataObject(dataNetId, dataBytes)) + { + activeInboundHasDataFlood = true; + activeInboundDataFloodDetail = $"netId {dataNetId}, {dataBytes}B — DATA flood."; + break; + } + } + catch { } + } + + if (partTag == 4 && SpawnShouldDrop(part)) + { + activeInboundHasSpawnExploit = true; + activeInboundSpawnExploitDetail = "Spawn flood (fake-owner / over per-scene cap)."; + break; + } + + if (partTag == 4 && IdenticalNetIdProtectionEnabled()) + { + try + { + MessageReader spawnCopy = MessageReader.Get(part); + spawnCopy.ReadPackedUInt32(); + int spawnOwnerId = spawnCopy.ReadPackedInt32(); + spawnCopy.ReadByte(); + int compCount = spawnCopy.ReadPackedInt32(); + if (compCount <= 0) break; + int spawnScan = compCount > 32 ? 32 : compCount; + + InnerNetClient innerForOwnerCheck = AmongUsClient.Instance != null + ? (InnerNetClient)AmongUsClient.Instance + : null; + + for (int ci = 0; ci < spawnScan; ci++) + { + if (spawnCopy.Position >= spawnCopy.Length) break; + uint newNetId = spawnCopy.ReadPackedUInt32(); + if (newNetId != 0) + { + if (ScratchSpawnNetIds.Contains(newNetId)) + { + activeInboundHasSpawnExploit = true; + activeInboundSpawnExploitDetail = $"Duplicate Spawn netId {newNetId} within packet."; + break; + } + + if (innerForOwnerCheck != null && + innerForOwnerCheck.allObjects.AllObjectsFast.ContainsKey(newNetId) && + !innerForOwnerCheck.DestroyedObjects.Contains(newNetId)) + { + activeInboundHasSpawnExploit = true; + try + { + InnerNetObject existingObj = innerForOwnerCheck.allObjects.AllObjectsFast[newNetId]; + activeInboundSpawnExploitDetail = (existingObj != null && existingObj.OwnerId != spawnOwnerId) + ? $"Spawn netId {newNetId} hijack: existing owner {existingObj.OwnerId} != new owner {spawnOwnerId}." + : $"Spawn netId {newNetId} duplicates a live object."; + } + catch { activeInboundSpawnExploitDetail = $"Spawn netId {newNetId} duplicates a live object."; } + break; + } + + ScratchSpawnNetIds.Add(newNetId); + } + MessageReader compInit = spawnCopy.ReadMessage(); + if (compInit == null) break; + } + + if (activeInboundHasSpawnExploit) break; + } + catch { } + } + + if (part.Tag == 2 && TryReadRpcEnvelope(part, out uint netId, out byte callId)) + { + if (rpcContexts == null) + { + rpcContexts = new List(); + } + + rpcContexts.Add(new RpcEnvelopeContext + { + NetId = netId, + CallId = callId, + TargetClientId = activeInboundTargetClientId, + SenderClientId = senderClientId, + DataIndex = activeInboundGameDataParts, + SendOption = sendOption, + CreatedAtMs = Environment.TickCount64, + }); + } + } + + if (rpcContexts != null) + { + for (int i = 0; i < rpcContexts.Count; i++) + { + RpcEnvelopeContext context = rpcContexts[i]; + context.DataCount = activeInboundGameDataParts; + QueueRpcEnvelopeContext(context); + } + } + } + catch + { + activeInboundGameDataParts = 0; + activeInboundTargetClientId = -1; + } + finally + { + try + { + reader.Position = originalPosition; + } + catch + { + } + } + } + +private static void CaptureJoinEnvelope(MessageReader reader) + { + if (reader == null) + { + return; + } + + int originalPosition = reader.Position; + try + { + MessageReader copy = MessageReader.Get(reader); + int gameId = copy.ReadInt32(); + if (AmongUsClient.Instance != null && ((InnerNetClient)AmongUsClient.Instance).GameId != gameId) + { + return; + } + + int joinedClientId = copy.ReadInt32(); + if (joinedClientId < 0 || AmongUsClient.Instance == null) + { + return; + } + + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + if (joinedClientId == inner.ClientId || joinedClientId == inner.HostId) + { + return; + } + + RememberConnectionClient(activeInboundConnectionKey, joinedClientId); + ClientJoinTimeAt[joinedClientId] = Time.realtimeSinceStartup; + RememberRecentJoinCandidate(joinedClientId); + if (GetActiveInboundSenderClientId() < 0) + { + SetActiveInboundSender(joinedClientId); + } + } + catch + { + } + finally + { + try + { + reader.Position = originalPosition; + } + catch + { + } + } + } + +private static bool TryReadRpcEnvelope(MessageReader part, out uint netId, out byte callId) + { + netId = 0; + callId = 0; + if (part == null) + { + return false; + } + + try + { + MessageReader copy = MessageReader.Get(part); + netId = copy.ReadPackedUInt32(); + callId = copy.ReadByte(); + return true; + } + catch + { + return false; + } + } + +private static int TryResolveOwnerFromGameDataContent(InnerNetClient client, MessageReader reader) + { + if (client == null || reader == null) return -1; + byte tag = reader.Tag; + if (tag != 1 && tag != 5 && tag != 6) return -1; + try + { + MessageReader copy = MessageReader.Get(reader); + if (copy.Length - copy.Position < 4) return -1; + int gameId = copy.ReadInt32(); + if (gameId != client.GameId) return -1; + + if (tag == 1) + { + if (copy.Length - copy.Position < 4) return -1; + int joinClientId = copy.ReadInt32(); + return IsKnownRemoteClient(client, joinClientId) ? joinClientId : -1; + } + + if (tag == 6) + { + if (copy.Position >= copy.Length) return -1; + copy.ReadPackedInt32(); + } + + if (copy.Position >= copy.Length) return -1; + MessageReader subMsg = copy.ReadMessage(); + if (subMsg == null) return -1; + byte subTag = subMsg.Tag; + if (subTag != 1 && subTag != 2) return -1; + if (subMsg.Position >= subMsg.Length) return -1; + uint netId = subMsg.ReadPackedUInt32(); + if (netId == 0) return -1; + return FindClientIdByNetId(netId); + } + catch + { + return -1; + } + } + +private static int FindClientIdByNetId(uint netId) + { + if (netId == 0 || PlayerControl.AllPlayerControls == null) return -1; + try + { + var players = PlayerControl.AllPlayerControls.GetEnumerator(); + while (players.MoveNext()) + { + PlayerControl player = players.Current; + if (player == null) continue; + try + { + bool matched = ((InnerNetObject)player).NetId == netId; + if (!matched && player.MyPhysics != null) + matched = ((InnerNetObject)player.MyPhysics).NetId == netId; + if (!matched && player.NetTransform != null) + matched = ((InnerNetObject)player.NetTransform).NetId == netId; + if (!matched) continue; + + int ownerId = ((InnerNetObject)player).OwnerId; + if (ClientIdByOwnerId.TryGetValue(ownerId, out int clientId) && IsKnownRemoteClient(clientId)) + return clientId; + if (IsKnownRemoteClient(ownerId)) + return ownerId; + } + catch { } + } + return -1; + } + catch + { + return -1; + } + } + +private static void QueueRpcEnvelopeContext(RpcEnvelopeContext context) + { + CleanupRpcEnvelopeContexts(); + PendingRpcContexts.Add(context); + if (PendingRpcContexts.Count <= 96) + { + return; + } + + int removeCount = PendingRpcContexts.Count - 96; + PendingRpcContexts.RemoveRange(0, removeCount); + } + +private static bool TryTakeRpcEnvelopeContext(PlayerControl player, byte callId, out RpcEnvelopeContext context) + { + context = default; + if (player == null) + { + return false; + } + + return TryTakeRpcEnvelopeContext((InnerNetObject)player, callId, out context); + } + +private static bool TryTakeRpcEnvelopeContext(InnerNetObject netObject, byte callId, out RpcEnvelopeContext context) + { + context = default; + CleanupRpcEnvelopeContexts(); + if (netObject == null || PendingRpcContexts.Count == 0) + { + return false; + } + + uint netId = netObject.NetId; + for (int i = 0; i < PendingRpcContexts.Count; i++) + { + RpcEnvelopeContext candidate = PendingRpcContexts[i]; + if (candidate.NetId != netId || candidate.CallId != callId) + { + continue; + } + + context = candidate; + PendingRpcContexts.RemoveAt(i); + return true; + } + + return false; + } + +private static int ResolveRpcSenderClientId(int currentClientId, bool hasRpcContext, RpcEnvelopeContext rpcContext) + { + if (hasRpcContext && IsKnownRemoteClient(rpcContext.SenderClientId)) + { + return rpcContext.SenderClientId; + } + + return IsKnownRemoteClient(currentClientId) ? currentClientId : -1; + } + +private static void CleanupRpcEnvelopeContexts() + { + if (PendingRpcContexts.Count == 0) + { + return; + } + + long now = Environment.TickCount64; + for (int i = PendingRpcContexts.Count - 1; i >= 0; i--) + { + if (now - PendingRpcContexts[i].CreatedAtMs > 3000) + { + PendingRpcContexts.RemoveAt(i); + } + } + } + +internal static bool CheckRpc(PlayerControl player, int callId, MessageReader reader) + { + if (!Enabled()) + { + return HarmonyControl.Continue; + } + + if (player == null) + { + return HarmonyControl.Continue; + } + + if (PlayerControl.LocalPlayer != null && player == PlayerControl.LocalPlayer) + { + return HarmonyControl.Continue; + } + + int originalPosition = reader == null ? 0 : reader.Position; + int clientId = GetResponsibleClientId(player); + try + { + if (reader == null) + { + return HarmonyControl.Continue; + } + + if (!ReaderLooksSane(reader)) + { + return HarmonyControl.Continue; + } + + if (callId < 0 || callId > byte.MaxValue) + { + BlockRpc(player, clientId, "RPC вне диапазона", $"CallId: {callId}."); + return HarmonyControl.SkipOriginal; + } + + byte rpcByte = (byte)callId; + bool hasRpcContext = TryTakeRpcEnvelopeContext(player, rpcByte, out RpcEnvelopeContext rpcContext); + clientId = ResolveRpcSenderClientId(clientId, hasRpcContext, rpcContext); + if (clientId < 0 && rpcByte == 13) + { + clientId = GetVerifiedPlayerClientId(player); + } + + if (ShouldTrustLocalOutfitRpc(player, callId, clientId)) + { + return HarmonyControl.Continue; + } + + if (TryHandleKnownModRpc(player, rpcByte, clientId, out bool skipKnownModRpc)) + { + return skipKnownModRpc ? HarmonyControl.SkipOriginal : HarmonyControl.Continue; + } + + if (LegacyExploitRpcIds.Contains(callId)) + { + BlockRpc(player, clientId, "Подозрительный RPC", $"CallId: {callId}."); + return HarmonyControl.SkipOriginal; + } + + if (SuspiciousRpcIds.Contains(callId)) + { + BlockRpc(player, clientId, "Suspicious RPC", $"CallId: {callId}."); + return HarmonyControl.SkipOriginal; + } + + if (rpcByte == 11) + { + NoteMeetingRpc(); + } + + bool lobbyJoinSyncGrace = IsLobbyJoinSyncGrace(player, clientId); + bool isKnownVanillaRpc = Enum.IsDefined(typeof(RpcCalls), rpcByte); + if (ShouldBlockRpcEnvelope(player, rpcByte, clientId, hasRpcContext, rpcContext)) + { + return HarmonyControl.SkipOriginal; + } + + float now = Time.realtimeSinceStartup; + if (!lobbyJoinSyncGrace && clientId >= 0) + { + (int sameLimit, float sameWindow) = RpcSpamLimit(callId); + if (HitSameRpcLimit(clientId, callId, now, sameWindow, sameLimit, out int sameCount)) + { + string rpcAction = callId == 21 && ModOptions.SnapToAction != null ? ModOptions.SnapToAction.Value : null; + BlockRpc(player, clientId, "RPC flood", $"{RpcName(rpcByte)}: {sameCount}/{sameLimit} за {sameWindow:0.00}с.", rpcAction); + return HarmonyControl.SkipOriginal; + } + } + + if (ShouldBlockCrashRpc(player, rpcByte, clientId)) + { + return HarmonyControl.SkipOriginal; + } + + if (ShouldBlockPlayerSemanticRpc(player, rpcByte, clientId, reader)) + { + return HarmonyControl.SkipOriginal; + } + + return HarmonyControl.Continue; + } + catch (Exception error) + { + AcovPlugin.Logger?.LogWarning((object)$"Network protection ignored RPC check error for {PlayerName(player)} (client {clientId}, call {callId}): {error.Message}"); + return HarmonyControl.Continue; + } + finally + { + if (reader != null) + { + try + { + reader.Position = originalPosition; + } + catch + { + } + } + } + } + +internal static void TrustLocalCosmeticApply(PlayerControl target) + { + if (target == null) + { + return; + } + + trustedLocalCosmeticPlayerId = target.PlayerId; + trustedLocalCosmeticClientId = GetPlayerClientId(target); + trustedLocalCosmeticUntil = Time.realtimeSinceStartup + 4.00f; + trustedLocalCosmeticBudget = 64; + trustedLocalCosmeticPacket = true; + } + +private static bool ShouldTrustLocalOutfitRpc(PlayerControl player, int callId, int clientId) + { + if (player == null) + { + return false; + } + + if (!IsTrustedLocalCosmeticApplyActive() || player.PlayerId != trustedLocalCosmeticPlayerId) + { + ClearTrustedLocalCosmeticApply(); + return false; + } + + if (trustedLocalCosmeticClientId >= 0) + { + int playerClientId = GetPlayerClientId(player); + if (playerClientId >= 0 && playerClientId != trustedLocalCosmeticClientId) + { + ClearTrustedLocalCosmeticApply(); + return false; + } + } + + trustedLocalCosmeticBudget--; + if (trustedLocalCosmeticBudget <= 0) + { + ClearTrustedLocalCosmeticApply(); + } + + return true; + } + +private static bool IsTrustedLocalCosmeticApplyActive() + { + if (!trustedLocalCosmeticPacket) + { + return false; + } + + if (trustedLocalCosmeticBudget <= 0 || Time.realtimeSinceStartup > trustedLocalCosmeticUntil) + { + ClearTrustedLocalCosmeticApply(); + return false; + } + + return true; + } + +private static void ClearTrustedLocalCosmeticApply() + { + trustedLocalCosmeticPlayerId = byte.MaxValue; + trustedLocalCosmeticClientId = -1; + trustedLocalCosmeticUntil = -1f; + trustedLocalCosmeticBudget = 0; + trustedLocalCosmeticPacket = false; + } + +internal static bool CheckShipStatusRpc(ShipStatus ship, int callId, MessageReader reader) + { + if (!Enabled() || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || ship == null) + { + return HarmonyControl.Continue; + } + + if (callId != 35 || reader == null) + { + return HarmonyControl.Continue; + } + + int clientId = GetActiveInboundSenderClientId(); + try + { + if (callId >= 0 && callId <= byte.MaxValue && TryTakeRpcEnvelopeContext((InnerNetObject)ship, (byte)callId, out RpcEnvelopeContext rpcContext)) + { + clientId = ResolveRpcSenderClientId(clientId, true, rpcContext); + } + + MessageReader copy = MessageReader.Get(reader); + int systemId = copy.ReadByte(); + PlayerControl actor = MessageExtensions.ReadNetObject(copy); + byte amount = copy.ReadByte(); + if (actor != null && clientId < 0) + { + clientId = GetResponsibleClientId(actor); + } + + int mapId = CurrentMapId(); + if (!SabotagePayloadFitsMap(systemId, amount, mapId)) + { + BlockSystemRpc(actor, clientId, "Invalid sabotage payload", $"Map {mapId}, system {systemId}, amount {amount}."); + return HarmonyControl.SkipOriginal; + } + + if (IsCrewSabotagePayload(systemId, amount) && actor != null && !IsImpostor(actor) && !IsDead(actor)) + { + BlockSystemRpc(actor, clientId, "Crew sabotage RPC", $"System {systemId}, amount {amount}."); + return HarmonyControl.SkipOriginal; + } + } + catch (Exception error) + { + BlockMessage(clientId, "Malformed ShipStatus RPC", error.Message); + return HarmonyControl.SkipOriginal; + } + + return HarmonyControl.Continue; + } + +internal static bool CheckVoteKickRpc(VoteBanSystem system, int callId, MessageReader reader) + { + if (!Enabled() || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || callId != 26 || reader == null) + { + return HarmonyControl.Continue; + } + + int voterClientId = GetActiveInboundSenderClientId(); + try + { + if (TryTakeRpcEnvelopeContext((InnerNetObject)system, (byte)callId, out RpcEnvelopeContext rpcContext)) + { + voterClientId = ResolveRpcSenderClientId(voterClientId, true, rpcContext); + } + + MessageReader copy = MessageReader.Get(reader); + copy.ReadInt32(); + int target = copy.ReadInt32(); + + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + if (target == inner.ClientId || target == inner.HostId) + { + BlockMessage(voterClientId, "Vote-kick host", $"Target client {target}."); + return HarmonyControl.SkipOriginal; + } + + float now = Time.realtimeSinceStartup; + if (ClientJoinTimeAt.TryGetValue(voterClientId, out float joinedAt) && now - joinedAt < EarlyJoinVoteGuardSeconds) + { + BlockMessage(voterClientId, "Early vote-kick", $"{now - joinedAt:0.0}s after join."); + return HarmonyControl.SkipOriginal; + } + + ClientData targetClient = GetClientById(target); + PlayerControl targetPlayer = targetClient?.Character; + if (targetPlayer != null && InActiveMatch() && !MeetingOrExileActive() && !IsDead(targetPlayer)) + { + BlockMessage(voterClientId, "In-match vote-kick", $"{ClientName(target)} is alive."); + return HarmonyControl.SkipOriginal; + } + } + catch (Exception error) + { + BlockMessage(voterClientId, "Malformed vote-kick", error.Message); + return HarmonyControl.SkipOriginal; + } + + return HarmonyControl.Continue; + } + +internal static void TrackClientJoined(ClientData client) + { + if (!Enabled() || client == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + return; + } + + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + if (client.Id == inner.ClientId || client.Id == inner.HostId) + { + return; + } + + RememberClient(client); + float now = Time.realtimeSinceStartup; + lastLobbyJoinAt = now; + ClientJoinTimeAt[client.Id] = now; + RememberRecentJoinCandidate(client.Id); + PendingJoinIntegrityAt[client.Id] = now + JoinIntegrityDelaySeconds; + AcovPlugin.Logger?.LogInfo((object)$"Acov.Net client {client.Id} passed entry checks"); + LogClientSuccessDetails(client); + AcovPlugin.Logger?.LogInfo((object)$"Acov.Net client {client.Id} entered lobby"); + } + +private static void LogClientSuccessDetails(ClientData client) + { + if (client == null) + { + return; + } + + int platformTag = 0; + string platform = "-"; + string rawPlatformName = string.Empty; + try + { + if (client.PlatformData != null) + { + platformTag = (int)client.PlatformData.Platform; + platform = client.PlatformData.Platform.ToString(); + rawPlatformName = SafeString(client.PlatformData.PlatformName); + } + } + catch + { + } + + string levelText = "-"; + try + { + if (AcovPlayerLevels.TryGetDisplayLevel(client.Id, out uint cachedLevel)) + { + levelText = cachedLevel.ToString(); + } + else if (client.Character != null && AcovPlayerLevels.TryGetDisplayLevel(client.Character, out uint characterLevel)) + { + levelText = characterLevel.ToString(); + } + } + catch + { + } + + AcovPlugin.Logger?.LogInfo((object)$"Acov.Net client info: id={client.Id}, player='{TrimJoinLog(ReadClientName(client, client.PlayerName), 64)}', platformTag={platformTag}, platform='{TrimJoinLog(platform, 48)}', rawPlatformName='{TrimJoinLog(rawPlatformName, 64)}', level={levelText}, friendCode='{TrimJoinLog(SafeString(client.FriendCode), 64)}', productUserId='{TrimJoinLog(SafeString(client.ProductUserId), 128)}'"); + } + +internal static void UpdateJoinIntegrityChecks() + { + PruneRecentJoinSenderCandidates(); + if (PendingJoinIntegrityAt.Count == 0) + { + return; + } + + float now = Time.realtimeSinceStartup; + List done = null; + List ids = new List(PendingJoinIntegrityAt.Keys); + for (int i = 0; i < ids.Count; i++) + { + int clientId = ids[i]; + if (!PendingJoinIntegrityAt.TryGetValue(clientId, out float dueAt) || now < dueAt) + { + continue; + } + + if (done == null) + { + done = new List(); + } + + done.Add(clientId); + ClientData client = GetClientById(clientId); + if (client == null) + { + continue; + } + + float joinedAt = ClientJoinTimeAt.TryGetValue(clientId, out float savedJoinAt) ? savedJoinAt : now; + if (!ClientIdentityReady(client)) + { + if (now - joinedAt < JoinIntegrityMaxWaitSeconds) + { + PendingJoinIntegrityAt[clientId] = now + JoinIntegrityRetrySeconds; + done.RemoveAt(done.Count - 1); + continue; + } + + AcovPlugin.Logger?.LogWarning((object)$"Network protection stopped waiting for client {clientId}: player data is still incomplete."); + continue; + } + + if (BrokenFriendCodeBanEnabled() && !FriendCodeReady(client) && now - joinedAt < JoinIntegrityMaxWaitSeconds) + { + PendingJoinIntegrityAt[clientId] = now + JoinIntegrityRetrySeconds; + done.RemoveAt(done.Count - 1); + continue; + } + + if (BrokenFriendCodeBanEnabled() && FriendCodeReady(client) && HasBrokenFriendCode(client)) + { + BlockMessage(clientId, "Broken friend code", "Client identity failed join hygiene check.", "Ban"); + } + } + + if (done == null) + { + return; + } + + for (int i = 0; i < done.Count; i++) + { + PendingJoinIntegrityAt.Remove(done[i]); + } + } + +private static bool ShouldBlockRpcEnvelope(PlayerControl player, byte rpcByte, int clientId, bool hasRpcContext, RpcEnvelopeContext rpcContext) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + return false; + } + + if (IsLobbyJoinSyncGrace(player, clientId)) + { + return false; + } + + if (IsMeetingTransitionGrace()) + { + return false; + } + + int targetClientId = hasRpcContext ? rpcContext.TargetClientId : activeInboundTargetClientId; + if (targetClientId >= 0 && !TargetedRpcAllowedIds.Contains(rpcByte)) + { + BlockRpc(player, clientId, "Target-only RPC", $"{RpcName(rpcByte)} -> client {targetClientId}."); + return true; + } + + int dataIndex = hasRpcContext ? rpcContext.DataIndex : 1; + if (!PackedDataAllowedRpcIds.Contains(rpcByte) && ((dataIndex > 1 && ImmediateRpcIds.Contains(rpcByte)) || dataIndex > 10)) + { + if (targetClientId > 0) + { + int dataCount = hasRpcContext ? rpcContext.DataCount : activeInboundGameDataParts; + BlockRpc(player, clientId, "Packed instant RPC", $"{RpcName(rpcByte)} at data #{dataIndex}/{dataCount}."); + return true; + } + + return false; + } + + SendOption sendOption = hasRpcContext ? rpcContext.SendOption : SendOption.Reliable; + if ((int)sendOption == 0 && rpcByte != 0) + { + if (targetClientId > 0 && rpcByte != 13) + { + BlockRpc(player, clientId, "Invalid RPC delivery", $"{RpcName(rpcByte)} was sent with {sendOption}."); + return true; + } + + return false; + } + + if (LobbyBehaviour.Instance != null && !LobbyAllowedRpcIds.Contains(rpcByte)) + { + BlockRpc(player, clientId, "Lobby RPC mismatch", $"{RpcName(rpcByte)} is not expected in lobby."); + return true; + } + + return false; + } +} +} diff --git a/anticheat/Acov/Patches/NetworkProtectionGuard.RpcChecks.cs b/anticheat/Acov/Patches/NetworkProtectionGuard.RpcChecks.cs new file mode 100644 index 0000000..f4b2b67 --- /dev/null +++ b/anticheat/Acov/Patches/NetworkProtectionGuard.RpcChecks.cs @@ -0,0 +1,1418 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static partial class NetworkProtectionGuard +{ + +private static void ForgetClientSnapshot(int clientId) + { + if (!ClientSnapshotsById.TryGetValue(clientId, out ClientSnapshot snapshot)) + { + return; + } + + ClientSnapshotsById.Remove(clientId); + RecentJoinSenderCandidates.Remove(clientId); + if (snapshot.PlayerId < 100 && ClientIdByPlayerId.TryGetValue(snapshot.PlayerId, out int mappedClientId) && mappedClientId == clientId) + { + ClientIdByPlayerId.Remove(snapshot.PlayerId); + } + + if (snapshot.OwnerId >= 0 && ClientIdByOwnerId.TryGetValue(snapshot.OwnerId, out mappedClientId) && mappedClientId == clientId) + { + ClientIdByOwnerId.Remove(snapshot.OwnerId); + } + } + +private static bool TryGetClientSnapshot(int clientId, out ClientSnapshot snapshot) + { + snapshot = default; + if (clientId < 0 || !ClientSnapshotsById.TryGetValue(clientId, out snapshot)) + { + return false; + } + + if (Time.realtimeSinceStartup - snapshot.LastSeenAt > ClientSnapshotTtlSeconds) + { + ForgetClientSnapshot(clientId); + snapshot = default; + return false; + } + + return true; + } + +private static bool TryResolveCachedClientId(PlayerControl player, out int clientId) + { + clientId = -1; + if (player == null) + { + return false; + } + + byte playerId = player.PlayerId; + if (playerId < 100 && ClientIdByPlayerId.TryGetValue(playerId, out clientId) && IsKnownRemoteClient(clientId)) + { + return true; + } + + try + { + int ownerId = ((InnerNetObject)player).OwnerId; + if (ownerId >= 0 && ClientIdByOwnerId.TryGetValue(ownerId, out clientId) && IsKnownRemoteClient(clientId)) + { + return true; + } + } + catch + { + } + + clientId = -1; + return false; + } + +private static bool TryGetSnapshotIdentity(int clientId, out AcovClientIdentity identity) + { + identity = default; + if (!TryGetClientSnapshot(clientId, out ClientSnapshot snapshot)) + { + return false; + } + + identity = new AcovClientIdentity(clientId, snapshot.PlayerName, snapshot.FriendCode, snapshot.ProductUserId); + return true; + } + +private static string SnapshotDisplayName(int clientId) + { + if (TryGetClientSnapshot(clientId, out ClientSnapshot snapshot) && !string.IsNullOrWhiteSpace(snapshot.PlayerName)) + { + return StripRichText(snapshot.PlayerName); + } + + return clientId >= 0 ? $"Client {clientId}" : "Unknown"; + } + +private static string ReadClientName(ClientData client, string fallback) + { + string name = fallback ?? string.Empty; + try + { + if (!string.IsNullOrWhiteSpace(client.PlayerName)) + { + name = client.PlayerName; + } + } + catch + { + } + + try + { + if (string.IsNullOrWhiteSpace(name) && client.Character != null && client.Character.Data != null) + { + name = client.Character.Data.PlayerName; + } + } + catch + { + } + + return SafeString(name); + } + +private static string SafeString(string value) + { + return value ?? string.Empty; + } + +private static string TrimJoinLog(string value, int maxLength) + { + string text = SafeString(value).Replace("\r", " ").Replace("\n", " ").Trim(); + if (maxLength <= 0 || text.Length <= maxLength) + { + return text; + } + + return text.Substring(0, maxLength) + "..."; + } + +private static string StripRichText(string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + string text = value; + int guard = 0; + while (guard++ < 24) + { + int start = text.IndexOf('<'); + if (start < 0) + { + break; + } + + int end = text.IndexOf('>', start); + if (end < start) + { + break; + } + + text = text.Remove(start, end - start + 1); + } + + return text; + } + +private static int GetResponsibleClientId(PlayerControl player) + { + int inboundSender = GetActiveInboundSenderClientId(); + if (IsKnownRemoteClient(inboundSender)) + { + return inboundSender; + } + + return -1; + } + +private static int GetPlayerClientId(PlayerControl player) + { + ClientData client = GetClient(player); + if (client != null) + { + return client.Id; + } + + try + { + int ownerId = ((InnerNetObject)player).OwnerId; + if (ownerId >= 0) + { + return ownerId; + } + } + catch + { + } + + return -1; + } + +private static int GetVerifiedPlayerClientId(PlayerControl player) + { + if (player == null || AmongUsClient.Instance == null) + { + return -1; + } + + try + { + ClientData client = GetClient(player); + if (client == null || client.Character == null || client.Character.PlayerId != player.PlayerId) + { + return -1; + } + + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + if (client.Id == inner.ClientId || client.Id == inner.HostId) + { + return -1; + } + + return client.Id; + } + catch + { + return -1; + } + } + +private static ClientData GetClient(PlayerControl player) + { + if (player == null || AmongUsClient.Instance == null) + { + return null; + } + + try + { + ClientData direct = ((InnerNetClient)AmongUsClient.Instance).GetClientFromCharacter(player); + if (direct != null) + { + RememberClient(direct); + return direct; + } + } + catch + { + } + + try + { + var clients = ((InnerNetClient)AmongUsClient.Instance).allClients.GetEnumerator(); + while (clients.MoveNext()) + { + ClientData client = clients.Current; + if (client != null && client.Character != null && client.Character.PlayerId == player.PlayerId) + { + RememberClient(client); + return client; + } + } + } + catch + { + } + + try + { + Il2CppSystem.Collections.Generic.List clients = new Il2CppSystem.Collections.Generic.List(); + ((InnerNetClient)AmongUsClient.Instance).GetAllClients(clients); + var cursor = clients.GetEnumerator(); + while (cursor.MoveNext()) + { + ClientData client = cursor.Current; + if (client != null && client.Character != null && client.Character.PlayerId == player.PlayerId) + { + RememberClient(client); + return client; + } + } + } + catch + { + } + + return null; + } + +private static ClientData GetClientById(int clientId) + { + if (clientId < 0 || AmongUsClient.Instance == null) + { + return null; + } + + try + { + var clients = ((InnerNetClient)AmongUsClient.Instance).allClients.GetEnumerator(); + while (clients.MoveNext()) + { + ClientData client = clients.Current; + if (client != null && client.Id == clientId) + { + RememberClient(client); + return client; + } + } + } + catch + { + } + + try + { + Il2CppSystem.Collections.Generic.List clients = new Il2CppSystem.Collections.Generic.List(); + ((InnerNetClient)AmongUsClient.Instance).GetAllClients(clients); + var cursor = clients.GetEnumerator(); + while (cursor.MoveNext()) + { + ClientData client = cursor.Current; + if (client != null && client.Id == clientId) + { + RememberClient(client); + return client; + } + } + } + catch + { + } + + return null; + } + +private static ClientData GetRecentClient(InnerNetClient client, int clientId) + { + if (client == null || clientId < 0) + { + return null; + } + + try + { + ClientData data = client.GetRecentClient(clientId); + RememberClient(data); + return data; + } + catch + { + return null; + } + } + +private static int GetActiveInboundSenderClientId() + { + long ageMs = Environment.TickCount64 - activeInboundSenderSetAtMs; + int frameAge = activeInboundSenderFrame < 0 ? int.MaxValue : Time.frameCount - activeInboundSenderFrame; + if (activeInboundSenderClientId < 0 || ageMs > ActiveInboundSenderTtlMs || (frameAge > 2 && ageMs > 80)) + { + ClearActiveInboundSender(); + return -1; + } + + return activeInboundSenderClientId; + } + +private static int ResolveBestActiveClientId(int clientId) + { + InnerNetClient inner = AmongUsClient.Instance == null ? null : (InnerNetClient)AmongUsClient.Instance; + if (IsRemoteClientIdValue(inner, clientId)) + { + return clientId; + } + + if (!string.IsNullOrWhiteSpace(activeInboundConnectionKey)) + { + if (FloodDropClientByConnectionKey.TryGetValue(activeInboundConnectionKey, out int droppedClientId) && IsRemoteClientIdValue(inner, droppedClientId)) + { + return droppedClientId; + } + + if (ClientIdByConnectionKey.TryGetValue(activeInboundConnectionKey, out int mappedClientId) && IsRemoteClientIdValue(inner, mappedClientId)) + { + return mappedClientId; + } + } + + if (IsRemoteClientIdValue(inner, activeInboundSenderClientId)) + { + return activeInboundSenderClientId; + } + + if (inner != null) + { + int singleClientId = ResolveSingleRemoteClientId(inner); + if (singleClientId >= 0) + return singleClientId; + } + + return clientId; + } + +private static void SetActiveInboundSender(int clientId) + { + if (clientId < 0 || AmongUsClient.Instance == null) + { + return; + } + + try + { + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + if (clientId == inner.ClientId || clientId == inner.HostId) + { + return; + } + } + catch + { + } + + activeInboundSenderClientId = clientId; + activeInboundSenderSetAtMs = Environment.TickCount64; + activeInboundSenderFrame = Time.frameCount; + } + +private static void ClearActiveInboundSender() + { + activeInboundSenderClientId = -1; + activeInboundConnectionKey = string.Empty; + activeInboundSenderSetAtMs = 0; + activeInboundSenderFrame = -1; + } + +private static void RememberRecentJoinCandidate(int clientId) + { + if (clientId < 0 || AmongUsClient.Instance == null) + { + return; + } + + try + { + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + if (clientId == inner.ClientId || clientId == inner.HostId) + { + return; + } + } + catch + { + } + + RecentJoinSenderCandidates[clientId] = Time.realtimeSinceStartup; + PruneRecentJoinSenderCandidates(); + } + +private static int ResolveSenderClientIdFromRecentJoin(InnerNetClient client) + { + PruneRecentJoinSenderCandidates(); + if (client == null || RecentJoinSenderCandidates.Count == 0) + { + return -1; + } + + int found = -1; + int count = 0; + float now = Time.realtimeSinceStartup; + foreach (KeyValuePair pair in RecentJoinSenderCandidates) + { + if (now - pair.Value > RecentJoinSenderFallbackSeconds) + { + continue; + } + + if (pair.Key == client.ClientId || pair.Key == client.HostId) + { + continue; + } + + found = pair.Key; + count++; + if (count > 1) + { + return -1; + } + } + + if (count != 1) + { + return -1; + } + + if (CountRemoteClients() != 1) + { + return -1; + } + + return found; + } + +private static int TryResolveRecentSingleFloodCandidate() + { + if (AmongUsClient.Instance == null) return -1; + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + PruneRecentJoinSenderCandidates(); + if (RecentJoinSenderCandidates.Count == 0) return -1; + const float recentJoinWindow = 3f; + float now = Time.realtimeSinceStartup; + int found = -1; + int count = 0; + foreach (KeyValuePair pair in RecentJoinSenderCandidates) + { + if (now - pair.Value > recentJoinWindow) continue; + if (pair.Key == inner.ClientId || pair.Key == inner.HostId) continue; + if (!IsKnownRemoteClient(pair.Key)) continue; + found = pair.Key; + count++; + if (count > 1) return -1; + } + return count == 1 ? found : -1; + } + +private static void PruneRecentJoinSenderCandidates() + { + if (RecentJoinSenderCandidates.Count == 0) + { + return; + } + + float now = Time.realtimeSinceStartup; + ScratchClientSnapshotIds.Clear(); + foreach (KeyValuePair pair in RecentJoinSenderCandidates) + { + if (now - pair.Value > RecentJoinSenderFallbackSeconds) + { + ScratchClientSnapshotIds.Add(pair.Key); + } + } + + for (int i = 0; i < ScratchClientSnapshotIds.Count; i++) + { + RecentJoinSenderCandidates.Remove(ScratchClientSnapshotIds[i]); + } + + ScratchClientSnapshotIds.Clear(); + } + +private static void RememberConnectionClient(string connectionKey, int clientId) + { + if (string.IsNullOrWhiteSpace(connectionKey) || clientId < 0 || AmongUsClient.Instance == null) + { + return; + } + + if (AmbiguousConnectionKeys.Contains(connectionKey)) + { + ConnectionKeySeenAt[connectionKey] = Time.realtimeSinceStartup; + return; + } + + try + { + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + if (clientId == inner.ClientId || clientId == inner.HostId) + { + return; + } + } + catch + { + } + + if (ClientIdByConnectionKey.TryGetValue(connectionKey, out int existingClientId) && existingClientId != clientId) + { + ClientIdByConnectionKey.Remove(connectionKey); + AmbiguousConnectionKeys.Add(connectionKey); + ConnectionKeySeenAt[connectionKey] = Time.realtimeSinceStartup; + AcovPlugin.Logger?.LogInfo((object)$"Network protection marked connection key as ambiguous: {TrimConnectionKeyForLog(connectionKey)} ({existingClientId} -> {clientId})."); + return; + } + + ClientIdByConnectionKey[connectionKey] = clientId; + ConnectionKeySeenAt[connectionKey] = Time.realtimeSinceStartup; + PruneConnectionKeys(); + } + +private static int ResolveConnectionClientId(string connectionKey, InnerNetClient client) + { + if (string.IsNullOrWhiteSpace(connectionKey) || AmbiguousConnectionKeys.Contains(connectionKey) || !ClientIdByConnectionKey.TryGetValue(connectionKey, out int clientId)) + { + return -1; + } + + if (!ConnectionKeySeenAt.TryGetValue(connectionKey, out float seenAt) || Time.realtimeSinceStartup - seenAt > ClientSnapshotTtlSeconds) + { + ClientIdByConnectionKey.Remove(connectionKey); + ConnectionKeySeenAt.Remove(connectionKey); + return -1; + } + + return IsRemoteClientIdValue(client, clientId) ? clientId : -1; + } + +private static void PruneConnectionKeys() + { + if (ConnectionKeySeenAt.Count == 0) + { + return; + } + + float now = Time.realtimeSinceStartup; + List expired = null; + foreach (KeyValuePair pair in ConnectionKeySeenAt) + { + if (!string.IsNullOrWhiteSpace(pair.Key) && now - pair.Value <= ClientSnapshotTtlSeconds) + { + continue; + } + + if (expired == null) + { + expired = new List(); + } + + expired.Add(pair.Key); + } + + if (expired == null) + { + return; + } + + for (int i = 0; i < expired.Count; i++) + { + string key = expired[i]; + if (string.IsNullOrWhiteSpace(key)) + { + continue; + } + + ClientIdByConnectionKey.Remove(key); + ConnectionKeySeenAt.Remove(key); + AmbiguousConnectionKeys.Remove(key); + } + } + +private static string TrimConnectionKeyForLog(string connectionKey) + { + if (string.IsNullOrWhiteSpace(connectionKey)) + { + return "-"; + } + + string clean = connectionKey.Trim(); + if (clean.Length <= 96) + { + return clean; + } + + return clean.Substring(0, 96) + "..."; + } + +private static string PacketConnectionKey(DataReceivedEventArgs eventArgs) + { + if (eventArgs == null) + { + return null; + } + + object connection = TryReadMember(eventArgs, "Connection", "connection", "Sender", "sender", "Peer", "peer", "Client", "client", "Conn", "conn", "Remote", "remote", "Socket", "socket"); + return StableObjectKey(connection) ?? StableObjectKey(eventArgs); + } + +private static string StableObjectKey(object source) + { + if (source == null) + { + return null; + } + + try + { + string endpoint = ReadFirstNonEmptyString(source, "EndPoint", "endpoint", "RemoteEndPoint", "remoteEndPoint", "Address", "address", "Host", "host", "Ip", "IP", "ip"); + if (!string.IsNullOrWhiteSpace(endpoint)) + { + return source.GetType().FullName + ":" + endpoint.Trim(); + } + } + catch + { + } + + try + { + return source.GetType().FullName + "#" + RuntimeHelpers.GetHashCode(source).ToString(); + } + catch + { + return null; + } + } + +private static string ReadFirstNonEmptyString(object source, params string[] memberNames) + { + if (source == null || memberNames == null) + { + return null; + } + + for (int i = 0; i < memberNames.Length; i++) + { + object value = TryReadMember(source, memberNames[i]); + if (value == null) + { + continue; + } + + try + { + string text = value.ToString(); + if (!string.IsNullOrWhiteSpace(text)) + { + return text; + } + } + catch + { + } + } + + return null; + } + +private static int ResolvePacketSenderClientId(InnerNetClient client, DataReceivedEventArgs eventArgs) + { + int clientId = ResolveConnectionClientId(activeInboundConnectionKey, client); + if (clientId >= 0) + { + return clientId; + } + + clientId = TryReadClientId(eventArgs); + if (IsKnownRemoteClient(client, clientId)) + { + RememberConnectionClient(activeInboundConnectionKey, clientId); + return clientId; + } + + object connection = TryReadMember(eventArgs, "Connection", "connection", "Sender", "sender", "Peer", "peer", "Client", "client", "Conn", "conn", "Remote", "remote", "Socket", "socket"); + if (TryResolveClientIdFromObject(client, connection, out clientId)) + { + RememberConnectionClient(activeInboundConnectionKey, clientId); + return clientId; + } + + object nestedConnection = TryReadMember(connection, "Connection", "connection", "Sender", "sender", "Client", "client", "Peer", "peer", "Owner", "owner", "Conn", "conn", "Remote", "remote", "Socket", "socket"); + if (TryResolveClientIdFromObject(client, nestedConnection, out clientId)) + { + RememberConnectionClient(activeInboundConnectionKey, clientId); + return clientId; + } + + if (TryResolveClientIdFromObject(client, eventArgs, out clientId)) + { + RememberConnectionClient(activeInboundConnectionKey, clientId); + return clientId; + } + + if (TryResolveClientIdFromMemberValues( + client, + eventArgs, + out clientId, + "Connection", "connection", "Sender", "sender", "Peer", "peer", "Client", "client", "Owner", "owner", + "Data", "data", "Packet", "packet", "Remote", "remote", "Socket", "socket", "Conn", "conn")) + { + RememberConnectionClient(activeInboundConnectionKey, clientId); + return clientId; + } + + return ResolveSenderClientIdFromRecentJoin(client); + } + +private static bool TryResolveClientIdFromObject(InnerNetClient client, object source, out int clientId) + { + clientId = TryReadClientId(source); + if (IsKnownRemoteClient(client, clientId)) + { + return true; + } + + return TryResolveClientIdFromLikelyMembers(client, source, out clientId); + } + +private static bool TryResolveClientIdFromMemberValues(InnerNetClient client, object source, out int clientId, params string[] memberNames) + { + clientId = -1; + if (source == null || memberNames == null) + { + return false; + } + + for (int i = 0; i < memberNames.Length; i++) + { + object member = TryReadMember(source, memberNames[i]); + if (member == null) + { + continue; + } + + if (TryResolveClientIdFromObject(client, member, out clientId)) + { + return true; + } + + object nested = TryReadMember(member, "Connection", "connection", "Sender", "sender", "Client", "client", "Peer", "peer", "Owner", "owner", "Conn", "conn", "Remote", "remote", "Socket", "socket"); + if (TryResolveClientIdFromObject(client, nested, out clientId)) + { + return true; + } + } + + return false; + } + +private sealed class TypeReflectionPlan + { + internal readonly MemberAccess[] IdCarriers; + internal readonly Dictionary ByName; + + internal TypeReflectionPlan(MemberAccess[] idCarriers, Dictionary byName) + { + IdCarriers = idCarriers; + ByName = byName; + } + } + +private readonly struct MemberAccess + { + private readonly PropertyInfo property; + private readonly FieldInfo field; + + internal MemberAccess(PropertyInfo property) + { + this.property = property; + field = null; + } + + internal MemberAccess(FieldInfo field) + { + property = null; + this.field = field; + } + + internal object Read(object target) + { + try + { + return property != null ? property.GetValue(target, null) : field?.GetValue(target); + } + catch + { + return null; + } + } + } + +private static readonly System.Collections.Concurrent.ConcurrentDictionary reflectionPlans = + new System.Collections.Concurrent.ConcurrentDictionary(); + +private static readonly string[] ClientIdMemberNames = + { + "ClientId", "clientId", "SenderId", "senderId", "OwnerId", "ownerId", + "PlayerId", "playerId", "Id", "id", + "ConnectionId", "connectionId", "ConnId", "connId", + "RemoteId", "remoteId", "SourceId", "sourceId", "PeerId", "peerId" + }; + +private static readonly string[] ConnectionTokens = { "client", "sender", "owner", "peer", "conn" }; + +private static TypeReflectionPlan PlanFor(object source) + { + if (source == null) + { + return null; + } + + Type type; + try + { + type = source.GetType(); + } + catch + { + return null; + } + + return reflectionPlans.GetOrAdd(type, BuildReflectionPlan); + } + +private static TypeReflectionPlan BuildReflectionPlan(Type type) + { + Dictionary byName = new Dictionary(StringComparer.Ordinal); + List idCarriers = new List(); + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; + + try + { + for (Type level = type; level != null && level != typeof(object); level = level.BaseType) + { + PropertyInfo[] properties; + try { properties = level.GetProperties(flags); } + catch { properties = Array.Empty(); } + + foreach (PropertyInfo property in properties) + { + if (property == null || property.GetIndexParameters().Length != 0) + { + continue; + } + + MemberAccess access = new MemberAccess(property); + if (!byName.ContainsKey(property.Name)) + { + byName[property.Name] = access; + } + + if (LooksLikeClientIdMember(property.Name)) + { + idCarriers.Add(access); + } + } + + FieldInfo[] fields; + try { fields = level.GetFields(flags); } + catch { fields = Array.Empty(); } + + foreach (FieldInfo field in fields) + { + if (field == null) + { + continue; + } + + MemberAccess access = new MemberAccess(field); + if (!byName.ContainsKey(field.Name)) + { + byName[field.Name] = access; + } + + if (LooksLikeClientIdMember(field.Name)) + { + idCarriers.Add(access); + } + } + } + } + catch + { + } + + return new TypeReflectionPlan(idCarriers.ToArray(), byName); + } + +private static bool TryResolveClientIdFromLikelyMembers(InnerNetClient client, object source, out int clientId) + { + clientId = -1; + if (client == null) + { + return false; + } + + TypeReflectionPlan plan = PlanFor(source); + if (plan == null) + { + return false; + } + + MemberAccess[] carriers = plan.IdCarriers; + for (int i = 0; i < carriers.Length; i++) + { + int candidate = TryReadClientId(carriers[i].Read(source)); + if (IsKnownRemoteClient(client, candidate)) + { + clientId = candidate; + return true; + } + } + + return false; + } + +private static bool LooksLikeClientIdMember(string memberName) + { + if (string.IsNullOrWhiteSpace(memberName)) + { + return false; + } + + string lower = memberName.Trim().ToLowerInvariant(); + if (lower.Length == 0) + { + return false; + } + + if (lower.EndsWith("id", StringComparison.Ordinal)) + { + return true; + } + + for (int i = 0; i < ConnectionTokens.Length; i++) + { + if (lower.IndexOf(ConnectionTokens[i], StringComparison.Ordinal) >= 0) + { + return true; + } + } + + return false; + } + +private static int ResolveSingleRemoteClientId(InnerNetClient client) + { + if (client == null || AmongUsClient.Instance == null) + { + return -1; + } + + try + { + int found = -1; + int count = 0; + var clients = ((InnerNetClient)AmongUsClient.Instance).allClients.GetEnumerator(); + while (clients.MoveNext()) + { + ClientData data = clients.Current; + if (data == null || data.Id < 0 || data.Id == client.ClientId || data.Id == client.HostId) + { + continue; + } + + found = data.Id; + count++; + if (count > 1) + { + return -1; + } + } + + return count == 1 ? found : -1; + } + catch + { + return -1; + } + } + +private static int CountRemoteClients() + { + if (AmongUsClient.Instance == null) + { + return 0; + } + + try + { + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + int count = 0; + var clients = inner.allClients.GetEnumerator(); + while (clients.MoveNext()) + { + ClientData data = clients.Current; + if (data != null && data.Id >= 0 && data.Id != inner.ClientId && data.Id != inner.HostId) + { + count++; + } + } + + return count; + } + catch + { + return 0; + } + } + +private static bool IsKnownRemoteClient(int clientId) + { + if (AmongUsClient.Instance == null) + { + return false; + } + + return IsKnownRemoteClient((InnerNetClient)AmongUsClient.Instance, clientId); + } + +private static bool IsKnownRemoteClient(InnerNetClient client, int clientId) + { + if (client == null || clientId < 0 || clientId == client.ClientId || clientId == client.HostId) + { + return false; + } + + return GetClientById(clientId) != null + || GetRecentClient(client, clientId) != null + || TryGetClientSnapshot(clientId, out _); + } + +private static bool IsRemoteClientIdValue(InnerNetClient client, int clientId) + { + if (clientId < 0) + { + return false; + } + + if (client == null) + { + return AmongUsClient.Instance == null || IsRemoteClientIdValue((InnerNetClient)AmongUsClient.Instance, clientId); + } + + return clientId != client.ClientId && clientId != client.HostId; + } + +private static int TryReadClientId(object source) + { + if (source == null) + { + return -1; + } + + if (TryConvertToInt(source, out int direct)) + { + return direct; + } + + object member = TryReadMember(source, ClientIdMemberNames); + return TryConvertToInt(member, out int nested) ? nested : -1; + } + +private static bool TryConvertToInt(object value, out int result) + { + result = -1; + if (value == null) + { + return false; + } + + switch (value) + { + case int i: result = i; return true; + case uint u: result = unchecked((int)u); return true; + case byte b: result = b; return true; + case sbyte sb: result = sb; return true; + case short s: result = s; return true; + case ushort us: result = us; return true; + case long l: result = unchecked((int)l); return true; + case ulong ul: result = unchecked((int)ul); return true; + case Enum e: + try { result = Convert.ToInt32(e); return true; } + catch { return false; } + } + + try + { + string text = value.ToString(); + return !string.IsNullOrWhiteSpace(text) && int.TryParse(text, out result); + } + catch + { + result = -1; + return false; + } + } + +private static object TryReadMember(object source, params string[] memberNames) + { + if (memberNames == null || memberNames.Length == 0) + { + return null; + } + + TypeReflectionPlan plan = PlanFor(source); + if (plan == null) + { + return null; + } + + for (int i = 0; i < memberNames.Length; i++) + { + string name = memberNames[i]; + if (name != null && plan.ByName.TryGetValue(name, out MemberAccess access)) + { + object value = access.Read(source); + if (value != null) + { + return value; + } + } + } + + return null; + } + +private static string PlayerName(PlayerControl player) + { + try + { + string name = player?.Data?.PlayerName; + if (!string.IsNullOrWhiteSpace(name)) + { + return StripTags(name); + } + } + catch + { + } + + return player == null ? "Unknown" : $"Player {player.PlayerId}"; + } + +private static string ClientName(int clientId) + { + try + { + if (AmongUsClient.Instance != null) + { + InnerNetClient client = (InnerNetClient)AmongUsClient.Instance; + ClientData data = GetClientById(clientId) ?? GetRecentClient(client, clientId); + if (data != null) + { + return AcovAccessLists.ClientDisplayName(data, clientId); + } + } + } + catch + { + } + + return SnapshotDisplayName(clientId); + } + +private static string RpcName(byte rpcByte) + { + try + { + if (Enum.IsDefined(typeof(RpcCalls), rpcByte)) + { + return ((RpcCalls)rpcByte).ToString(); + } + } + catch + { + } + + return $"RPC {rpcByte}"; + } + +private static bool IsDangerousChat(string text) + { + if (string.IsNullOrEmpty(text)) + { + return false; + } + + if (text.Length > 200) + { + return true; + } + + string lower = text.ToLowerInvariant(); + if (lower.Contains("= 0) + { + spriteCount++; + if (spriteCount > 3) + { + return true; + } + + index += 7; + } + + return false; + } + +private static string StripTags(string value) + { + if (string.IsNullOrEmpty(value) || value.IndexOf('<') < 0) + { + return value ?? string.Empty; + } + + char[] buffer = new char[value.Length]; + int count = 0; + bool insideTag = false; + for (int i = 0; i < value.Length; i++) + { + char ch = value[i]; + if (ch == '<') + { + insideTag = true; + continue; + } + + if (ch == '>') + { + insideTag = false; + continue; + } + + if (!insideTag) + { + buffer[count++] = ch; + } + } + + return new string(buffer, 0, count); + } + +private static void Notice(string title, string detail) + { + float now = Time.realtimeSinceStartup; + if (now - lastScreenNoticeAt < 0.75f) + { + return; + } + + lastScreenNoticeAt = now; + AcovNotifications.Show(title, detail, 3.2f); + } + +internal static int ResolvePlayerClientId(PlayerControl player) + { + return GetPlayerClientId(player); + } + +internal static void FlagLobbyTeleport(PlayerControl player, int clientId, float dist) + { + string action = ModOptions.LobbyTeleportAction == null + ? "Warn" + : ModOptions.NormalizeNetworkProtectionAction(ModOptions.LobbyTeleportAction.Value); + string name = PlayerName(player); + string title = AcovText.T("Телепорт в лобби", "Lobby teleport"); + string detail = AcovText.T($"Снап на {dist:F1} ед.", $"Snapped {dist:F1} units."); + AcovPlugin.Logger?.LogWarning((object)$"Lobby teleport: {name} (client {clientId}) snapped {dist:F1} units."); + bool notify = ShouldShowProtectionNotice(clientId, title, detail); + if (notify && action != "Null") + { + AcovSecurityNotifications.Show(action, name, title, detail, clientId); + } + ApplyProtectionAction(clientId, title, detail, action); + } +} +} diff --git a/anticheat/Acov/Patches/NetworkProtectionGuard.State.cs b/anticheat/Acov/Patches/NetworkProtectionGuard.State.cs new file mode 100644 index 0000000..853b9f4 --- /dev/null +++ b/anticheat/Acov/Patches/NetworkProtectionGuard.State.cs @@ -0,0 +1,1213 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static partial class NetworkProtectionGuard +{ + +private struct RpcEnvelopeContext + { + internal uint NetId; + internal byte CallId; + internal int TargetClientId; + internal int SenderClientId; + internal int DataIndex; + internal int DataCount; + internal SendOption SendOption; + internal long CreatedAtMs; + } + +private struct ClientSnapshot + { + internal int ClientId; + internal byte PlayerId; + internal int OwnerId; + internal string PlayerName; + internal string FriendCode; + internal string ProductUserId; + internal float JoinedAt; + internal float LastSeenAt; + } + +private const int MaxMessagesPerFrame = 200; + +private const int DefaultRpcLimitPerWindow = 10; + +private const float DefaultRpcWindowSeconds = 1f; + +private const int MaxChatMessagesPerWindow = 5; + +private const float ChatWindowSeconds = 2f; + +private const int MaxGameDataPartsPerPacket = 160; + +private const int LobbyJoinGameDataPartsLimit = 320; + +private const int MaxTrackedGameDataParts = LobbyJoinGameDataPartsLimit + 1; + +private const int DataFloodCountPerSec = 60; + +private const int DataFloodBytesPerSec = 65536; + +private const int DataSinglePayloadMax = 4096; + +private const float EarlyJoinVoteGuardSeconds = 5f; + +private const float LobbyEnteredSyncGraceSeconds = 5f; + +private const float LobbyRecentJoinSyncGraceSeconds = 4f; + +private const float LobbyJoinWarmupSeconds = 12f; + +private const float JoinIntegrityDelaySeconds = 25f; + +private const float JoinIntegrityRetrySeconds = 10f; + +private const float JoinIntegrityMaxWaitSeconds = 120f; + +private const float MeetingTransitionGraceSeconds = 8f; + +private const float ClientSnapshotTtlSeconds = 600f; + +private const float RecentJoinSenderFallbackSeconds = 12f; + +private const long ActiveInboundSenderTtlMs = 500; + +private const float FloodConnectionDropSeconds = 14f; + +private const float ProtectionNoticeRepeatSeconds = 1.25f; + +private static readonly HashSet ValidMessageTags = new HashSet + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, + 11, 12, 13, 14, 16, 17, 22, 24, + }; + +private static readonly HashSet LegacyExploitRpcIds = new HashSet + { + }; + +private static readonly HashSet SuspiciousRpcIds = new HashSet + { + 85, 101, + }; + +private static readonly HashSet AllowedCustomRpcIds = new HashSet + { + 212, 213, 214, 215, 216, 217, 218, 219, + 151, 152, 176, 210, + 70, 78, 80, 81, + }; + +private static readonly HashSet ImmediateRpcIds = new HashSet + { + 45, 46, 47, 48, 51, 52, 53, 54, 55, 56, + 62, 63, 64, 65, + 2, 5, 7, 12, 14, 21, + }; + +private static readonly HashSet PackedDataAllowedRpcIds = new HashSet + { + 38, 39, 40, 41, 42, 43, + 5, 6, 8, + }; + +private static readonly HashSet TargetedRpcAllowedIds = new HashSet + { + 13, 51, 55, 62, 64, 5, 7, + }; + +private static readonly HashSet LobbyAllowedRpcIds = new HashSet + { + 212, 213, 214, 215, 216, 217, 218, 219, + 151, 152, 176, 210, 70, 78, 80, 81, + 36, 37, 38, 39, 40, 41, 42, 43, + 2, 5, 7, 9, 10, 13, 17, 18, 21, 33, 49, 50, 60, 61, + }; + +private static readonly HashSet CosmeticMutationRpcIds = new HashSet + { + 5, 6, 8, 39, 40, 41, 42, 43, + }; + +private static readonly HashSet NameStringRpcIds = new HashSet { 5, 6 }; + +private const int MaxRpcStringBytes = 512; + +private const int MaxSpawnsPerScene = 300; + +private const int MaxUnownedSpawnsPerScene = 100; + +private static int spawnSceneId; + +private static int spawnsThisScene; + +private static int unownedSpawnsThisScene; + +internal static void ResetSpawnFloodCounters() + { + spawnSceneId = 0; + spawnsThisScene = 0; + unownedSpawnsThisScene = 0; + } + +private static readonly Dictionary HighVolumeRpcLimits = new Dictionary + { + { 49, (100, 0.1f) }, + { 50, (100, 0.1f) }, + { 18, (100, 0.1f) }, + { 7, (100, 0.1f) }, + { 44, (50, 0.1f) }, + { 6, (30, 0.1f) }, + { 8, (30, 0.1f) }, + { 39, (30, 0.1f) }, + { 40, (30, 0.1f) }, + { 41, (30, 0.1f) }, + { 42, (30, 0.1f) }, + { 21, (15, 1f) }, + { 54, (5, 1f) }, + { 33, (1, 1f) }, + }; + +private static readonly Dictionary>> SameRpcByClient = new Dictionary>>(); + +private static readonly Dictionary> ChatByClient = new Dictionary>(); + +private static readonly Dictionary LastTaskRpcAt = new Dictionary(); + +private static readonly Dictionary ClientJoinTimeAt = new Dictionary(); + +private static readonly Dictionary RecentJoinSenderCandidates = new Dictionary(); + +private static readonly Dictionary LastClientActionAt = new Dictionary(); + +private static readonly Dictionary PendingLocalCleanupAt = new Dictionary(); + +private static readonly Dictionary PendingJoinIntegrityAt = new Dictionary(); + +private static readonly Dictionary ClientSnapshotsById = new Dictionary(); + +private static readonly Dictionary ClientIdByPlayerId = new Dictionary(); + +private static readonly Dictionary ClientIdByOwnerId = new Dictionary(); + +private static readonly Dictionary MessagesThisFrameByClient = new Dictionary(); + +private static readonly HashSet WarnedFloodClientsThisFrame = new HashSet(); + +private static readonly Dictionary ClientIdByConnectionKey = new Dictionary(); + +private static readonly Dictionary ConnectionKeySeenAt = new Dictionary(); + +private static readonly HashSet AmbiguousConnectionKeys = new HashSet(); + +private static readonly Dictionary FloodDropClientUntil = new Dictionary(); + +private static readonly Dictionary FloodDropConnectionUntil = new Dictionary(); + +private static readonly Dictionary FloodDropClientByConnectionKey = new Dictionary(); + +private static readonly Dictionary HostRateDropByKey = new Dictionary(); + +private static readonly Dictionary HostDatagramsThisFrameByKey = new Dictionary(); + +private static int hostDatagramFrame = -1; + +private static readonly Dictionary FloodAttributionByKey = new Dictionary(); + +private const float FloodAttributionWindowSeconds = 5f; + +private const float SpoofingSuppressionSeconds = 30f; + +private sealed class FloodAttributionState + { + public readonly HashSet DistinctClientIds = new HashSet(); + public float WindowStart; + public float SuppressUntil; + } + +private static readonly Dictionary ProtectionNoticeSeenAt = new Dictionary(); + +private static readonly HashSet WarnedModRpcOnce = new HashSet(); + +private static readonly List ScratchClientSnapshotIds = new List(16); + +private static readonly List ScratchConnectionKeys = new List(16); + +private static readonly List PendingRpcContexts = new List(32); + +private static readonly HashSet ScratchSpawnNetIds = new HashSet(); + +private sealed class DataBurst { public readonly Queue Ts = new Queue(); public readonly Queue By = new Queue(); public int Total; } + +private static readonly Dictionary DataByNetId = new Dictionary(); + +private static float nextDataFloodCleanupAt; + +private static readonly Dictionary> NonHostRpcByKey = new Dictionary>(); + +private static float nextNonHostRpcCleanupAt; + +private static readonly Queue SnapToAggregate = new Queue(); + +private const int MaxSnapToAggregatePerSec = 30; + +private static int messageFrame = -1; + +private static int messagesThisFrame; + +private static float nonHostFloodDropUntil = 0f; + +private static int nonHostDatagramFrame = -1; + +private static int nonHostDatagramsThisFrame = 0; + +private const int NonHostFloodDatagramThreshold = 50; + +private static int lastRememberedClientsFrame = -1; + +private static float lastScreenNoticeAt = -10f; + +private static float lastLobbyJoinAt = -1000f; + +private static float lastLobbyEnteredAt = -1000f; + +private static int lastObservedLobbyGameState = -1; + +private static byte trustedLocalCosmeticPlayerId = byte.MaxValue; + +private static int trustedLocalCosmeticClientId = -1; + +private static float trustedLocalCosmeticUntil = -1f; + +private static int trustedLocalCosmeticBudget; + +private static bool trustedLocalCosmeticPacket; + +private static int activeInboundSenderClientId = -1; + +private static string activeInboundConnectionKey; + +private static int activeInboundTargetClientId = -1; + +private static int activeInboundGameDataParts = 0; + +private static bool activeInboundHasInvalidPartTag; + +private static bool activeInboundHasSpawnExploit; + +private static string activeInboundSpawnExploitDetail; + +private static bool activeInboundHasDataFlood; + +private static string activeInboundDataFloodDetail; + +private static bool activeInboundPhantomFlood; + +private static bool activeInboundHasPhantomNetId; + +private static readonly Dictionary pendingUnknownNetId = new Dictionary(); + +private static long lastPendingUnknownCleanupMs; + +private const int UnknownNetIdGraceMs = 2000; + +private static long phantomDataWindowMs; + +private static int phantomDataInWindow; + +private const int MaxPhantomDataPerSec = 30; + +private static long activeInboundSenderSetAtMs; + +private static int activeInboundSenderFrame = -1; + +private static float lastMeetingRpcAt = -1000f; + +private static float lastInboundSenderErrorAt = -10f; + +internal static void TrackInboundSender(InnerNetClient client, DataReceivedEventArgs eventArgs) + { + try + { + ClearActiveInboundSender(); + + if (AcovNetPacketMonitor.Enabled && client != null && !client.AmHost && eventArgs != null) + { + AcovNetPacketMonitor.RecordInboundPacket(-1, eventArgs.Message); + } + + if (!Enabled() || client == null || !client.AmHost || eventArgs == null) + { + if (EnabledNonHostFloodDrop() && client != null && !client.AmHost) + { + int frame = Time.frameCount; + if (frame != nonHostDatagramFrame) + { + nonHostDatagramFrame = frame; + nonHostDatagramsThisFrame = 0; + } + nonHostDatagramsThisFrame++; + if (nonHostDatagramsThisFrame > NonHostFloodDatagramThreshold) + { + nonHostFloodDropUntil = Time.realtimeSinceStartup + FloodConnectionDropSeconds; + } + } + return; + } + + activeInboundConnectionKey = PacketConnectionKey(eventArgs) ?? string.Empty; + + if (!string.IsNullOrWhiteSpace(activeInboundConnectionKey)) + { + float nowFast = Time.realtimeSinceStartup; + + if (FloodDropConnectionUntil.TryGetValue(activeInboundConnectionKey, out float dropUntil) && nowFast <= dropUntil) + { + if (FloodDropClientByConnectionKey.TryGetValue(activeInboundConnectionKey, out int droppedFast) && droppedFast >= 0) + { + SetActiveInboundSender(droppedFast); + if (AcovNetPacketMonitor.Enabled) + { + AcovNetPacketMonitor.RecordInboundPacket(droppedFast, eventArgs.Message); + } + } + return; + } + + int dgFrame = Time.frameCount; + if (dgFrame != hostDatagramFrame) + { + hostDatagramFrame = dgFrame; + HostDatagramsThisFrameByKey.Clear(); + } + HostDatagramsThisFrameByKey.TryGetValue(activeInboundConnectionKey, out int dgCount); + dgCount++; + HostDatagramsThisFrameByKey[activeInboundConnectionKey] = dgCount; + if (dgCount > NonHostFloodDatagramThreshold) + { + HostRateDropByKey[activeInboundConnectionKey] = nowFast + FloodConnectionDropSeconds; + return; + } + + if (HostRateDropByKey.TryGetValue(activeInboundConnectionKey, out float rateDrop) && nowFast < rateDrop) + { + return; + } + } + + RememberAllClients(); + int senderClientId = ResolvePacketSenderClientId(client, eventArgs); + if (senderClientId < 0) + { + senderClientId = ResolveConnectionClientId(activeInboundConnectionKey, client); + } + + if (senderClientId < 0 && TryGetFloodDropClientForActiveConnection(out int droppedClientId)) + { + senderClientId = droppedClientId; + } + + if (senderClientId < 0) + { + return; + } + + ClientData sender = GetClientById(senderClientId) ?? GetRecentClient(client, senderClientId); + RememberClient(sender); + RememberConnectionClient(activeInboundConnectionKey, senderClientId); + SetActiveInboundSender(senderClientId); + if (AcovNetPacketMonitor.Enabled) + { + AcovNetPacketMonitor.RecordInboundPacket(ResolveBestActiveClientId(senderClientId), eventArgs.Message); + } + } + catch (Exception error) + { + ClearActiveInboundSender(); + float now = Time.realtimeSinceStartup; + if (now - lastInboundSenderErrorAt > 2f) + { + lastInboundSenderErrorAt = now; + AcovPlugin.Logger?.LogWarning((object)$"Network protection ignored sender tracking error: {error.Message}"); + } + } + } + +internal static bool CheckMessage(InnerNetClient client, MessageReader reader, SendOption sendOption) + { + if (!Enabled()) + { + if (client != null && reader != null && !ValidMessageTags.Contains(reader.Tag)) + { + return HarmonyControl.SkipOriginal; + } + + if (client != null) + { + int so = (int)sendOption; + if (so != 0 && so != 1) + { + return HarmonyControl.SkipOriginal; + } + } + + if (client != null && reader != null && (reader.Tag == 5 || reader.Tag == 6)) + { + if (reader.Length - reader.Position < 4) + { + return HarmonyControl.SkipOriginal; + } + + if (!GameDataFramingValid(reader, reader.Tag)) + { + AcovSecurityNotifications.Show("Warn", null, "Malformed GameData (non-host)", "Bad framing", -1); + return HarmonyControl.SkipOriginal; + } + + try + { + MessageReader copy = MessageReader.Get(reader); + copy.ReadInt32(); + if (reader.Tag == 6) + { + copy.ReadPackedInt32(); + } + + int parts = 0; + while (copy.Position < copy.Length && parts < MaxTrackedGameDataParts) + { + MessageReader part = copy.ReadMessage(); + if (part == null) break; + parts++; + byte partTag = part.Tag; + if (partTag == 0 || partTag == 3 || partTag > 8) + { + return HarmonyControl.SkipOriginal; + } + + if (partTag == 1 && client != null && !client.AmHost && + ModOptions.NonHostDataDrop != null && ModOptions.NonHostDataDrop.Value) + { + try + { + MessageReader idc = MessageReader.Get(part); + uint nid = idc.ReadPackedUInt32(); + int by = idc.Length - idc.Position; + + if (AmongUsClient.Instance != null) + { + InnerNetClient innerData = (InnerNetClient)AmongUsClient.Instance; + if (nid != 0 && + !innerData.allObjects.AllObjectsFast.ContainsKey(nid) && + !innerData.DestroyedObjects.Contains(nid)) + { + bool flood = PhantomDataRateExceeded(); + bool phantom = IsPhantomNetId(nid); + if (flood && phantom) + { + AcovSecurityNotifications.Show("Warn", null, "Phantom DATA flood (non-host)", $"netId {nid}", -1); + return HarmonyControl.SkipOriginal; + } + } + } + + if (ShouldDropDataObject(nid, by)) + { + AcovSecurityNotifications.Show("Warn", null, "DATA flood (non-host)", $"netId {nid}", -1); + return HarmonyControl.SkipOriginal; + } + } + catch { } + } + + if (partTag == 2 && client != null && !client.AmHost && + ModOptions.NonHostDataDrop != null && ModOptions.NonHostDataDrop.Value) + { + try + { + MessageReader rc = MessageReader.Get(part); + uint rnid = rc.ReadPackedUInt32(); + byte rcall = rc.ReadByte(); + + if (NameRpcStringTooLong(part, rcall)) + { + AcovSecurityNotifications.Show("Warn", null, "Oversized name RPC (non-host)", $"#{rcall} netId {rnid}", -1); + return HarmonyControl.SkipOriginal; + } + + if (VentRpcOutOfRange(part, rcall)) + { + AcovSecurityNotifications.Show("Warn", null, "Invalid vent id (non-host)", $"#{rcall} netId {rnid}", -1); + return HarmonyControl.SkipOriginal; + } + + bool rpcFlood = ShouldDropNonHostHighVolumeRpc(rnid, rcall); + if (rcall == 21 && SnapToAggregateExceeded()) + { + rpcFlood = true; + } + + bool rpcPhantom = false; + if (!rpcFlood && AmongUsClient.Instance != null) + { + InnerNetClient innerRpc = (InnerNetClient)AmongUsClient.Instance; + if (rnid != 0 && + !innerRpc.allObjects.AllObjectsFast.ContainsKey(rnid) && + !innerRpc.DestroyedObjects.Contains(rnid)) + { + bool rflood = PhantomDataRateExceeded(); + if (rflood && IsPhantomNetId(rnid)) + { + rpcFlood = true; + rpcPhantom = true; + } + } + } + + if (rpcFlood) + { + string what = rpcPhantom ? $"Phantom RPC flood (non-host) #{rcall}" + : rcall == 21 ? "SnapTo flood (non-host)" : $"RPC flood (non-host) #{rcall}"; + AcovSecurityNotifications.Show("Warn", null, what, $"netId {rnid}", -1); + return HarmonyControl.SkipOriginal; + } + } + catch { } + } + + if (partTag == 4 && client != null && !client.AmHost && SpawnShouldDrop(part)) + { + AcovSecurityNotifications.Show("Warn", null, "Spawn flood (non-host)", "fake-owner / over cap", -1); + return HarmonyControl.SkipOriginal; + } + + if (partTag == 4 && client != null && !client.AmHost && IdenticalNetIdProtectionEnabled() && + SpawnHasDuplicateNetId(part)) + { + AcovSecurityNotifications.Show("Warn", null, "Duplicate spawn (non-host)", "netId already live", -1); + return HarmonyControl.SkipOriginal; + } + } + + if (parts > LobbyJoinGameDataPartsLimit) + { + return HarmonyControl.SkipOriginal; + } + } + catch { } + } + + if (EnabledNonHostFloodDrop() && client != null && reader != null) + { + float now = Time.realtimeSinceStartup; + if (now < nonHostFloodDropUntil) + return HarmonyControl.SkipOriginal; + if (ShouldBlockMessageFrameFlood(-1, out _)) + { + nonHostFloodDropUntil = now + FloodConnectionDropSeconds; + return HarmonyControl.SkipOriginal; + } + } + return HarmonyControl.Continue; + } + + if (!string.IsNullOrWhiteSpace(activeInboundConnectionKey) && + HostRateDropByKey.TryGetValue(activeInboundConnectionKey, out float hostRateUntil) && + Time.realtimeSinceStartup < hostRateUntil) + { + return HarmonyControl.SkipOriginal; + } + + if (IsTrustedLocalCosmeticApplyActive()) + { + return HarmonyControl.Continue; + } + + PlatformSpoofGuard.InspectRawJoinMessage(client, reader); + + if (AcovAccessLists.TryHandleJoinMessage(client, reader)) + { + return HarmonyControl.SkipOriginal; + } + + if (client == null) + { + return HarmonyControl.Continue; + } + + RememberAllClients(); + if (reader == null) + { + BlockMessage(GetActiveInboundSenderClientId(), "Сетевой пакет заблокирован", "Пустой MessageReader."); + return HarmonyControl.SkipOriginal; + } + + try + { + ResetInboundEnvelope(sendOption); + + int senderClientId = ResolveBestActiveClientId(GetActiveInboundSenderClientId()); + + if (senderClientId < 0 && reader != null) + { + int contentId = TryResolveOwnerFromGameDataContent(client, reader); + if (contentId >= 0) + { + SetActiveInboundSender(contentId); + senderClientId = contentId; + } + } + + if (ShouldDropInboundFlood(senderClientId)) + { + return HarmonyControl.SkipOriginal; + } + + if (ShouldBlockMessageFrameFlood(senderClientId, out int frameCount)) + { + RegisterInboundFloodDrop(senderClientId); + string floodAction = IsFloodAttributionUnreliable(senderClientId) ? "Null" : null; + BlockMessage(senderClientId, "Message flood", $"{frameCount}/{MaxMessagesPerFrame} messages in one frame.", floodAction); + return HarmonyControl.SkipOriginal; + } + + if (!ReaderLooksSane(reader)) + { + BlockMessage(senderClientId, "Сетевой пакет заблокирован", $"Позиция {reader.Position}, длина {reader.Length}."); + return HarmonyControl.SkipOriginal; + } + + byte tag = reader.Tag; + if (!ValidMessageTags.Contains(tag)) + { + BlockMessage(senderClientId, "Сетевой пакет заблокирован", $"Неверный tag: {tag}.", "Null"); + return HarmonyControl.SkipOriginal; + } + + if ((tag == 5 || tag == 6) && reader.Length - reader.Position < 4) + { + BlockMessage(senderClientId, "GameData заблокирован", "Пакет слишком короткий.", "Null"); + return HarmonyControl.SkipOriginal; + } + + if (tag == 1) + { + CaptureJoinEnvelope(reader); + senderClientId = GetActiveInboundSenderClientId(); + } + + if (tag == 5 || tag == 6) + { + if (!GameDataFramingValid(reader, tag)) + { + if (senderClientId >= 0) RegisterInboundFloodDrop(senderClientId); + string malformedAction = ModOptions.MalformedDataAction != null ? ModOptions.MalformedDataAction.Value : "Warn"; + BlockMessage(senderClientId, "Malformed GameData", "Bad framing — dropped before parse.", malformedAction); + return HarmonyControl.SkipOriginal; + } + + CaptureGameDataEnvelope(reader, tag, sendOption); + int dataPartsLimit = IsLobbyJoinSyncGrace() ? LobbyJoinGameDataPartsLimit : MaxGameDataPartsPerPacket; + if (activeInboundGameDataParts > dataPartsLimit) + { + RegisterInboundFloodDrop(senderClientId); + BlockMessage(senderClientId, "GameData flood", $"{activeInboundGameDataParts}/{dataPartsLimit} parts.", "Null"); + return HarmonyControl.SkipOriginal; + } + + if (activeInboundHasInvalidPartTag) + { + RegisterInboundFloodDrop(senderClientId); + BlockMessage(senderClientId, "Invalid GameData tag", "Sub-message tag out of valid range.", "Null"); + return HarmonyControl.SkipOriginal; + } + + if (activeInboundHasPhantomNetId && activeInboundPhantomFlood) + { + if (senderClientId >= 0) RegisterInboundFloodDrop(senderClientId); + BlockMessage(senderClientId, "Phantom DATA flood", "Flood of DATA for non-existent netId(s).", "Null"); + return HarmonyControl.SkipOriginal; + } + + if (activeInboundHasSpawnExploit) + { + if (senderClientId >= 0) RegisterInboundFloodDrop(senderClientId); + BlockMessage(senderClientId, "Identical NetId attack", activeInboundSpawnExploitDetail ?? "Spawn payload would cause server desync."); + return HarmonyControl.SkipOriginal; + } + + if (activeInboundHasDataFlood) + { + if (senderClientId >= 0) RegisterInboundFloodDrop(senderClientId); + BlockMessage(senderClientId, "DATA flood", activeInboundDataFloodDetail ?? "Too many DATA updates on one object.", "Warn"); + return HarmonyControl.SkipOriginal; + } + } + + return HarmonyControl.Continue; + } + catch (Exception error) + { + AcovPlugin.Logger?.LogWarning((object)$"Network protection blocked a malformed message: {error.Message}"); + BlockMessage(GetActiveInboundSenderClientId(), "Сетевой пакет заблокирован", "Ошибка проверки пакета."); + return HarmonyControl.SkipOriginal; + } + } + +private static bool ShouldBlockMessageFrameFlood(int senderClientId, out int frameCount) + { + if (Time.frameCount != messageFrame) + { + messageFrame = Time.frameCount; + messagesThisFrame = 0; + MessagesThisFrameByClient.Clear(); + WarnedFloodClientsThisFrame.Clear(); + } + + messagesThisFrame++; + if (senderClientId < 0) + { + frameCount = messagesThisFrame; + return frameCount > MaxMessagesPerFrame; + } + + MessagesThisFrameByClient.TryGetValue(senderClientId, out frameCount); + frameCount++; + MessagesThisFrameByClient[senderClientId] = frameCount; + return frameCount > MaxMessagesPerFrame; + } + +private static bool ShouldDropInboundFlood(int senderClientId) + { + PruneFloodDrops(); + float now = Time.realtimeSinceStartup; + senderClientId = ResolveBestActiveClientId(senderClientId); + if (senderClientId >= 0 && FloodDropClientUntil.TryGetValue(senderClientId, out float clientUntil)) + { + if (now <= clientUntil) + { + return true; + } + + FloodDropClientUntil.Remove(senderClientId); + } + + if (!string.IsNullOrWhiteSpace(activeInboundConnectionKey) && + FloodDropConnectionUntil.TryGetValue(activeInboundConnectionKey, out float connectionUntil)) + { + if (now <= connectionUntil) + { + if (activeInboundSenderClientId < 0 && FloodDropClientByConnectionKey.TryGetValue(activeInboundConnectionKey, out int droppedClientId)) + { + SetActiveInboundSender(droppedClientId); + } + + return true; + } + + FloodDropConnectionUntil.Remove(activeInboundConnectionKey); + FloodDropClientByConnectionKey.Remove(activeInboundConnectionKey); + } + + return false; + } + +private static void RegisterInboundFloodDrop(int clientId) + { + PruneFloodDrops(); + clientId = ResolveBestActiveClientId(clientId); + + if (clientId < 0 && AmongUsClient.Instance != null) + { + clientId = ResolveSingleRemoteClientId((InnerNetClient)AmongUsClient.Instance); + } + + if (clientId < 0) + { + clientId = TryResolveRecentSingleFloodCandidate(); + } + + float until = Time.realtimeSinceStartup + FloodConnectionDropSeconds; + if (clientId >= 0) + { + FloodDropClientUntil[clientId] = until; + } + + if (string.IsNullOrWhiteSpace(activeInboundConnectionKey)) + { + return; + } + + bool keyIsAmbiguous = AmbiguousConnectionKeys.Contains(activeInboundConnectionKey); + if (!keyIsAmbiguous || clientId >= 0) + { + FloodDropConnectionUntil[activeInboundConnectionKey] = until; + } + if (clientId >= 0) + { + FloodDropClientByConnectionKey[activeInboundConnectionKey] = clientId; + RememberConnectionClient(activeInboundConnectionKey, clientId); + } + } + +private static bool IsFloodAttributionUnreliable(int clientId) + { + if (string.IsNullOrWhiteSpace(activeInboundConnectionKey)) + { + return false; + } + + bool keyAmbiguous = AmbiguousConnectionKeys.Contains(activeInboundConnectionKey); + if (keyAmbiguous) + { + return true; + } + + if (clientId < 0) + { + return false; + } + + float now = Time.realtimeSinceStartup; + if (!FloodAttributionByKey.TryGetValue(activeInboundConnectionKey, out FloodAttributionState state)) + { + state = new FloodAttributionState { WindowStart = now }; + state.DistinctClientIds.Add(clientId); + FloodAttributionByKey[activeInboundConnectionKey] = state; + return false; + } + + if (now < state.SuppressUntil) + { + return true; + } + + if (now - state.WindowStart > FloodAttributionWindowSeconds) + { + state.DistinctClientIds.Clear(); + state.WindowStart = now; + } + + state.DistinctClientIds.Add(clientId); + if (state.DistinctClientIds.Count >= 2) + { + state.SuppressUntil = now + SpoofingSuppressionSeconds; + return true; + } + + return false; + } + +private static bool TryGetFloodDropClientForActiveConnection(out int clientId) + { + clientId = -1; + if (string.IsNullOrWhiteSpace(activeInboundConnectionKey)) + { + return false; + } + + PruneFloodDrops(); + return FloodDropClientByConnectionKey.TryGetValue(activeInboundConnectionKey, out clientId) && clientId >= 0; + } + +private static void PruneFloodDrops() + { + float now = Time.realtimeSinceStartup; + ScratchClientSnapshotIds.Clear(); + foreach (KeyValuePair pair in FloodDropClientUntil) + { + if (now > pair.Value) + { + ScratchClientSnapshotIds.Add(pair.Key); + } + } + + for (int i = 0; i < ScratchClientSnapshotIds.Count; i++) + { + FloodDropClientUntil.Remove(ScratchClientSnapshotIds[i]); + } + + ScratchClientSnapshotIds.Clear(); + ScratchConnectionKeys.Clear(); + foreach (KeyValuePair pair in FloodDropConnectionUntil) + { + if (string.IsNullOrWhiteSpace(pair.Key) || now > pair.Value) + { + ScratchConnectionKeys.Add(pair.Key); + } + } + + for (int i = 0; i < ScratchConnectionKeys.Count; i++) + { + string key = ScratchConnectionKeys[i]; + if (string.IsNullOrWhiteSpace(key)) + { + continue; + } + + FloodDropConnectionUntil.Remove(key); + FloodDropClientByConnectionKey.Remove(key); + } + + ScratchConnectionKeys.Clear(); + foreach (KeyValuePair pair in HostRateDropByKey) + { + if (string.IsNullOrWhiteSpace(pair.Key) || now > pair.Value) + ScratchConnectionKeys.Add(pair.Key); + } + for (int i = 0; i < ScratchConnectionKeys.Count; i++) + HostRateDropByKey.Remove(ScratchConnectionKeys[i]); + + ScratchConnectionKeys.Clear(); + foreach (KeyValuePair pair in FloodAttributionByKey) + { + if (string.IsNullOrWhiteSpace(pair.Key)) { ScratchConnectionKeys.Add(pair.Key); continue; } + FloodAttributionState s = pair.Value; + bool windowExpired = now - s.WindowStart > FloodAttributionWindowSeconds; + bool suppressionExpired = s.SuppressUntil <= 0f || now > s.SuppressUntil; + if (windowExpired && suppressionExpired) ScratchConnectionKeys.Add(pair.Key); + } + for (int i = 0; i < ScratchConnectionKeys.Count; i++) + { + FloodAttributionByKey.Remove(ScratchConnectionKeys[i]); + } + ScratchConnectionKeys.Clear(); + } + +private static bool SpawnHasDuplicateNetId(MessageReader part) + { + if (part == null || AmongUsClient.Instance == null) return false; + try + { + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + MessageReader copy = MessageReader.Get(part); + copy.ReadPackedUInt32(); + copy.ReadPackedInt32(); + copy.ReadByte(); + int compCount = copy.ReadPackedInt32(); + if (compCount <= 0) return false; + int scan = compCount > 32 ? 32 : compCount; + for (int i = 0; i < scan; i++) + { + if (copy.Position >= copy.Length) break; + uint netId = copy.ReadPackedUInt32(); + if (netId != 0 && + inner.allObjects.AllObjectsFast.ContainsKey(netId) && + !inner.DestroyedObjects.Contains(netId)) + { + return true; + } + + MessageReader init = copy.ReadMessage(); + if (init == null) break; + } + + return false; + } + catch + { + return false; + } + } + +private static bool ShouldDropDataObject(uint netId, int payloadBytes) + { + if (payloadBytes >= DataSinglePayloadMax) + { + return true; + } + + float now = Time.realtimeSinceStartup; + if (!DataByNetId.TryGetValue(netId, out DataBurst b)) + { + b = new DataBurst(); + DataByNetId[netId] = b; + } + + while (b.Ts.Count > 0 && now - b.Ts.Peek() > 1f) + { + b.Ts.Dequeue(); + if (b.By.Count > 0) b.Total -= b.By.Dequeue(); + } + + b.Ts.Enqueue(now); + b.By.Enqueue(payloadBytes); + b.Total += payloadBytes; + + if (now >= nextDataFloodCleanupAt) + { + nextDataFloodCleanupAt = now + 5f; + CleanupDataBursts(now); + } + + return b.Ts.Count > DataFloodCountPerSec || b.Total > DataFloodBytesPerSec; + } + +private static void CleanupDataBursts(float now) + { + if (DataByNetId.Count == 0) return; + List dead = null; + foreach (KeyValuePair kv in DataByNetId) + { + DataBurst b = kv.Value; + while (b.Ts.Count > 0 && now - b.Ts.Peek() > 1f) + { + b.Ts.Dequeue(); + if (b.By.Count > 0) b.Total -= b.By.Dequeue(); + } + if (b.Ts.Count == 0) + { + (dead ??= new List()).Add(kv.Key); + } + } + if (dead != null) + { + for (int i = 0; i < dead.Count; i++) DataByNetId.Remove(dead[i]); + } + } + +private static bool ShouldDropNonHostHighVolumeRpc(uint netId, byte callId) + { + if (!HighVolumeRpcLimits.TryGetValue(callId, out (int Limit, float Window) rule)) + { + return false; + } + + float now = Time.realtimeSinceStartup; + long key = ((long)netId << 8) | callId; + if (!NonHostRpcByKey.TryGetValue(key, out Queue q)) + { + q = new Queue(); + NonHostRpcByKey[key] = q; + } + + while (q.Count > 0 && now - q.Peek() > rule.Window) + { + q.Dequeue(); + } + + q.Enqueue(now); + + if (now >= nextNonHostRpcCleanupAt) + { + nextNonHostRpcCleanupAt = now + 5f; + CleanupNonHostRpc(now); + } + + return q.Count > rule.Limit; + } + +private static bool SpawnShouldDrop(MessageReader part) + { + if (ModOptions.SpawnFloodGuard != null && !ModOptions.SpawnFloodGuard.Value) + { + return false; + } + + try + { + if (AmongUsClient.Instance == null) + { + return false; + } + + int sceneId = ((InnerNetClient)AmongUsClient.Instance).GameId; + if (sceneId == 0) + { + return false; + } + + if (sceneId != spawnSceneId) + { + spawnSceneId = sceneId; + spawnsThisScene = 0; + unownedSpawnsThisScene = 0; + } + + bool fabricatedOwner = false; + if (part != null) + { + try + { + InnerNetClient inner = (InnerNetClient)AmongUsClient.Instance; + MessageReader copy = MessageReader.Get(part); + copy.ReadPackedUInt32(); + int ownerId = copy.ReadPackedInt32(); + fabricatedOwner = ownerId != -2 && ownerId != inner.ClientId && !IsKnownRemoteClient(ownerId); + } + catch + { + } + } + + if (fabricatedOwner) + { + unownedSpawnsThisScene++; + return unownedSpawnsThisScene > MaxUnownedSpawnsPerScene; + } + + spawnsThisScene++; + return spawnsThisScene > MaxSpawnsPerScene; + } + catch + { + return false; + } + } +} +} diff --git a/anticheat/Acov/Patches/PlatformSpoofGuard.cs b/anticheat/Acov/Patches/PlatformSpoofGuard.cs new file mode 100644 index 0000000..7bbfaf7 --- /dev/null +++ b/anticheat/Acov/Patches/PlatformSpoofGuard.cs @@ -0,0 +1,63 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +internal static class PlatformSpoofGuard +{ + internal static void InspectRawJoinMessage(InnerNetClient client, MessageReader reader) { } +} +} diff --git a/anticheat/Acov/Patches/PlayerRpcProtectionPatch.cs b/anticheat/Acov/Patches/PlayerRpcProtectionPatch.cs new file mode 100644 index 0000000..ee4a66c --- /dev/null +++ b/anticheat/Acov/Patches/PlayerRpcProtectionPatch.cs @@ -0,0 +1,69 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +[HarmonyPatch(typeof(PlayerControl), "HandleRpc")] +internal static class PlayerRpcProtectionPatch +{ + public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] int callId, [HarmonyArgument(1)] MessageReader reader) + { + if (!ElysiumModMenu.ElysiumModMenuGUI.oldAntiCheatVersion) return true; + using (Acov.AcovProfiler.Sample("Net.CheckRpc")) + return NetworkProtectionGuard.CheckRpc(__instance, callId, reader); + } +} +} diff --git a/anticheat/Acov/Patches/ShipStatusRpcProtectionPatch.cs b/anticheat/Acov/Patches/ShipStatusRpcProtectionPatch.cs new file mode 100644 index 0000000..238b725 --- /dev/null +++ b/anticheat/Acov/Patches/ShipStatusRpcProtectionPatch.cs @@ -0,0 +1,68 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +[HarmonyPatch(typeof(ShipStatus), "HandleRpc")] +internal static class ShipStatusRpcProtectionPatch +{ + public static bool Prefix(ShipStatus __instance, [HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) + { + if (!ElysiumModMenu.ElysiumModMenuGUI.oldAntiCheatVersion) return true; + return NetworkProtectionGuard.CheckShipStatusRpc(__instance, callId, reader); + } +} +} diff --git a/anticheat/Acov/Patches/VoteKickRpcProtectionPatch.cs b/anticheat/Acov/Patches/VoteKickRpcProtectionPatch.cs new file mode 100644 index 0000000..0a3c351 --- /dev/null +++ b/anticheat/Acov/Patches/VoteKickRpcProtectionPatch.cs @@ -0,0 +1,68 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace Acov.Patches +{ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using BepInEx.Configuration; +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; + + +[HarmonyPatch(typeof(VoteBanSystem), "HandleRpc")] +internal static class VoteKickRpcProtectionPatch +{ + public static bool Prefix(VoteBanSystem __instance, [HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) + { + if (!ElysiumModMenu.ElysiumModMenuGUI.oldAntiCheatVersion) return true; + return NetworkProtectionGuard.CheckVoteKickRpc(__instance, callId, reader); + } +} +} diff --git a/anticheat/ElysiumAnticheat.cs b/anticheat/ElysiumAnticheat.cs new file mode 100644 index 0000000..a871d3c --- /dev/null +++ b/anticheat/ElysiumAnticheat.cs @@ -0,0 +1,438 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { +public static class ElysiumAnticheat + { + public static void Flag(PlayerControl player, string reason) + { + if (player == null || player.Data == null || player == PlayerControl.LocalPlayer) return; + + string pName = player.Data.PlayerName ?? "Unknown"; + + int mode = ElysiumModMenuGUI.punishmentMode; + + if (mode >= 1) + { + ElysiumModMenuGUI.ShowNotification($"[ANTICHEAT] {pName}: {reason}"); + } + + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + if (mode == 2) + { + AmongUsClient.Instance.KickPlayer(player.OwnerId, false); + } + else if (mode == 3) + { + string fc = string.IsNullOrEmpty(player.Data.FriendCode) ? "Unknown" : player.Data.FriendCode; + string puid = "Unknown"; + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(player); + if (client != null) puid = GetClientPuid(client); + } + catch { } + + ElysiumModMenuGUI.AddToBanList(fc, puid, pName, $"Anticheat: {reason}"); + + AmongUsClient.Instance.KickPlayer(player.OwnerId, true); + } + } + } + } + +[HarmonyPatch(typeof(PlayerPhysics), "RpcBootFromVent")] + public static class Anticheat_RpcBootFromVent_Patch + { + public static bool Prefix(PlayerPhysics __instance) + { + if (!ElysiumModMenuGUI.blockSpoofRPC) return true; + + try + { + if (__instance == null || __instance.myPlayer == null) return true; + if (!__instance.myPlayer.inVent) return false; + } + catch { } + + return true; + } + } + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] + public static class Anticheat_PlayerControl_RPC + { + private static readonly Dictionary> chatRpcTimes = new Dictionary>(); + private static readonly Dictionary> meetingRpcTimes = new Dictionary>(); + private static readonly HashSet lobbyGameRpcs = new HashSet + { + (byte)RpcCalls.MurderPlayer, + (byte)RpcCalls.ReportDeadBody, + (byte)RpcCalls.StartMeeting, + (byte)RpcCalls.EnterVent, + (byte)RpcCalls.ExitVent, + (byte)RpcCalls.Shapeshift, + (byte)RpcCalls.ProtectPlayer + }; + + private static bool IsFlooded(Dictionary> map, byte playerId, int maxCalls, float windowSeconds) + { + float now = Time.unscaledTime; + if (!map.TryGetValue(playerId, out Queue times)) + { + times = new Queue(); + map[playerId] = times; + } + + times.Enqueue(now); + while (times.Count > 0 && now - times.Peek() > windowSeconds) + times.Dequeue(); + + return times.Count > maxCalls; + } + + public static bool Prefix(PlayerControl __instance, byte callId, Hazel.MessageReader reader) + { + if (__instance != null && __instance != PlayerControl.LocalPlayer && __instance.Data != null && ElysiumModMenuGUI.enablePasosLimit) + { + } + + if (!ElysiumModMenuGUI.blockSpoofRPC && + !ElysiumModMenuGUI.blockSabotageRPC && + !ElysiumModMenuGUI.blockGameRpcInLobby && + !ElysiumModMenuGUI.blockChatFloodRpc && + !ElysiumModMenuGUI.blockMeetingFloodRpc) return true; + if (__instance == null || __instance == PlayerControl.LocalPlayer || __instance.Data == null) return true; + + int oldPos = reader.Position; + bool isCheat = false; + string cheatReason = ""; + + try + { + if (ElysiumModMenuGUI.blockGameRpcInLobby && + AmongUsClient.Instance != null && + !AmongUsClient.Instance.IsGameStarted && + lobbyGameRpcs.Contains(callId)) + { + isCheat = true; + cheatReason = $"Game RPC in lobby ({((RpcCalls)callId)})"; + } + + if (!isCheat && ElysiumModMenuGUI.blockChatFloodRpc && + (callId == (byte)RpcCalls.SendChat || callId == (byte)RpcCalls.SendQuickChat)) + { + if (IsFlooded(chatRpcTimes, __instance.PlayerId, ElysiumModMenuGUI.chatRpcLimit, ElysiumModMenuGUI.chatRpcWindow)) + { + isCheat = true; + cheatReason = "Chat RPC flood"; + } + } + + + if (!isCheat && ElysiumModMenuGUI.enableQuickChatEmptyGuard && + callId == (byte)RpcCalls.SendQuickChat) + { + int qcPos = reader.Position; + int zeroRun = 0, zeroMax = 0, scanned = 0; + while (reader.Position < reader.Length && scanned < 4096) + { + scanned++; + if (reader.ReadByte() == 0) { zeroRun++; if (zeroRun > zeroMax) zeroMax = zeroRun; } + else zeroRun = 0; + } + reader.Position = qcPos; + + if (zeroMax >= 8) + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && + __instance != null && __instance != PlayerControl.LocalPlayer && + __instance.OwnerId != AmongUsClient.Instance.HostId) + { + try + { + bool qcBan = ElysiumModMenuGUI.banQuickChatEmptySpammer; + string qcName = (__instance.Data != null && !string.IsNullOrEmpty(__instance.Data.PlayerName)) + ? __instance.Data.PlayerName : $"Client {__instance.OwnerId}"; + if (qcBan) + { + string qcFc = (__instance.Data != null && !string.IsNullOrEmpty(__instance.Data.FriendCode)) + ? __instance.Data.FriendCode : "Unknown"; + string qcPuid = "Unknown"; + try + { + var qcClient = AmongUsClient.Instance.GetClientFromCharacter(__instance); + if (qcClient != null) qcPuid = ElysiumModMenuGUI.GetClientPuid(qcClient); + } + catch { } + ElysiumModMenuGUI.AddToBanList(qcFc, qcPuid, qcName, "QuickChat Empty spam (anti-crash)"); + } + AmongUsClient.Instance.KickPlayer(__instance.OwnerId, qcBan); + ElysiumModMenuGUI.ShowNotification($"[ANTI-CRASH] {qcName} {(qcBan ? "banned" : "kicked")}: QuickChat spam"); + } + catch { } + } + return false; + } + } + + if (!isCheat && ElysiumModMenuGUI.blockMeetingFloodRpc && + (callId == (byte)RpcCalls.StartMeeting || callId == (byte)RpcCalls.ReportDeadBody)) + { + if (IsFlooded(meetingRpcTimes, __instance.PlayerId, ElysiumModMenuGUI.meetingRpcLimit, ElysiumModMenuGUI.meetingRpcWindow)) + { + isCheat = true; + cheatReason = "Meeting RPC flood"; + } + } + + if (!isCheat && ElysiumModMenuGUI.blockSpoofRPC) + { + if (callId == (byte)RpcCalls.SetColor) + { + uint netId = reader.ReadUInt32(); + byte color = reader.ReadByte(); + if (color >= Palette.PlayerColors.Length) { isCheat = true; cheatReason = $"Invalid Color ID ({color})"; } + } + else if (callId == (byte)RpcCalls.SetName || callId == (byte)RpcCalls.CheckName) + { + uint netId = callId == (byte)RpcCalls.SetName ? reader.ReadUInt32() : 0; + string reqName = reader.ReadString(); + if (reqName.Length > 12) { isCheat = true; cheatReason = "Name length too long"; } + if (reqName.Contains("<")) { isCheat = true; cheatReason = "HTML Tags in name"; } + } + else if (callId == (byte)RpcCalls.SetScanner) + { + bool scanning = reader.ReadBoolean(); + if (scanning && RoleManager.IsImpostorRole(__instance.Data.RoleType)) + { isCheat = true; cheatReason = "Scanner activated as Impostor"; } + } + else if (callId == (byte)RpcCalls.PlayAnimation) + { + byte anim = reader.ReadByte(); + if (RoleManager.IsImpostorRole(__instance.Data.RoleType)) + { isCheat = true; cheatReason = "Task Animation as Impostor"; } + } + else if (callId == (byte)RpcCalls.EnterVent || callId == (byte)RpcCalls.ExitVent) + { + if (!__instance.Data.IsDead && __instance.Data.Role != null && !__instance.Data.Role.CanVent) + { isCheat = true; cheatReason = "Vent without vent ability"; } + + if (GameManager.Instance != null && GameManager.Instance.IsHideAndSeek() && RoleManager.IsImpostorRole(__instance.Data.RoleType)) + { isCheat = true; cheatReason = "Venting as Seeker in H&S"; } + } + } + + if (!isCheat && ElysiumModMenuGUI.blockSabotageRPC) + { + if (callId == (byte)RpcCalls.ReportDeadBody) + { + if (GameManager.Instance != null && GameManager.Instance.IsHideAndSeek()) + { isCheat = true; cheatReason = "Reported body in H&S"; } + } + else if (callId == (byte)RpcCalls.SetStartCounter) + { + reader.ReadPackedInt32(); + sbyte counter = reader.ReadSByte(); + + if (__instance.OwnerId != AmongUsClient.Instance.HostId && counter != -1) + { isCheat = true; cheatReason = "Start counter changed by non-host"; } + } + } + } + catch { } + + reader.Position = oldPos; + + if (isCheat) + { + ElysiumAnticheat.Flag(__instance, cheatReason); + return false; + } + + return true; + } + } + +[HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.HandleRpc))] + public static class Anticheat_ShipStatus_RPC + { + public static bool Prefix(ShipStatus __instance, byte callId, Hazel.MessageReader reader) + { + if (!ElysiumModMenuGUI.blockSabotageRPC) return true; + + int oldPos = reader.Position; + bool isCheat = false; + string cheatReason = ""; + PlayerControl sender = null; + + try + { + if (callId == (byte)RpcCalls.UpdateSystem) + { + SystemTypes system = (SystemTypes)reader.ReadByte(); + sender = reader.ReadNetObject(); + + if (sender != null && !sender.AmOwner) + { + if (system == SystemTypes.Sabotage) + { + SystemTypes sabSystem = (SystemTypes)reader.ReadByte(); + if (sender.Data != null && !RoleManager.IsImpostorRole(sender.Data.RoleType)) + { isCheat = true; cheatReason = "Triggered Sabotage as Crewmate"; } + } + } + } + else if (callId == (byte)RpcCalls.CloseDoorsOfType) + { + if (GameManager.Instance != null && GameManager.Instance.IsHideAndSeek()) + { isCheat = true; cheatReason = "Closed doors in H&S"; } + } + } + catch { } + + reader.Position = oldPos; + + if (isCheat && sender != null && sender != PlayerControl.LocalPlayer) + { + ElysiumAnticheat.Flag(sender, cheatReason); + return false; + } + + return true; + } + } + +public static bool autoChatEveryone = false; + +public static bool pendingAutoMeeting = false; + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckColor))] + public static class AllowDuplicateColors_CheckColor_Patch + { + private static bool applyingDuplicateColor; + + public static bool Prefix(PlayerControl __instance, byte bodyColor) + { + if (applyingDuplicateColor || !ElysiumModMenuGUI.allowDuplicateColors || + __instance == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || + bodyColor == byte.MaxValue) + return true; + + try + { + applyingDuplicateColor = true; + __instance.RpcSetColor(bodyColor); + return false; + } + catch { return true; } + finally { applyingDuplicateColor = false; } + } + } + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Start))] + public static class Anticheat_Platform_Check + { + public static void Postfix(PlayerControl __instance) + { + if ((!ElysiumModMenuGUI.blockSpoofRPC && !ElysiumModMenuGUI.autoBanPlatformSpoof && !ElysiumModMenuGUI.banCustomPlatformsFromTxt) || + __instance == null || __instance == PlayerControl.LocalPlayer) return; + + try + { + var clientData = AmongUsClient.Instance.GetClientFromCharacter(__instance); + if (clientData == null || clientData.PlatformData == null) return; + + if (ElysiumModMenuGUI.banCustomPlatformsFromTxt && + MatchesPlatformBanTxt(clientData, out string customPlatformName, out string token)) + { + HostBanForPlatform(__instance, $"Custom platform TXT match '{token}' ({customPlatformName})"); + return; + } + + var platform = clientData.PlatformData; + string pName = platform.PlatformName; + ulong xuid = platform.XboxPlatformId; + ulong psid = platform.PsnPlatformId; + + bool isValid = true; + + switch (platform.Platform) + { + case Platforms.StandaloneEpicPC: + case Platforms.StandaloneSteamPC: + case Platforms.StandaloneMac: + case Platforms.StandaloneItch: + case Platforms.IPhone: + case Platforms.Android: + isValid = (pName == "TESTNAME" && xuid == 0 && psid == 0); + break; + case Platforms.StandaloneWin10: + isValid = (pName == "TESTNAME" && xuid != 0 && psid == 0); + break; + case Platforms.Xbox: + isValid = (pName != "TESTNAME" && pName.Length >= 3 && xuid != 0 && psid == 0); + break; + case Platforms.Playstation: + isValid = (pName != "TESTNAME" && xuid == 0 && psid != 0); + break; + case Platforms.Switch: + isValid = (pName != "TESTNAME" && xuid == 0 && psid == 0); + break; + } + + if (!isValid) + { + string reason = $"Platform Spoof detected ({platform.Platform})"; + if (ElysiumModMenuGUI.autoBanPlatformSpoof) + HostBanForPlatform(__instance, reason); + else if (ElysiumModMenuGUI.blockSpoofRPC) + ElysiumAnticheat.Flag(__instance, reason); + } + } + catch { } + } + } + } +} diff --git a/features/AnimationToggles.cs b/features/AnimationToggles.cs new file mode 100644 index 0000000..214b427 --- /dev/null +++ b/features/AnimationToggles.cs @@ -0,0 +1,832 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +private static void ApplyCameraZoomTick() + { + try + { + Camera mainCamera = Camera.main; + Camera uiCamera = HudManager.Instance?.UICamera; + if (mainCamera == null) return; + + if (!cameraZoom) + { + bool changed = Mathf.Abs(mainCamera.orthographicSize - 3f) > 0.001f || + (uiCamera != null && Mathf.Abs(uiCamera.orthographicSize - 3f) > 0.001f); + + mainCamera.orthographicSize = 3f; + if (uiCamera != null) uiCamera.orthographicSize = 3f; + + if (zoomResolutionRefreshNeeded || changed) + { + RefreshHudResolutionForZoom(); + zoomResolutionRefreshNeeded = false; + } + + return; + } + + float scrollWheel = Input.GetAxis("Mouse ScrollWheel"); + if (Mathf.Abs(scrollWheel) <= 0.0001f || !IsCameraZoomScrollAllowed()) return; + + if (scrollWheel < 0f) + { + mainCamera.orthographicSize += 1f; + if (uiCamera != null) uiCamera.orthographicSize += 1f; + zoomResolutionRefreshNeeded = true; + RefreshHudResolutionForZoom(); + } + else if (scrollWheel > 0f && mainCamera.orthographicSize > 3f) + { + mainCamera.orthographicSize -= 1f; + if (uiCamera != null) uiCamera.orthographicSize = Mathf.Max(3f, uiCamera.orthographicSize - 1f); + zoomResolutionRefreshNeeded = true; + RefreshHudResolutionForZoom(); + } + } + catch { } + } + +[HarmonyPatch(typeof(PassiveButton), nameof(PassiveButton.ReceiveClickDown))] + public static class HardMenu_BlockClickDown_Patch + { + public static bool Prefix() { return !ElysiumModMenuGUI.IsCursorOverMenu(); } + } + +[HarmonyPatch(typeof(PassiveButton), nameof(PassiveButton.ReceiveClickUp))] + public static class HardMenu_BlockClickUp_Patch + { + public static bool Prefix() { return !ElysiumModMenuGUI.IsCursorOverMenu(); } + } + +private void DrawPlayersTab() + { + GUILayout.BeginHorizontal(); + for (int i = 0; i < playersSubTabs.Length; i++) + if (GUILayout.Button(playersSubTabs[i], currentPlayersSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) + { currentPlayersSubTab = i; scrollPosition = Vector2.zero; } + GUILayout.EndHorizontal(); + GUILayout.Space(8); + + if (currentPlayersSubTab == 1) + { + DrawPlayersHistoryTab(); + return; + } + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(menuCardStyle, GUILayout.Width(200)); + playerListScrollPos = GUILayout.BeginScrollView(playerListScrollPos); + if (lockedPlayersList.Count > 0) + { + foreach (var pc in lockedPlayersList) + { + if (pc == null || pc.Data == null || pc.PlayerId >= 100) continue; + string pName = pc.Data.PlayerName ?? "Unknown"; + + if (forcedPreGameRoles.ContainsKey(pc.PlayerId)) pName += " [*]"; + else if (forcedImpostors.Contains(pc.PlayerId)) pName += " [Imp]"; + + bool isSelected = selectedAntiCheatPlayerId == pc.PlayerId; + + GUI.contentColor = Color.white; + try { GUI.contentColor = Palette.PlayerColors[pc.Data.DefaultOutfit.ColorId]; } catch { } + + if (GUILayout.Button(pName, isSelected ? activeTabStyle : btnStyle, GUILayout.Height(30))) + { + selectedAntiCheatPlayerId = pc.PlayerId; + } + GUI.contentColor = Color.white; + } + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + + GUILayout.Space(8); GUILayout.BeginVertical(menuCardStyle, GUILayout.ExpandWidth(true)); + playerActionScrollPos = GUILayout.BeginScrollView(playerActionScrollPos); + + PlayerControl target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedAntiCheatPlayerId); + + if (target != null && target.Data != null) + { + GUILayout.Label($"Selected: {target.Data.PlayerName}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 14 }); + GUILayout.Space(10); + GUILayout.BeginHorizontal(); + + GUI.backgroundColor = new Color(0.8f, 0.2f, 0.2f, 1f); + if (GUILayout.Button("KILL", btnStyle, GUILayout.Height(25))) + { + Vector3 op = PlayerControl.LocalPlayer.transform.position; + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(target.transform.position); + PlayerControl.LocalPlayer.CmdCheckMurder(target); + PlayerControl.LocalPlayer.RpcMurderPlayer(target, true); + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(op); + } + GUI.backgroundColor = Color.white; + + if (GUILayout.Button("TP TO", activeTabStyle, GUILayout.Height(25))) + { + teleportToPlayer(target); + ShowNotification($"[TELEPORT] Телепортирован к {target.Data.PlayerName}!"); + } + + GUI.backgroundColor = new Color(1f, 0.5f, 0f, 1f); + if (GUILayout.Button("Force Eject", btnStyle, GUILayout.Height(25))) ForceGlobalEject(target); + GUI.backgroundColor = Color.white; + + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + + if (GUILayout.Button("Force Meeting", btnStyle, GUILayout.Height(25))) ForceMeetingAsPlayer(target); + + bool hr = rainbowPlayers.Contains(target.PlayerId); + if (GUILayout.Button(hr ? "RGB: ON" : "RGB: OFF", hr ? activeTabStyle : btnStyle, GUILayout.Height(25))) + { + if (!hr) rainbowPlayers.Add(target.PlayerId); + else rainbowPlayers.Remove(target.PlayerId); + } + + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + + if (GUILayout.Button("Report Body", btnStyle, GUILayout.Height(25))) + AttemptReportBody(target); + + if (GUILayout.Button("Flood Tasks", btnStyle, GUILayout.Height(25))) + FloodPlayerWithTasks(target); + + if (GUILayout.Button("Clear Tasks", btnStyle, GUILayout.Height(25))) + ClearPlayerTasks(target); + + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + DrawMenuSectionHeader("TARGET ROLE CONTROL"); + + GUILayout.BeginHorizontal(); + GUIStyle roleMidStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetMenuAccentColor() }, alignment = TextAnchor.MiddleCenter }; + if (GUILayout.Button("<", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) + { + targetRoleAssignIdx--; + if (targetRoleAssignIdx < 0) targetRoleAssignIdx = roleAssignOptions.Length - 1; + } + GUILayout.Label(roleAssignNames[targetRoleAssignIdx], roleMidStyle, GUILayout.Height(24), GUILayout.ExpandWidth(true)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) + { + targetRoleAssignIdx++; + if (targetRoleAssignIdx >= roleAssignOptions.Length) targetRoleAssignIdx = 0; + } + GUILayout.EndHorizontal(); + + GUILayout.Space(4); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("TARGET -> ROLE", btnStyle, GUILayout.Height(26))) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ОШИБКА] Требуются права хоста!"); + } + else + { + if (IsGhostRoleSelection(targetRoleAssignIdx)) + { + MakePlayerGhost(target); + } + else if (IsGhostImpostorRoleSelection(targetRoleAssignIdx)) + { + MakePlayerGhost(target, true); + } + else + { + SetPlayerRole(target, roleAssignOptions[targetRoleAssignIdx]); + ShowNotification($"[ROLE] {target.Data.PlayerName} -> {roleAssignNames[targetRoleAssignIdx]}"); + } + } + } + if (GUILayout.Button("TARGET -> GHOST", btnStyle, GUILayout.Height(26))) + { + MakePlayerGhost(target); + } + GUILayout.EndHorizontal(); + + GUILayout.Space(4); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("REVIVE TARGET", activeTabStyle, GUILayout.Height(26))) + { + RevivePlayer(target); + } + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + GUILayout.Label("Morph Target:", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11 }); + GUILayout.BeginHorizontal(); + + int mIdx = lockedPlayersList.FindIndex(p => p.PlayerId == selectedMorphTargetId); + + GUI.backgroundColor = GetMenuControlAccentColor(); + if (GUILayout.Button("<", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) + { + if (lockedPlayersList.Count > 0) { mIdx--; if (mIdx < 0) mIdx = lockedPlayersList.Count - 1; selectedMorphTargetId = lockedPlayersList[mIdx].PlayerId; } + } + GUI.backgroundColor = Color.white; + + string morphName = "Target"; + if (mIdx >= 0 && mIdx < lockedPlayersList.Count) morphName = lockedPlayersList[mIdx].Data.PlayerName; + if (morphName.Length > 10) morphName = morphName.Substring(0, 10) + ".."; + + GUIStyle morphLabelStyle = new GUIStyle(btnStyle); + morphLabelStyle.normal.background = null; + morphLabelStyle.hover.background = null; + morphLabelStyle.normal.textColor = GetMenuAccentColor(); + morphLabelStyle.fontStyle = FontStyle.Bold; + morphLabelStyle.alignment = TextAnchor.MiddleCenter; + + GUILayout.Label(morphName, morphLabelStyle, GUILayout.Height(25), GUILayout.ExpandWidth(true)); + + GUI.backgroundColor = GetMenuControlAccentColor(); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) + { + if (lockedPlayersList.Count > 0) { mIdx++; if (mIdx >= lockedPlayersList.Count) mIdx = 0; selectedMorphTargetId = lockedPlayersList[mIdx].PlayerId; } + } + GUILayout.EndHorizontal(); + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + GUI.backgroundColor = GetMenuControlAccentColor(); + if (GUILayout.Button("MORPH TARGET", btnStyle, GUILayout.Width(160), GUILayout.Height(25))) + { + var morphTarget = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedMorphTargetId) ?? target; + this.StartCoroutine(AttemptShapeshiftFrame(target, morphTarget).WrapToIl2Cpp()); + } + GUI.backgroundColor = Color.white; + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + DrawMenuSectionHeader("SET PLAYER COLOR"); + GUILayout.BeginVertical(); + + GUIStyle roundedColorBtnStyle = new GUIStyle(); + roundedColorBtnStyle.normal.background = texColorBtn; + roundedColorBtnStyle.margin = CreateRectOffset(2, 2, 2, 2); + + int colorsPerRow = 7; + for (int i = 0; i < Palette.PlayerColors.Length; i++) + { + if (i % colorsPerRow == 0) GUILayout.BeginHorizontal(); + + GUI.color = Palette.PlayerColors[i]; + + if (GUILayout.Button("", roundedColorBtnStyle, GUILayout.Width(32), GUILayout.Height(30))) + target.RpcSetColor((byte)i); + + if (i % colorsPerRow == colorsPerRow - 1 || i == Palette.PlayerColors.Length - 1) + GUILayout.EndHorizontal(); + } + GUI.color = Color.white; + GUILayout.EndVertical(); + + GUILayout.Space(15); + DrawMenuSectionHeader("PRE-GAME ROLE (HOST)"); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Impostor", btnStyle, GUILayout.Height(25))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Add(target.PlayerId); enablePreGameRoleForce = true; } + if (GUILayout.Button("Crewmate", btnStyle, GUILayout.Height(25))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Crewmate; enablePreGameRoleForce = true; } + if (GUILayout.Button("Shapeshifter", btnStyle, GUILayout.Height(25))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Shapeshifter; enablePreGameRoleForce = true; } + GUILayout.EndHorizontal(); + GUILayout.Space(5); + if (GUILayout.Button("REMOVE FORCED ROLE", activeTabStyle, GUILayout.Height(25))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Remove(target.PlayerId); } + } + else + { + GUILayout.FlexibleSpace(); + GUILayout.Label("Select a player...", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); + GUILayout.FlexibleSpace(); + } + + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + + GUILayout.EndHorizontal(); + } + +private void DrawPlayersHistoryTab() + { + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader("PLAYER HISTORY"); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"Entries: {playerHistoryEntries.Count}", new GUIStyle(toggleLabelStyle) { fontSize = 11, clipping = TextClipping.Overflow, wordWrap = false }, GUILayout.MinWidth(128), GUILayout.ExpandWidth(false), GUILayout.Height(24)); + GUILayout.Label("File: ElysiumPlayerHistory.txt", new GUIStyle(toggleLabelStyle) { fontSize = 11, clipping = TextClipping.Overflow, wordWrap = false }, GUILayout.MinWidth(220), GUILayout.ExpandWidth(false), GUILayout.Height(24)); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Clear History", btnStyle, GUILayout.Width(136), GUILayout.Height(24))) + { + playerHistoryEntries.Clear(); + playerHistoryKeysById.Clear(); + WritePlayerHistoryFile(); + } + GUILayout.EndHorizontal(); + + GUILayout.Space(6); + playersHistoryScroll = GUILayout.BeginScrollView(playersHistoryScroll); + if (playerHistoryEntries.Count == 0) + { + GUILayout.Label("История пока пустая.", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); + } + else + { + foreach (var e in playerHistoryEntries.OrderByDescending(x => x.LastSeenUtc)) + { + GUILayout.BeginVertical(); + string status = e.IsOnline ? "ONLINE" : "LEFT"; + GUILayout.Label($"{e.Name} {status}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 13 }); + GUILayout.Label($"Lv: {e.Level} | FC: {e.FriendCode} | PUID: {e.Puid}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); + GUILayout.Label($"Joined: {e.FirstSeenUtc:HH:mm:ss} | Left: {(e.LeftUtc.HasValue ? e.LeftUtc.Value.ToString("HH:mm:ss") : "online")}", new GUIStyle(GUI.skin.label) { fontSize = 11 }); + GUILayout.Label($"Platform: {FormatPlatformHistory(e)}", new GUIStyle(GUI.skin.label) { fontSize = 11, wordWrap = true }); + GUILayout.Label($"RPC: {FormatRpcHistory(e)}", new GUIStyle(GUI.skin.label) { fontSize = 11, wordWrap = true }); + GUILayout.EndVertical(); + GUILayout.Space(2); + } + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + } + +private void ForceGlobalEject(PlayerControl target) + { + if (target == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ERROR] Нужны права Хоста!"); + return; + } + + try + { + target.Data.IsDead = false; + + if (MeetingHud.Instance == null) + { + MeetingHud.Instance = UnityEngine.Object.Instantiate(DestroyableSingleton.Instance.MeetingPrefab); + AmongUsClient.Instance.Spawn(MeetingHud.Instance.Cast(), -2, SpawnFlags.None); + } + + var emptyStates = new Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray(0); + + MeetingHud.Instance.RpcVotingComplete(emptyStates, target.Data, false); + + MeetingHud.Instance.RpcClose(); + + ShowNotification($"[EJECT] Изгоняем {target.Data.PlayerName}..."); + } + catch (Exception) + { + } + } + +private static bool IsDeadBodyForPlayerPresent(byte playerId) + { + try + { + var allBehaviours = UnityEngine.Object.FindObjectsOfType(); + foreach (var mb in allBehaviours) + { + if (mb == null || mb.gameObject == null) continue; + Type t = mb.GetType(); + if (t == null || t.Name != "DeadBody") continue; + + byte parentId = byte.MaxValue; + var parentProp = t.GetProperty("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentProp != null) + { + object val = parentProp.GetValue(mb, null); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + else + { + var parentField = t.GetField("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentField != null) + { + object val = parentField.GetValue(mb); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + } + + if (parentId == playerId) return true; + } + } + catch { } + + return false; + } + +private static void AttemptReportBody(PlayerControl target) + { + if (target == null || target.Data == null || PlayerControl.LocalPlayer == null) return; + + try + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + PlayerControl.LocalPlayer.CmdReportDeadBody(target.Data); + ShowNotification($"[REPORT] Репорт {target.Data.PlayerName}"); + return; + } + + if (LobbyBehaviour.Instance != null) + { + ShowNotification("[REPORT] Игра должна начаться."); + return; + } + + if (!target.Data.IsDead) + { + ShowNotification("[REPORT] Можно репортить только мертвых игроков."); + return; + } + + if (!IsDeadBodyForPlayerPresent(target.PlayerId)) + { + ShowNotification("[REPORT] Труп не найден или уже исчез."); + return; + } + + PlayerControl.LocalPlayer.CmdReportDeadBody(target.Data); + ShowNotification($"[REPORT] Репорт {target.Data.PlayerName}"); + } + catch (Exception) + { + } + } + +private static void FloodPlayerWithTasks(PlayerControl target) + { + if (target == null || target.Data == null) + { + ShowNotification("[TASKS] Цель не найдена."); + return; + } + + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[TASKS] Нужны права хоста."); + return; + } + + try + { + byte[] taskIds = new byte[255]; + for (byte i = 0; i < 255; i++) taskIds[i] = i; + target.Data.RpcSetTasks(taskIds); + ShowNotification($"[TASKS] {target.Data.PlayerName} получил flood tasks."); + } + catch (Exception) + { + } + } + +private static void ClearPlayerTasks(PlayerControl target) + { + if (target == null || target.Data == null) + { + ShowNotification("[TASKS] Цель не найдена."); + return; + } + + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[TASKS] Нужны права хоста."); + return; + } + + try + { + target.Data.RpcSetTasks(Array.Empty()); + ShowNotification($"[TASKS] Задачи {target.Data.PlayerName} очищены."); + } + catch (Exception) + { + } + } + +private static string GetRoleDisplayName(RoleTypes role) + { + for (int i = 0; i < roleAssignOptions.Length; i++) + if (roleAssignOptions[i].Equals(role)) + return roleAssignNames[i]; + return role.ToString(); + } + +private static bool IsGhostRoleSelection(int roleIndex) + { + return roleIndex >= 0 && + roleIndex < roleAssignNames.Length && + string.Equals(roleAssignNames[roleIndex], "Ghost", StringComparison.OrdinalIgnoreCase); + } + +private static bool IsGhostImpostorRoleSelection(int roleIndex) + { + return roleIndex >= 0 && + roleIndex < roleAssignNames.Length && + string.Equals(roleAssignNames[roleIndex], "Ghost Imp", StringComparison.OrdinalIgnoreCase); + } + +private static bool IsImpostorTeamRole(RoleTypes role) + { + int roleId = (int)role; + return role == RoleTypes.Impostor || role == RoleTypes.Shapeshifter || roleId == 9 || roleId == 18; + } + +private static bool IsLocalPhantomVanished() + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null || local.Data == null || local.Data.Role == null) return false; + return local.Data.Role is PhantomRole phantom && (phantom.fading || phantom.isInvisible || phantom.IsInvisible); + } + catch { return false; } + } + +private static bool IsMalumValidKillTarget(NetworkedPlayerInfo target) + { + try + { + if (target == null || target.Object == null || target.Role == null) return false; + if (target.Disconnected || target.PlayerId == PlayerControl.LocalPlayer.PlayerId) return false; + + bool baseRequirements = target.Object.Visible; + if (!baseRequirements) return false; + if (killAnyone) return true; + + return !target.IsDead && + !target.Object.inVent && + !target.Object.inMovingPlat && + target.Role.CanBeKilled; + } + catch { return false; } + } + +private static bool IsLocalImpostorRole(NetworkedPlayerInfo info = null) + { + try + { + NetworkedPlayerInfo playerInfo = info ?? PlayerControl.LocalPlayer?.Data; + if (playerInfo == null) return false; + return RoleManager.IsImpostorRole(playerInfo.RoleType) || + (playerInfo.Role != null && playerInfo.Role.IsImpostor); + } + catch { return false; } + } + +private static float GetConsoleUsableDistance(global::Console console) + { + if (console == null) return 1f; + + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + try + { + FieldInfo field = console.GetType().GetField("usableDistance", flags) ?? console.GetType().GetField("UsableDistance", flags); + if (field != null && field.GetValue(console) is float fieldValue) return fieldValue; + + PropertyInfo property = console.GetType().GetProperty("usableDistance", flags) ?? console.GetType().GetProperty("UsableDistance", flags); + if (property != null && property.GetValue(console, null) is float propertyValue) return propertyValue; + } + catch { } + + return 1f; + } + +private static bool LocalPlayerHasTaskForConsole(global::Console console) + { + try + { + if (console == null || PlayerControl.LocalPlayer?.myTasks == null) return false; + + foreach (var task in PlayerControl.LocalPlayer.myTasks) + { + if (task == null) continue; + try { if (task.IsComplete) continue; } catch { } + if (TaskAcceptsConsole(task, console)) return true; + } + } + catch { } + + return false; + } + +private static bool TaskAcceptsConsole(object task, global::Console console) + { + if (task == null || console == null) return false; + + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + try + { + MethodInfo validConsole = task.GetType().GetMethod("ValidConsole", flags, null, new[] { typeof(global::Console) }, null); + if (validConsole != null && validConsole.Invoke(task, new object[] { console }) is bool valid) + return valid; + } + catch { } + + return false; + } + +private static bool ShouldBlockUnsafeConsoleUse(global::Console console) + { + try + { + if (!allowTasksAsImpostor || console == null) return false; + if (!IsLocalImpostorRole()) return false; + return !LocalPlayerHasTaskForConsole(console); + } + catch { return false; } + } + +private static float GetVanillaKillDistance() + { + try + { + int killDistance = GameOptionsManager.Instance.CurrentGameOptions.GetInt(Int32OptionNames.KillDistance); + if (killDistance <= 0) return 1f; + if (killDistance == 1) return 1.8f; + return 2.5f; + } + catch { return 2.5f; } + } + +private static PlayerControl FindClosestKillTarget(ImpostorRole role, float maxDistance) + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null || local.Data == null || PlayerControl.AllPlayerControls == null) return null; + + Vector3 localWorld = local.transform.position; + Vector2 localPos = new Vector2(localWorld.x, localWorld.y); + PlayerControl result = null; + float bestDistance = maxDistance; + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == local || pc.Data == null) continue; + if (pc.Data.Disconnected || pc.Data.IsDead) continue; + if (pc.inVent || pc.onLadder) continue; + if (!killAnyone && IsImpostorTeamRole(pc.Data.RoleType)) continue; + if (!killAnyone && role != null && !role.IsValidTarget(pc.Data)) continue; + + Vector3 targetWorld = pc.transform.position; + float distance = Vector2.Distance(localPos, new Vector2(targetWorld.x, targetWorld.y)); + if (distance < bestDistance) + { + bestDistance = distance; + result = pc; + } + } + + return result; + } + catch { return null; } + } + +public static void RevivePlayer(PlayerControl target) + { + if (target == null || target.Data == null) + { + ShowNotification("[ОШИБКА] Цель не найдена!"); + return; + } + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ОШИБКА] Требуются права хоста!"); + return; + } + if (!target.Data.IsDead) + { + ShowNotification($"{target.Data.PlayerName} уже жив!"); + return; + } + + try + { + target.Data.IsDead = false; + + if (target.Collider != null) target.Collider.enabled = true; + + if (target.MyPhysics != null) + target.MyPhysics.gameObject.layer = LayerMask.NameToLayer("Players"); + + try + { + var allBehaviours = UnityEngine.Object.FindObjectsOfType(); + foreach (var mb in allBehaviours) + { + if (mb == null || mb.gameObject == null) continue; + Type t = mb.GetType(); + if (t == null || t.Name != "DeadBody") continue; + + byte parentId = byte.MaxValue; + + var parentProp = t.GetProperty("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentProp != null) + { + object val = parentProp.GetValue(mb, null); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + else + { + var parentField = t.GetField("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentField != null) + { + object val = parentField.GetValue(mb); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + } + + if (parentId == target.PlayerId) + mb.gameObject.SetActive(false); + } + } + catch { } + + bool wasImpTeam = false; + try + { + if (target.Data.Role != null) + { + int roleId = (int)target.Data.Role.Role; + wasImpTeam = roleId == 1 || roleId == 5 || roleId == 7 || roleId == 9 || roleId == 18; + } + else + { + var rt = target.Data.RoleType; + wasImpTeam = rt == RoleTypes.Impostor || rt == RoleTypes.Shapeshifter || (int)rt == 9 || (int)rt == 18; + } + } + catch { } + + target.RpcSetRole(wasImpTeam ? RoleTypes.Impostor : RoleTypes.Crewmate, true); + + var netObj = GameData.Instance?.GetComponent(); + if (netObj != null) netObj.SetDirtyBit(uint.MaxValue); + + ShowNotification($"[ВОСКРЕШЕНИЕ] {target.Data.PlayerName} воскрешён!"); + } + catch (Exception) + { + ShowNotification("Ошибка воскрешения!"); + } + } +} +} diff --git a/features/AutoHost.cs b/features/AutoHost.cs new file mode 100644 index 0000000..54cd69f --- /dev/null +++ b/features/AutoHost.cs @@ -0,0 +1,777 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { +public static class ElysiumAutoLobbyReturn + { + private const float AutoReturnDelaySeconds = 3f; + private const float AutoReturnRetrySeconds = 0.4f; + private const int AutoReturnMaxAttempts = 40; + + private static int trackedEndGameId; + private static int exhaustedEndGameId; + private static int attempt; + private static float nextAttemptAt; + private static bool pending; + + public static void UpdateLogic() + { + if (!ShouldAutoReturn()) + { + ResetState(); + return; + } + if (LobbyBehaviour.Instance != null) + { + ResetState(); + return; + } + + EndGameManager val = UnityEngine.Object.FindObjectOfType(); + if (val != null) + { + int instanceID = val.gameObject.GetInstanceID(); + if (trackedEndGameId != instanceID) + { + trackedEndGameId = instanceID; + exhaustedEndGameId = 0; + attempt = 0; + nextAttemptAt = Time.unscaledTime + AutoReturnDelaySeconds; + pending = true; + } + } + else if (trackedEndGameId == 0) return; + + if (!pending || exhaustedEndGameId == trackedEndGameId || Time.unscaledTime < nextAttemptAt) + return; + + bool flag = false; + if (val != null) + { + flag = TryInvokeEndGameAction(val); + flag = TryClickEndGameButtons(val) || flag; + } + flag = TryClickGlobalReturnButtons() || flag; + + if (LobbyBehaviour.Instance != null) + { + ResetState(); + return; + } + + attempt++; + if (attempt >= AutoReturnMaxAttempts) + pending = false; + else + nextAttemptAt = Time.unscaledTime + AutoReturnRetrySeconds; + } + + public static void ResetState() + { + trackedEndGameId = 0; + exhaustedEndGameId = 0; + attempt = 0; + nextAttemptAt = 0f; + pending = false; + } + + private static bool ShouldAutoReturn() + { + return ElysiumModMenuGUI.AutoReturnLobbyAfterMatch || ElysiumAutoHostService.ShouldReturnAfterMatch; + } + + private static bool TryInvokeEndGameAction(EndGameManager manager) + { + if (manager == null) return false; + string[] methods = new string[] { "Continue", "NextGame", "PlayAgain" }; + for (int i = 0; i < methods.Length; i++) + { + System.Reflection.MethodInfo methodInfo = FindMethodNoWarn(manager.GetType(), methods[i], Type.EmptyTypes); + if (methodInfo != null) + { + try { methodInfo.Invoke(manager, null); return true; } + catch { } + } + } + return false; + } + + private static System.Reflection.MethodInfo FindMethodNoWarn(Type type, string name, Type[] parameters) + { + if (type == null || string.IsNullOrWhiteSpace(name)) return null; + Type[] types = parameters ?? Type.EmptyTypes; + Type t = type; + while (t != null) + { + System.Reflection.MethodInfo method = t.GetMethod(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic, null, types, null); + if (method != null) return method; + t = t.BaseType; + } + return null; + } + + private static bool TryClickEndGameButtons(EndGameManager manager) + { + if (manager == null) return false; + if (TryClickPassiveButtons(manager.GetComponentsInChildren(true), true)) + return true; + return TryClickUnityButtons(manager.GetComponentsInChildren(true), true); + } + + private static bool TryClickGlobalReturnButtons() + { + if (TryClickPassiveButtons(UnityEngine.Object.FindObjectsOfType(), true)) + return true; + return TryClickUnityButtons(UnityEngine.Object.FindObjectsOfType(), true); + } + + private static bool TryClickPassiveButtons(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase buttons, bool onlyActive) + { + if (buttons == null) return false; + foreach (PassiveButton btn in buttons) + { + if (btn == null) continue; + if (onlyActive && (!btn.gameObject.activeInHierarchy || !btn.isActiveAndEnabled)) + continue; + if (!IsLobbyReturnButton(btn.name, btn.GetComponentsInChildren(true))) + continue; + try + { + if (btn.OnClick != null) + { + btn.OnClick.Invoke(); + return true; + } + } + catch { } + } + return false; + } + + private static bool TryClickUnityButtons(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase buttons, bool onlyActive) + { + if (buttons == null) return false; + foreach (UnityEngine.UI.Button btn in buttons) + { + if (btn == null) continue; + if (onlyActive && (!btn.gameObject.activeInHierarchy || !btn.isActiveAndEnabled || !btn.interactable)) + continue; + if (!IsLobbyReturnButton(btn.name, btn.GetComponentsInChildren(true))) + continue; + try + { + if (btn.onClick != null) + { + btn.onClick.Invoke(); + return true; + } + } + catch { } + } + return false; + } + + private static bool IsLobbyReturnButton(string objectName, Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase texts) + { + string input = (objectName ?? string.Empty).ToLowerInvariant(); + if (ContainsAny(input, "exit", "quit", "menu", "back", "leave", "вых", "выйт", "назад")) + return false; + if (ContainsAny(input, "continue", "nextgame", "playagain", "returntolobby", "tolobby", "lobby", "again", "продолж", "занов", "снов", "лобби", "играть", "вернут")) + return true; + if (texts == null) return false; + foreach (TMPro.TMP_Text txt in texts) + { + if (txt == null) continue; + string stripped = StripRichText(txt.text).ToLowerInvariant(); + if (ContainsAny(stripped, "exit", "quit", "menu", "back", "leave", "вых", "выйт", "назад")) + return false; + if (ContainsAny(stripped, "continue", "next game", "play again", "return to lobby", "lobby", "again", "продолж", "занов", "снов", "лобби", "играть", "вернут")) + return true; + } + return false; + } + + private static bool ContainsAny(string input, params string[] tokens) + { + if (string.IsNullOrEmpty(input)) return false; + foreach (string token in tokens) + if (!string.IsNullOrWhiteSpace(token) && input.Contains(token)) + return true; + return false; + } + + private static string StripRichText(string input) + { + if (string.IsNullOrEmpty(input)) return string.Empty; + char[] chars = new char[input.Length]; + int length = 0; + bool inTag = false; + foreach (char c in input) + { + switch (c) + { + case '<': inTag = true; continue; + case '>': inTag = false; continue; + } + if (!inTag) chars[length++] = c; + } + return new string(chars, 0, length); + } + } + +public static class ElysiumAutoHostService + { + public sealed class AutoHostStatusSnapshot + { + public bool Enabled; + public bool IsHost; + public bool IsLobby; + public bool IsInGame; + public string State = string.Empty; + public string LastReason = string.Empty; + public int ConnectedPlayers; + public int ReadyPlayers; + public int RequiredPlayers; + public float CountdownRemainingSeconds; + public float BackoffRemainingSeconds; + public float LobbyAgeSeconds; + public float LobbyLifeRemainingSeconds = -1f; + public bool WaitingForLoadedPlayers; + public bool AutoReturnAfterMatch; + public bool ForceLastMinute; + public string StartMode = string.Empty; + public float EffectiveStartDelaySeconds; + public float WarmupRemainingSeconds; + public float LoadGraceRemainingSeconds; + public bool FastStartActive; + public bool ForceStartActive; + } + + private enum AutoHostState + { + Disabled, Idle, Warmup, WaitingPlayers, WaitingLoad, + Countdown, Starting, InGame, Returning, Backoff, + } + + private const float TickIntervalSeconds = 0.2f; + private const float StartRequestGraceSeconds = 7f; + private const float LobbyLifetimeSeconds = 600f; + private const float LastMinuteStartSeconds = 60f; + private const float NotificationCooldownSeconds = 0.75f; + + private static AutoHostState state = AutoHostState.Disabled; + private static string lastReason = "disabled"; + private static float nextTickAt; + private static float countdownStartedAt = -1f; + private static float activeCountdownDelay = -1f; + private static float backoffUntil = -1f; + private static float lastStartIssuedAt = -1f; + private static float lobbyOpenedAt = -1f; + private static float loadWaitStartedAt = -1f; + private static float lastNotificationAt = -1f; + private static int lobbyGameId = -1; + private static int lastCountdownNotice = -1; + + public static void Tick() + { + float now = Time.unscaledTime; + if (now < nextTickAt) return; + nextTickAt = now + TickIntervalSeconds; + + if (!IsEnabled) + { + ResetLobbyFlow(true); + SetState(AutoHostState.Disabled, "Выключен"); + return; + } + + InnerNetClient client = TryGetClient(); + if (client == null) + { + ResetLobbyFlow(false); + SetState(AutoHostState.Idle, "Клиент недоступен"); + return; + } + + if (!client.AmHost) + { + ResetLobbyFlow(false); + SetState(AutoHostState.Idle, "Ожидаю хост-контекст"); + return; + } + + if (IsEndGameScreen()) + { + ResetLobbyFlow(false); + SetState(ShouldReturnAfterMatch ? AutoHostState.Returning : AutoHostState.InGame, + ShouldReturnAfterMatch ? "Возврат в лобби" : "Матч завершен"); + return; + } + + if (IsInMatch()) + { + ResetLobbyFlow(true); + SetState(AutoHostState.InGame, "Матч идет"); + return; + } + + if (LobbyBehaviour.Instance == null) + { + ResetLobbyFlow(false); + lobbyOpenedAt = -1f; + lobbyGameId = -1; + SetState(AutoHostState.Idle, "Вне лобби"); + return; + } + + TrackLobby(client, now); + TickHostedLobby(client, now); + } + + public static AutoHostStatusSnapshot GetStatusSnapshot() + { + AutoHostStatusSnapshot snapshot = new AutoHostStatusSnapshot + { + Enabled = IsEnabled, + State = FormatState(state), + LastReason = lastReason ?? string.Empty, + RequiredPlayers = RequiredPlayers, + CountdownRemainingSeconds = CountdownRemaining, + BackoffRemainingSeconds = BackoffRemaining, + LobbyAgeSeconds = lobbyOpenedAt > 0f ? Mathf.Max(0f, Time.unscaledTime - lobbyOpenedAt) : 0f, + LobbyLifeRemainingSeconds = LobbyLifeRemaining, + AutoReturnAfterMatch = ShouldReturnAfterMatch, + ForceLastMinute = ForceLastMinuteEnabled, + StartMode = ElysiumModMenuGUI.AutoHostInstantStart ? "Мгновенный" : "Обычный", + EffectiveStartDelaySeconds = EffectiveStartDelay(0), + WarmupRemainingSeconds = WarmupRemaining, + LoadGraceRemainingSeconds = LoadGraceRemaining, + }; + InnerNetClient client = TryGetClient(); + if (client != null) + { + snapshot.IsHost = client.AmHost; + snapshot.IsLobby = LobbyBehaviour.Instance != null; + snapshot.IsInGame = IsInMatch(); + snapshot.ConnectedPlayers = CountLobbyPlayers(client, out int readyPlayers, out _); + snapshot.ReadyPlayers = readyPlayers; + snapshot.WaitingForLoadedPlayers = snapshot.ConnectedPlayers > snapshot.ReadyPlayers; + snapshot.FastStartActive = IsFastStartActive(snapshot.ConnectedPlayers); + snapshot.ForceStartActive = ShouldForceStart(snapshot.ConnectedPlayers, out _); + snapshot.EffectiveStartDelaySeconds = EffectiveStartDelay(snapshot.ConnectedPlayers); + } + return snapshot; + } + + public static void ResetTransientState() + { + nextTickAt = 0f; + ResetLobbyFlow(true); + SetState(IsEnabled ? AutoHostState.Idle : AutoHostState.Disabled, IsEnabled ? "Сброшен" : "Выключен"); + } + + public static string TryStartNow() + { + if (!IsEnabled) return "Автохост выключен."; + InnerNetClient client = TryGetClient(); + if (client == null || !client.AmHost) return "Ручной старт доступен только хосту."; + if (LobbyBehaviour.Instance == null) return "Ручной старт доступен только в лобби."; + GameStartManager manager = TryGetGameStartManager(); + if (manager == null) return "Кнопка старта не найдена."; + + if (!TryConfiguredStart(manager)) + { + EnterBackoff("Ручной старт отклонен"); + return "Старт не сработал."; + } + lastStartIssuedAt = Time.unscaledTime; + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + backoffUntil = -1f; + SetState(AutoHostState.Starting, "Ручной старт"); + Notify("Автохост", "Матч запускается вручную."); + return "Старт отправлен."; + } + + private static void TickHostedLobby(InnerNetClient client, float now) + { + int connectedPlayers = CountLobbyPlayers(client, out int readyPlayers, out string loadingName); + bool forceStart = ShouldForceStart(connectedPlayers, out string forceReason); + float warmupRemaining = WarmupRemaining; + + if (!forceStart && warmupRemaining > 0.05f) + { + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + lastStartIssuedAt = -1f; + lastCountdownNotice = -1; + SetState(AutoHostState.Warmup, $"Прогрев лобби {Mathf.CeilToInt(warmupRemaining)}с"); + return; + } + + bool waitingForLoad = ElysiumModMenuGUI.AutoHostWaitLoadedPlayers && connectedPlayers > readyPlayers; + if (waitingForLoad && !forceStart && !CanBypassLoadWait(now, readyPlayers, connectedPlayers, loadingName)) + { + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + lastStartIssuedAt = -1f; + lastCountdownNotice = -1; + SetState(AutoHostState.WaitingLoad, $"Ожидаю прогрузку {readyPlayers}/{connectedPlayers}: {loadingName}"); + return; + } + if (!waitingForLoad) loadWaitStartedAt = -1f; + + if (lastStartIssuedAt > 0f) + { + if (now - lastStartIssuedAt < StartRequestGraceSeconds) + { + SetState(AutoHostState.Starting, "Старт отправлен"); + return; + } + lastStartIssuedAt = -1f; + EnterBackoff("Старт не подтвердился"); + return; + } + + if (backoffUntil > now) + { + SetState(AutoHostState.Backoff, "Пауза после попытки"); + return; + } + + int requiredPlayers = RequiredPlayers; + bool enoughPlayers = ElysiumModMenuGUI.AutoHostWaitLoadedPlayers ? readyPlayers >= requiredPlayers : connectedPlayers >= requiredPlayers; + bool continueBelowMin = !ElysiumModMenuGUI.AutoHostCancelBelowMin && countdownStartedAt >= 0f && connectedPlayers >= 2; + + if (!forceStart && !enoughPlayers && !continueBelowMin) + { + if (countdownStartedAt >= 0f) + Notify("Автохост", "Отсчет отменен: игроков стало меньше минимума."); + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + lastCountdownNotice = -1; + SetState(AutoHostState.WaitingPlayers, $"Игроки {connectedPlayers}/{requiredPlayers}"); + return; + } + + float delay = EffectiveStartDelay(connectedPlayers); + if (!forceStart && countdownStartedAt < 0f) + { + countdownStartedAt = now; + activeCountdownDelay = delay; + lastCountdownNotice = -1; + SetState(AutoHostState.Countdown, IsFastStartActive(connectedPlayers) ? "Быстрый старт" : "Минимум игроков набран"); + Notify("Автохост", $"Старт через {Mathf.CeilToInt(delay)} с."); + } + + if (!forceStart && now - countdownStartedAt < delay) + { + AnnounceCountdown(delay - (now - countdownStartedAt)); + SetState(AutoHostState.Countdown, "Отсчет"); + return; + } + + GameStartManager manager = TryGetGameStartManager(); + if (manager == null) + { + EnterBackoff("Кнопка старта не найдена"); + return; + } + if (!TryConfiguredStart(manager)) + { + EnterBackoff(forceStart ? "Форс-старт отклонен" : "Старт отклонен"); + return; + } + + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + backoffUntil = -1f; + lastStartIssuedAt = now; + lastCountdownNotice = -1; + SetState(AutoHostState.Starting, forceStart ? forceReason : "Старт матча"); + Notify("Автохост", forceStart ? forceReason : "Минимум набран, запускаю матч."); + } + + private static void TrackLobby(InnerNetClient client, float now) + { + int gameId; + try { gameId = client.GameId; } catch { gameId = 0; } + if (lobbyOpenedAt >= 0f && lobbyGameId == gameId) return; + lobbyOpenedAt = now; + lobbyGameId = gameId; + ResetLobbyFlow(true); + SetState(AutoHostState.WaitingPlayers, "Новое лобби"); + } + + private static void AnnounceCountdown(float remaining) + { + int whole = Mathf.CeilToInt(Mathf.Max(0f, remaining)); + if (whole == lastCountdownNotice) return; + if (whole == 60 || whole == 30 || whole == 15 || whole == 10 || whole == 5 || whole == 3 || whole == 2 || whole == 1) + { + lastCountdownNotice = whole; + Notify("Автохост", $"Старт через {whole} с."); + } + } + + private static bool TryConfiguredStart(GameStartManager manager) + { + if (manager == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || LobbyBehaviour.Instance == null) + return false; + try + { + manager.MinPlayers = 1; + if (ElysiumModMenuGUI.AutoHostInstantStart) + { + manager.startState = GameStartManager.StartingStates.Countdown; + manager.countDownTimer = 0f; + return true; + } + manager.BeginGame(); + return true; + } + catch { return false; } + } + + private static void EnterBackoff(string reason) + { + countdownStartedAt = -1f; + activeCountdownDelay = -1f; + lastStartIssuedAt = -1f; + loadWaitStartedAt = -1f; + lastCountdownNotice = -1; + backoffUntil = Time.unscaledTime + BackoffSeconds; + SetState(AutoHostState.Backoff, reason); + Notify("Автохост: пауза", reason); + } + + private static void ResetLobbyFlow(bool clearBackoff) + { + countdownStartedAt = -1f; + lastStartIssuedAt = -1f; + lastCountdownNotice = -1; + if (clearBackoff) backoffUntil = -1f; + } + + private static void SetState(AutoHostState nextState, string reason) + { + if (!string.IsNullOrWhiteSpace(reason)) lastReason = reason.Trim(); + state = nextState; + } + + private static int CountLobbyPlayers(InnerNetClient client, out int readyPlayers, out string loadingName) + { + readyPlayers = 0; + loadingName = "игрок"; + if (client == null || client.allClients == null) return 0; + + int connected = 0; + try + { + var cursor = client.allClients.GetEnumerator(); + while (cursor.MoveNext()) + { + ClientData data = cursor.Current; + if (data == null || data.Id < 0) continue; + if (IsDisconnected(data)) continue; + connected++; + if (IsReady(data)) readyPlayers++; + else loadingName = CleanName(data.PlayerName); + } + } + catch { return CountReadyPlayerControls(out readyPlayers); } + return connected; + } + + private static int CountReadyPlayerControls(out int readyPlayers) + { + readyPlayers = 0; + try + { + if (PlayerControl.AllPlayerControls == null) return 0; + int count = 0; + var cursor = PlayerControl.AllPlayerControls.GetEnumerator(); + while (cursor.MoveNext()) + { + PlayerControl player = cursor.Current; + if (player == null || player.Data == null || player.Data.Disconnected || player.PlayerId >= 100) continue; + count++; + readyPlayers++; + } + return count; + } + catch { return 0; } + } + + private static bool IsReady(ClientData data) + { + try + { + PlayerControl character = data.Character; + return character != null && character.Data != null && !character.Data.Disconnected && character.PlayerId < 100; + } + catch { return false; } + } + + private static bool IsDisconnected(ClientData data) + { + try { return data.Character != null && data.Character.Data != null && data.Character.Data.Disconnected; } + catch { return false; } + } + + private static GameStartManager TryGetGameStartManager() + { + try { if (DestroyableSingleton.InstanceExists) return DestroyableSingleton.Instance; } catch { } + try { return UnityEngine.Object.FindObjectOfType(); } catch { return null; } + } + + private static InnerNetClient TryGetClient() + { + try { return AmongUsClient.Instance == null ? null : (InnerNetClient)AmongUsClient.Instance; } catch { return null; } + } + + private static bool CanBypassLoadWait(float now, int readyPlayers, int connectedPlayers, string loadingName) + { + if (readyPlayers < RequiredPlayers) { loadWaitStartedAt = -1f; return false; } + int grace = Mathf.Clamp((int)ElysiumModMenuGUI.AutoHostLoadGraceSeconds, 0, 90); + if (grace <= 0) { loadWaitStartedAt = -1f; return false; } + if (loadWaitStartedAt < 0f) loadWaitStartedAt = now; + if (now - loadWaitStartedAt < grace) + { + SetState(AutoHostState.WaitingLoad, $"Жду прогрузку {readyPlayers}/{connectedPlayers}: {loadingName}"); + return false; + } + SetState(AutoHostState.Countdown, "Прогрузка задержалась, старт по готовым"); + return true; + } + + private static bool ShouldForceStart(int connectedPlayers, out string reason) + { + int minPlayers = ForceMinPlayers; + if (ForceLastMinuteEnabled && connectedPlayers >= minPlayers && LobbyLifeRemaining >= 0f && LobbyLifeRemaining <= LastMinuteStartSeconds) + { + reason = "Форс-старт: лобби скоро закроется"; + return true; + } + int forceAfterMinutes = Mathf.Clamp(ElysiumModMenuGUI.AutoHostForceAfterMinutes, 0, 10); + if (forceAfterMinutes > 0 && connectedPlayers >= minPlayers && lobbyOpenedAt > 0f && Time.unscaledTime - lobbyOpenedAt >= forceAfterMinutes * 60f) + { + reason = $"Форс-старт: ожидание {forceAfterMinutes} мин"; + return true; + } + reason = string.Empty; + return false; + } + + private static bool IsFastStartActive(int connectedPlayers) + { + int threshold = Mathf.Clamp(ElysiumModMenuGUI.AutoHostFastStartPlayers, 0, 15); + return threshold > 0 && connectedPlayers >= threshold; + } + + private static float EffectiveStartDelay(int connectedPlayers) + { + float delay = StartDelaySeconds; + if (IsFastStartActive(connectedPlayers)) + delay = Mathf.Min(delay, Mathf.Clamp(ElysiumModMenuGUI.AutoHostFastStartDelaySeconds, 0, 60)); + return delay; + } + + private static bool IsInMatch() => ShipStatus.Instance != null && LobbyBehaviour.Instance == null && !IsEndGameScreen(); + + private static bool IsEndGameScreen() + { + try { return UnityEngine.Object.FindObjectOfType() != null; } catch { return false; } + } + + private static void Notify(string title, string detail) + { + if (!ElysiumModMenuGUI.AutoHostNotifications) return; + float now = Time.unscaledTime; + if (lastNotificationAt > 0f && now - lastNotificationAt < NotificationCooldownSeconds) return; + lastNotificationAt = now; + ElysiumModMenuGUI.ShowNotification($"[{title}] {detail}"); + } + + private static string FormatState(AutoHostState value) + { + return value switch + { + AutoHostState.Disabled => L("Disabled", "Выключен"), + AutoHostState.Idle => L("Idle", "Ожидание"), + AutoHostState.Warmup => L("Warmup", "Прогрев"), + AutoHostState.WaitingPlayers => L("Waiting for players", "Ждет игроков"), + AutoHostState.WaitingLoad => L("Waiting for load", "Ждет прогрузку"), + AutoHostState.Countdown => L("Countdown", "Отсчет"), + AutoHostState.Starting => L("Starting", "Запуск"), + AutoHostState.InGame => L("In Game", "В игре"), + AutoHostState.Returning => L("Returning", "Возврат"), + AutoHostState.Backoff => L("Backoff", "Пауза"), + _ => value.ToString(), + }; + } + + private static string CleanName(string value) + { + if (string.IsNullOrWhiteSpace(value)) return "игрок"; + string clean = value.Replace("\r", " ").Replace("\n", " ").Trim(); + return clean.Length <= 18 ? clean : clean.Substring(0, 17) + "..."; + } + + public static bool IsEnabled => ElysiumModMenuGUI.AutoHostEnabled; + public static bool ShouldReturnAfterMatch => IsEnabled && ElysiumModMenuGUI.AutoReturnLobbyAfterMatch; + private static bool ForceLastMinuteEnabled => ElysiumModMenuGUI.AutoHostForceLastMinute; + private static int RequiredPlayers => Mathf.Clamp(ElysiumModMenuGUI.AutoHostMinPlayers, 1, 15); + private static int ForceMinPlayers => Mathf.Clamp(ElysiumModMenuGUI.AutoHostForceMinPlayers, 1, 15); + private static float StartDelaySeconds => Mathf.Clamp(ElysiumModMenuGUI.AutoHostStartDelaySeconds, 0f, 180f); + private static float BackoffSeconds => Mathf.Clamp(ElysiumModMenuGUI.AutoHostBackoffSeconds, 2f, 60f); + private static float CountdownRemaining => countdownStartedAt < 0f ? 0f : Mathf.Clamp((activeCountdownDelay >= 0f ? activeCountdownDelay : StartDelaySeconds) - (Time.unscaledTime - countdownStartedAt), 0f, StartDelaySeconds); + private static float BackoffRemaining => backoffUntil < 0f ? 0f : Mathf.Clamp(backoffUntil - Time.unscaledTime, 0f, BackoffSeconds); + private static float LobbyLifeRemaining => lobbyOpenedAt < 0f ? -1f : Mathf.Clamp(LobbyLifetimeSeconds - (Time.unscaledTime - lobbyOpenedAt), 0f, LobbyLifetimeSeconds); + private static float WarmupRemaining => lobbyOpenedAt < 0f ? 0f : Mathf.Clamp(ElysiumModMenuGUI.AutoHostWarmupSeconds - (Time.unscaledTime - lobbyOpenedAt), 0f, 120f); + private static float LoadGraceRemaining => loadWaitStartedAt < 0f || ElysiumModMenuGUI.AutoHostLoadGraceSeconds <= 0 ? 0f : Mathf.Clamp(ElysiumModMenuGUI.AutoHostLoadGraceSeconds - (Time.unscaledTime - loadWaitStartedAt), 0f, 90f); + } + +private int currentVisualsSubTab = 0; + } +} diff --git a/features/ChatOptions.cs b/features/ChatOptions.cs new file mode 100644 index 0000000..60427c4 --- /dev/null +++ b/features/ChatOptions.cs @@ -0,0 +1,504 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleAnimation))] + public static class PlayerPhysics_HandleAnimation + { + public static bool Prefix(PlayerPhysics __instance) + { + if (ElysiumModMenuGUI.moonWalk && __instance.AmOwner) + { + __instance.ResetAnimState(); + return false; + } + return true; + } + } + +[HarmonyPatch(typeof(FreeChatInputField), nameof(FreeChatInputField.UpdateCharCount))] + public static class FreeChatInputField_UpdateCharCount_Patch + { + public static void Postfix(FreeChatInputField __instance) + { + if (__instance == null || __instance.textArea == null || __instance.charCountText == null) return; + + __instance.textArea.characterLimit = 120; + + int length = __instance.textArea.text.Length; + + __instance.charCountText.SetText($"{length}/{__instance.textArea.characterLimit}"); + + if (length < 90) + { + __instance.charCountText.color = Color.white; + } + else if (length < 115) + { + __instance.charCountText.color = Color.yellow; + } + else + { + __instance.charCountText.color = Color.red; + } + } + } + +public static class ChatHistory + { + public static List sentMessages = new List(); + public static int HistoryIndex = -1; + public static string DraftBeforeHistory = ""; + public static bool BrowsingHistory = false; + + public static void Remember(string message) + { + if (string.IsNullOrWhiteSpace(message)) return; + bool isNewEntry = sentMessages.Count == 0 || sentMessages[sentMessages.Count - 1] != message; + if (isNewEntry) + { + sentMessages.Add(message); + while (sentMessages.Count > ElysiumModMenuGUI.chatHistoryLimit) + sentMessages.RemoveAt(0); + } + HistoryIndex = sentMessages.Count; + } + + public static void HandleNavigation(ChatController chat) + { + if (sentMessages.Count == 0 || chat.freeChatField == null || chat.freeChatField.textArea == null || !chat.freeChatField.textArea.hasFocus) + return; + + if (Input.GetKeyDown(KeyCode.UpArrow)) + { + if (!BrowsingHistory) + { + DraftBeforeHistory = chat.freeChatField.textArea.text; + BrowsingHistory = true; + } + if (HistoryIndex <= 0) return; + + HistoryIndex = Mathf.Clamp(HistoryIndex - 1, 0, sentMessages.Count - 1); + chat.freeChatField.textArea.SetText(sentMessages[HistoryIndex], string.Empty); + } + else if (Input.GetKeyDown(KeyCode.DownArrow)) + { + if (!BrowsingHistory) return; + + HistoryIndex += 1; + if (HistoryIndex < sentMessages.Count) + { + chat.freeChatField.textArea.SetText(sentMessages[HistoryIndex], string.Empty); + } + else + { + chat.freeChatField.textArea.SetText(DraftBeforeHistory, string.Empty); + BrowsingHistory = false; + } + } + } + } + +public static class ClipboardBridge + { + private static bool isPastingChatInput = false; + private static int currentPasteCharPos = 0; + private static int lastClipboardFrame = -1; + + public static void Run(TextBoxTMP box) + { + if (!enableClipboard) return; + if (box == null || !box.hasFocus) return; + + bool controlHeld = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); + bool shiftHeld = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); + + bool copyPressed = controlHeld && (Input.GetKeyDown(KeyCode.C) || Input.GetKeyDown(KeyCode.Insert)); + bool pastePressed = (controlHeld && Input.GetKeyDown(KeyCode.V)) || (shiftHeld && Input.GetKeyDown(KeyCode.Insert)); + bool cutPressed = controlHeld && Input.GetKeyDown(KeyCode.X); + + if (!copyPressed && !pastePressed && !cutPressed) return; + if (lastClipboardFrame == Time.frameCount) return; + lastClipboardFrame = Time.frameCount; + + if (copyPressed) + { + GUIUtility.systemCopyBuffer = box.text ?? string.Empty; + } + else if (pastePressed) + { + string paste = GUIUtility.systemCopyBuffer; + if (!string.IsNullOrEmpty(paste)) + { + string currentText = box.text ?? string.Empty; + int caretPos = Mathf.Clamp(box.caretPos, 0, currentText.Length); + string nextText = currentText.Insert(caretPos, paste); + + isPastingChatInput = true; + box.SetText(nextText, string.Empty); + isPastingChatInput = false; + } + } + else if (cutPressed) + { + GUIUtility.systemCopyBuffer = box.text ?? string.Empty; + box.SetText(string.Empty, string.Empty); + } + } + + public static bool IsCharAllowed(TextBoxTMP box, ref bool result) + { + if (box == null) return true; + + string compositionString = Input.compositionString; + if (!string.IsNullOrEmpty(compositionString)) + { + result = true; + return false; + } + + string input = isPastingChatInput ? GUIUtility.systemCopyBuffer : Input.inputString; + if (string.IsNullOrEmpty(input)) return true; + + string currentText = box.text ?? string.Empty; + int caretPos = Mathf.Clamp(box.caretPos, 0, currentText.Length); + string text = currentText.Insert(caretPos, input); + + currentPasteCharPos = Mathf.Clamp(currentPasteCharPos, 0, Mathf.Max(0, text.Length - 1)); + char currentChar = text[currentPasteCharPos]; + currentPasteCharPos = currentPasteCharPos >= text.Length - 1 ? 0 : currentPasteCharPos + 1; + + if (allowLinksAndSymbols) + { + HashSet blockedSymbols = new HashSet { '\b', '\r', '\n', '>', '<', '[' }; + result = !blockedSymbols.Contains(currentChar); + return false; + } + + return true; + } + } + +public static class ChatBubbleCopyInteraction + { + private const float DoubleClickSeconds = 0.35f; + private static int lastMessageBubbleId = -1; + private static float lastMessageClickAt = -10f; + + public static void HandleClick(ChatController chat) + { + if ((!enableChatMessageDoubleClickCopy && !enableChatNameColorCopy) || chat == null) return; + if (!chat.IsOpenOrOpening || !Input.GetMouseButtonDown(0)) return; + + Camera camera = Camera.main; + if (camera == null) return; + + Vector3 mouse = Input.mousePosition; + mouse.z = Mathf.Abs(camera.transform.position.z); + Vector3 worldPoint = camera.ScreenToWorldPoint(mouse); + + if (enableChatNameColorCopy && TryFindNameBubble(worldPoint, out ChatBubble nameBubble)) + { + CopyPlayerName(nameBubble); + ResetMessageClick(); + return; + } + + if (!enableChatMessageDoubleClickCopy || !TryFindMessageBubble(worldPoint, out ChatBubble messageBubble)) + { + return; + } + + int bubbleId = messageBubble.GetInstanceID(); + float now = Time.unscaledTime; + if (lastMessageBubbleId == bubbleId && now - lastMessageClickAt <= DoubleClickSeconds) + { + CopyMessage(messageBubble); + ResetMessageClick(); + return; + } + + lastMessageBubbleId = bubbleId; + lastMessageClickAt = now; + } + + private static bool TryFindNameBubble(Vector3 worldPoint, out ChatBubble bubble) + { + bubble = null; + ChatBubble[] bubbles = UnityEngine.Object.FindObjectsOfType(); + if (bubbles == null) return false; + + for (int i = bubbles.Length - 1; i >= 0; i--) + { + ChatBubble candidate = bubbles[i]; + if (candidate == null || candidate.NameText == null) continue; + if (IsPointOverText(candidate.NameText, worldPoint)) + { + bubble = candidate; + return true; + } + } + + return false; + } + + private static bool TryFindMessageBubble(Vector3 worldPoint, out ChatBubble bubble) + { + bubble = null; + ChatBubble[] bubbles = UnityEngine.Object.FindObjectsOfType(); + if (bubbles == null) return false; + + for (int i = bubbles.Length - 1; i >= 0; i--) + { + ChatBubble candidate = bubbles[i]; + if (candidate == null || candidate.TextArea == null) continue; + if (IsPointOverText(candidate.TextArea, worldPoint)) + { + bubble = candidate; + return true; + } + } + + return false; + } + + private static bool IsPointOverText(TMP_Text text, Vector3 worldPoint) + { + if (text == null || !text.gameObject.activeInHierarchy) return false; + + try + { + text.ForceMeshUpdate(); + Bounds bounds = text.textBounds; + if (bounds.size.sqrMagnitude > 0.0001f) + { + bounds.Expand(new Vector3(0.2f, 0.14f, 0.1f)); + Vector3 localPoint = text.transform.InverseTransformPoint(worldPoint); + localPoint.z = bounds.center.z; + if (bounds.Contains(localPoint)) return true; + } + } + catch { } + + try + { + Renderer renderer = text.GetComponent(); + if (renderer != null) + { + Bounds bounds = renderer.bounds; + bounds.Expand(new Vector3(0.2f, 0.14f, 0.1f)); + Vector3 adjustedPoint = worldPoint; + adjustedPoint.z = bounds.center.z; + return bounds.Contains(adjustedPoint); + } + } + catch { } + + return false; + } + + private static void CopyMessage(ChatBubble bubble) + { + string message = StripRichText(bubble != null && bubble.TextArea != null ? bubble.TextArea.text : string.Empty); + if (string.IsNullOrWhiteSpace(message)) return; + + GUIUtility.systemCopyBuffer = message; + ElysiumModMenuGUI.ShowNotification("[CHAT] Message copied."); + } + + private static void CopyPlayerName(ChatBubble bubble) + { + string playerName = GetBubblePlayerName(bubble); + if (string.IsNullOrWhiteSpace(playerName)) return; + + GUIUtility.systemCopyBuffer = playerName; + ElysiumModMenuGUI.ShowNotification("[CHAT] Name copied."); + } + + private static string GetBubblePlayerName(ChatBubble bubble) + { + try + { + if (bubble != null && bubble.playerInfo != null && !string.IsNullOrWhiteSpace(bubble.playerInfo.PlayerName)) + { + return StripRichText(bubble.playerInfo.PlayerName).Trim(); + } + } + catch { } + + return StripRichText(bubble != null && bubble.NameText != null ? bubble.NameText.text : string.Empty).Trim(); + } + + private static string StripRichText(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return Regex.Replace(value, "<[^>]*>", string.Empty); + } + + private static void ResetMessageClick() + { + lastMessageBubbleId = -1; + lastMessageClickAt = -10f; + } + } + +[HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.Update))] + public static class AllowSymbols_TextBoxTMP_Update_Patch + { + public static void Postfix(TextBoxTMP __instance) + { + if (__instance == null) return; + __instance.allowAllCharacters = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.AllowSymbols = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.AllowEmail = ElysiumModMenuGUI.allowLinksAndSymbols; + } + } + +[HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.Update))] + public static class Clipboard_TextBoxTMP_Patch + { + public static void Postfix(TextBoxTMP __instance) + { + ClipboardBridge.Run(__instance); + } + } + +[HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.IsCharAllowed))] + public static class Clipboard_TextBoxTMP_IsCharAllowed_Patch + { + public static bool Prefix(TextBoxTMP __instance, ref bool __result) + { + return ClipboardBridge.IsCharAllowed(__instance, ref __result); + } + } + +[HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] + public static class ChatHistory_Update_Patch + { + public static void Postfix(ChatController __instance) + { + if (__instance != null && __instance.freeChatField != null && __instance.freeChatField.textArea != null) + { + ClipboardBridge.Run(__instance.freeChatField.textArea); + } + ChatHistory.HandleNavigation(__instance); + ChatBubbleCopyInteraction.HandleClick(__instance); + } + } + +public static bool enableExtendedChat = true; + +public static bool enableChatHistory = true; + +public static bool enableClipboard = true; + +public static bool enableChatMessageDoubleClickCopy = true; + +public static bool enableChatNameColorCopy = true; + +public static bool AnimEmptyGarbageEnabled = false; + +public static bool skipShhhAnim = false; + +public static bool isManualMapSpawn = false; + +private void DrawAnimationsTab() + { + GUILayout.BeginVertical(menuCardStyle); + + DrawMenuSectionHeader(L("LOOPED PLAYER ANIMATIONS", "ЗАЦИКЛЕННЫЕ АНИМАЦИИ ИГРОКА")); + + string animInfo = L("Animations are looped. They will run as long as the toggle is ON.", + "Анимации зациклены. Будут работать, пока включен тумблер."); + GUILayout.Label(animInfo, new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); + + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + AnimAsteroidsEnabled = DrawToggle(AnimAsteroidsEnabled, L("Weapons (Asteroids)", "Оружие (Астероиды)"), 250); + IsScanning = DrawToggle(IsScanning, L("Medbay Scan", "Скан в медпункте"), 250); + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + AnimShieldsEnabled = DrawToggle(AnimShieldsEnabled, L("Turn On Shields", "Включить щиты"), 250); + AnimCamsInUseEnabled = DrawToggle(AnimCamsInUseEnabled, L("Use Cameras (Blink Red)", "Камеры (Красный индикатор)"), 250); + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + AnimEmptyGarbageEnabled = DrawToggle(AnimEmptyGarbageEnabled, L("Empty Garbage", "Выкинуть мусор"), 250); + skipShhhAnim = DrawToggle(skipShhhAnim, L("Skip 'Shhh!' Intro", "Пропустить 'Shhh!' интро"), 250); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + + } + +public static string GetPlatform(ClientData client) + { + if (client == null || client.PlatformData == null) return "Unknown"; + + int platformId = (int)client.PlatformData.Platform; + + switch (platformId) + { + case 1: return "Epic"; + case 2: return "Steam"; + case 3: return "Mac"; + case 4: return "Microsoft"; + case 5: return "Itch"; + case 6: return "iOS"; + case 7: return "Android"; + case 8: return "Switch"; + case 9: return "Xbox"; + case 10: return "PlayStation"; + case 112: return "Starlight"; + default: return $"Unknown ({platformId})"; + } + } +} +} diff --git a/features/EspFormatting.cs b/features/EspFormatting.cs new file mode 100644 index 0000000..4892def --- /dev/null +++ b/features/EspFormatting.cs @@ -0,0 +1,756 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +private static string CompactEspValue(string value, int maxLength = 24) + { + value = Regex.Replace(value ?? string.Empty, "<.*?>", string.Empty) + .Replace('\r', ' ') + .Replace('\n', ' ') + .Trim(); + + if (string.IsNullOrEmpty(value)) return "Hidden"; + if (value.Length > maxLength) value = value.Substring(0, maxLength - 3) + "..."; + return value; + } + +private static string NormalizeEspToken(string value) + { + return Regex.Replace(value ?? string.Empty, "<.*?>", string.Empty) + .Replace('\r', ' ') + .Replace('\n', ' ') + .Trim(); + } + +private static string FriendEspIgnoreFilePath() + { + string folder = string.IsNullOrWhiteSpace(Plugin.ElysiumFolder) + ? System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu") + : Plugin.ElysiumFolder; + return System.IO.Path.Combine(folder, "ElysiumFriendEspIgnore.txt"); + } + +private static void LoadFriendEspIgnoreTokensIfNeeded() + { + try + { + if (Time.unscaledTime < friendEspIgnoreNextLoadAt) return; + friendEspIgnoreNextLoadAt = Time.unscaledTime + 3f; + + friendEspIgnoreTokens.Clear(); + string path = FriendEspIgnoreFilePath(); + if (!System.IO.File.Exists(path)) + { + System.IO.File.WriteAllText(path, "# One nickname, Friend Code, or PUID per line. Matching players will not show ESP info.\n"); + return; + } + + foreach (string line in System.IO.File.ReadAllLines(path)) + { + string token = NormalizeEspToken(line); + if (string.IsNullOrWhiteSpace(token) || token.StartsWith("#")) continue; + friendEspIgnoreTokens.Add(token); + } + } + catch { } + } + +private static bool IsEspIgnored(NetworkedPlayerInfo info) + { + if (info == null) return false; + + LoadFriendEspIgnoreTokensIfNeeded(); + if (friendEspIgnoreTokens.Count == 0) return false; + + try + { + string name = NormalizeEspToken(info.PlayerName); + if (!string.IsNullOrEmpty(name) && friendEspIgnoreTokens.Contains(name)) return true; + + string displayedFc = NormalizeEspToken(GetDisplayedFriendCode(info, string.Empty)); + if (!string.IsNullOrEmpty(displayedFc) && friendEspIgnoreTokens.Contains(displayedFc)) return true; + + string rawFc = NormalizeEspToken(info.FriendCode); + if (!string.IsNullOrEmpty(rawFc) && friendEspIgnoreTokens.Contains(rawFc)) return true; + + ClientData client = AmongUsClient.Instance?.GetClientFromPlayerInfo(info); + string puid = client == null ? string.Empty : NormalizeEspToken(GetClientPuid(client)); + return !string.IsNullOrEmpty(puid) && friendEspIgnoreTokens.Contains(puid); + } + catch { return false; } + } + +public static string BuildESPInfoLine(NetworkedPlayerInfo info) + { + if (info == null) return string.Empty; + + int level = 0; + string platform = "Unknown"; + bool isHost = false; + + try { level = (int)info.PlayerLevel + 1; } catch { } + + try + { + var client = AmongUsClient.Instance.GetClientFromPlayerInfo(info); + if (client != null) + { + platform = GetPlatform(client); + isHost = AmongUsClient.Instance.GetHost() == client; + } + } + catch { } + + if (enablePlatformSpoof && + PlayerControl.LocalPlayer != null && + info.PlayerId == PlayerControl.LocalPlayer.PlayerId) + { + platform = $"{platform} spf"; + } + + string fc = CompactEspValue(GetDisplayedFriendCode(info)); + List parts = new List(); + if (isHost) parts.Add("Host"); + parts.Add($"Lv:{level}"); + parts.Add(platform); + if (showEspFriendCode) parts.Add(fc); + return string.Join(" - ", parts); + } + +public static Color GetRoleColor(int roleId, Color fallbackColor) + { + switch (roleId) + { + case 1: return new Color32(255, 0, 0, 255); + case 2: return new Color32(0, 0, 128, 255); + case 3: return new Color32(127, 255, 212, 255); + case 4: return new Color32(176, 196, 222, 255); + case 5: return new Color32(255, 140, 0, 255); + case 8: return new Color32(255, 105, 180, 255); + case 9: return new Color32(139, 0, 0, 255); + case 10: return new Color32(106, 90, 205, 255); + case 12: return new Color32(189, 183, 107, 255); + case 18: return new Color32(173, 255, 47, 255); + default: return fallbackColor; + } + } + +public static void HandleTracer(PlayerControl target, bool enable) + { + try + { + if (target == null || target.gameObject == null) return; + + LineRenderer lr = target.GetComponent(); + + if (!enable || PlayerControl.LocalPlayer == null || target == PlayerControl.LocalPlayer || target.Data == null || target.Data.Disconnected || IsEspIgnored(target.Data)) + { + if (lr != null) lr.enabled = false; + return; + } + + if (target.Data.IsDead && !seeGhosts && !PlayerControl.LocalPlayer.Data.IsDead) + { + if (lr != null) lr.enabled = false; + return; + } + + if (lr == null) + { + lr = target.gameObject.AddComponent(); + lr.SetVertexCount(2); + lr.SetWidth(0.02f, 0.02f); + try { if (HatManager.Instance != null) lr.material = HatManager.Instance.PlayerMaterial; } catch { } + } + + lr.enabled = true; + + Color tColor = Color.white; + try + { + if (target.Data.IsDead) + { + tColor = Color.gray; + } + else if (target.Data.Role != null) + { + tColor = GetRoleColor((int)target.Data.Role.Role, target.Data.Role.TeamColor); + } + } + catch { } + + lr.SetColors(tColor, tColor); + + lr.SetPosition(0, PlayerControl.LocalPlayer.transform.position); + lr.SetPosition(1, target.transform.position); + } + catch { } + } + +private void DrawLobbyAllColorSlider() + { + int maxColor = MaxOutfitColorId(); + lobbyAllColorId = Mathf.Clamp(lobbyAllColorId, 0, maxColor); + + Rect row = GUILayoutUtility.GetRect(250f, 26f, GUILayout.Width(250f), GUILayout.Height(26f)); + string colorName = SafeColorName(lobbyAllColorId); + + GUIStyle rowLabelStyle = new GUIStyle(toggleLabelStyle) + { + alignment = TextAnchor.MiddleLeft, + clipping = TextClipping.Clip, + fontSize = 11, + padding = CreateRectOffset(0, 0, 0, 0) + }; + + Rect labelRect = new Rect(row.x, row.y + 2f, 88f, 22f); + Rect sliderRect = new Rect(labelRect.xMax + 7f, row.y, 86f, row.height); + Rect applyRect = new Rect(row.xMax - 58f, row.y + 2f, 58f, 22f); + + GUI.Label(labelRect, $"{L("Color:", "Цвет:")} {colorName}", rowLabelStyle); + lobbyAllColorId = DrawCenteredColorSlider(sliderRect, lobbyAllColorId, maxColor); + + if (GUI.Button(applyRect, L("Apply", "Применить"), btnStyle)) + { + ApplyColorToLobby(lobbyAllColorId); + ShowNotification($"[LOBBY] {L("Applied lobby color.", "Цвет лобби применен.")}"); + } + } + +private int DrawCenteredColorSlider(Rect rect, int value, int maxValue) + { + maxValue = Mathf.Max(0, maxValue); + value = Mathf.Clamp(value, 0, maxValue); + + float centerY = rect.y + rect.height * 0.5f; + Rect trackRect = new Rect(rect.x + 2f, centerY - 4f, rect.width - 4f, 8f); + float thumbSize = 18f; + float t = maxValue <= 0 ? 0f : value / (float)maxValue; + float thumbCenterX = Mathf.Lerp(trackRect.xMin, trackRect.xMax, t); + Rect thumbRect = new Rect(thumbCenterX - thumbSize * 0.5f, centerY - thumbSize * 0.5f, thumbSize, thumbSize); + Rect hitRect = new Rect(rect.x, centerY - 12f, rect.width, 24f); + + int controlId = GUIUtility.GetControlID("LobbyAllColorSlider".GetHashCode(), FocusType.Passive, hitRect); + Event e = Event.current; + + if (e != null) + { + EventType type = e.GetTypeForControl(controlId); + if (type == EventType.MouseDown && e.button == 0 && hitRect.Contains(e.mousePosition)) + { + GUIUtility.hotControl = controlId; + value = SliderValueFromMouse(e.mousePosition.x, trackRect, maxValue); + e.Use(); + } + else if (type == EventType.MouseDrag && GUIUtility.hotControl == controlId) + { + value = SliderValueFromMouse(e.mousePosition.x, trackRect, maxValue); + e.Use(); + } + else if (type == EventType.MouseUp && GUIUtility.hotControl == controlId) + { + GUIUtility.hotControl = 0; + e.Use(); + } + } + + GUI.Box(trackRect, string.Empty, sliderStyle); + GUI.Box(thumbRect, string.Empty, sliderThumbStyle); + return value; + } + +private static int SliderValueFromMouse(float mouseX, Rect trackRect, int maxValue) + { + if (maxValue <= 0) return 0; + float t = Mathf.InverseLerp(trackRect.xMin, trackRect.xMax, mouseX); + return Mathf.Clamp(Mathf.RoundToInt(t * maxValue), 0, maxValue); + } + +private static void ApplyColorToLobby(int colorId) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) return; + + byte targetColor = (byte)Mathf.Clamp(colorId, 0, MaxOutfitColorId()); + try + { + foreach (var player in PlayerControl.AllPlayerControls) + { + if (player != null && player.Data != null && !player.Data.Disconnected) + player.RpcSetColor(targetColor); + } + } + catch { } + } + +private static string GetSafeColorName(int colorId) + { + try { return Palette.GetColorName(Mathf.Clamp(colorId, 0, MaxOutfitColorId())); } + catch { return $"Color {colorId}"; } + } + +private void DrawLobbyControls() + { + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(menuCardStyle, GUILayout.Width(282)); + DrawMenuSectionHeader(L("GAME RULES", "ПРАВИЛА ИГРЫ")); + neverEndGame = DrawToggle(neverEndGame, L("Unlimited Game", "Бесконечная игра"), 250); + GUILayout.Space(5); + noSettingLimit = DrawToggle(noSettingLimit, L("No Setting Limit", "Без лимитов настроек"), 250); + GUILayout.Space(5); + noTaskMode = DrawToggle(noTaskMode, L("No Task Mode", "Без заданий"), 250); + GUILayout.Space(5); + allowDuplicateColors = DrawToggle(allowDuplicateColors, L("Allow Duplicate Colors", "Разрешить одинаковые цвета"), 250); + GUILayout.Space(5); + + bool prevLobbyRainbowAll = lobbyRainbowAll; + lobbyRainbowAll = DrawToggle(lobbyRainbowAll, L("Rainbow All", "Радуга всем"), 250); + if (lobbyRainbowAll && !prevLobbyRainbowAll) + { + lobbyAllColor = false; + colorTimer = 0f; + } + + GUILayout.Space(5); + bool prevLobbyAllColor = lobbyAllColor; + lobbyAllColor = DrawToggle(lobbyAllColor, L("All Color", "Цвет всем"), 250); + if (lobbyAllColor && !prevLobbyAllColor) + { + lobbyRainbowAll = false; + } + + if (lobbyAllColor) + { + GUILayout.Space(3); + DrawLobbyAllColorSlider(); + } + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.BeginVertical(menuCardStyle, GUILayout.Width(282)); + DrawMenuSectionHeader(L("CHAT MODERATION", "МОДЕРАЦИЯ ЧАТА")); + enableColorCommand = DrawToggle(enableColorCommand, L("Enable /c command (Public)", "Разрешить команду /c"), 250); + GUILayout.Space(5); + blockFortegreenChat = DrawToggle(blockFortegreenChat, L("Block Fortegreen Chat", "Блокировать чат Fortegreen"), 250); + GUILayout.Space(5); + blockRainbowChat = DrawToggle(blockRainbowChat, L("Block Rainbow Chat", "Блокировать радужный чат"), 250); + GUILayout.Space(5); + autoChatEveryone = DrawToggle(autoChatEveryone, L("Chat Everyone (Auto-Meeting)", "Чат всем через авто-митинг"), 250); + if (autoChatEveryone) + { + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Delay:", "Задержка:")} {autoChatEveryoneDelay:0.0}s", toggleLabelStyle, GUILayout.Width(92)); + autoChatEveryoneDelay = GUILayout.HorizontalSlider(autoChatEveryoneDelay, 0f, 10f, sliderStyle, sliderThumbStyle, GUILayout.Width(170)); + GUILayout.EndHorizontal(); + } + GUILayout.EndVertical(); + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(menuCardStyle, GUILayout.Width(282)); + DrawMenuSectionHeader(L("LOBBY ACTIONS", "ДЕЙСТВИЯ ЛОББИ")); + if (GUILayout.Button(L("Insta Start", "Мгновенный старт"), btnStyle, GUILayout.Height(26))) + { GameStartManager.Instance.startState = GameStartManager.StartingStates.Countdown; GameStartManager.Instance.countDownTimer = 0f; } + GUILayout.Space(5); + if (GUILayout.Button(L("Close Meeting", "Закрыть собрание"), btnStyle, GUILayout.Height(26))) MeetingHud.Instance.RpcClose(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button(L("Spawn Lobby", "Создать лобби"), activeTabStyle, GUILayout.Height(26))) SpawnLobby(); + GUILayout.Space(5); + if (GUILayout.Button(L("Despawn", "Удалить"), btnStyle, GUILayout.Height(26))) DespawnLobby(); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button(L("Kill All", "Убить всех"), btnStyle, GUILayout.Height(26))) KillAll(); + GUILayout.Space(5); + if (GUILayout.Button(L("Kick All", "Кикнуть всех"), btnStyle, GUILayout.Height(26))) KickAll(); + GUILayout.Space(5); + if (GUILayout.Button(L("Mass Morph", "Масс-морф"), btnStyle, GUILayout.Height(26))) this.StartCoroutine(MassMorphCoroutine().WrapToIl2Cpp()); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.BeginVertical(menuCardStyle, GUILayout.Width(282)); + DrawMenuSectionHeader(L("END GAME", "КОНЕЦ ИГРЫ")); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(L("Crewmate Win", "Победа экипажа"), btnStyle, GUILayout.Height(26))) SmartEndGame("CrewWin"); + GUILayout.Space(5); + if (GUILayout.Button(L("Impostor Win", "Победа предателей"), btnStyle, GUILayout.Height(26))) SmartEndGame("ImpWin"); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button(L("Imp Disconnect", "Дисконнект предателя"), btnStyle, GUILayout.Height(26))) SmartEndGame("ImpDisconnect"); + GUILayout.Space(5); + if (GUILayout.Button(L("H&S Disconnect", "H&S дисконнект"), activeTabStyle, GUILayout.Height(26))) SmartEndGame("HnsImpDisconnect"); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + if (GUILayout.Button(L("Force End (Impostor Disconnect)", "Завершить принудительно"), btnStyle, GUILayout.Height(26)) && GameManager.Instance != null && AmongUsClient.Instance.AmHost) + { bool tempNeverEnd = neverEndGame; neverEndGame = false; GameManager.Instance.RpcEndGame((GameOverReason)4, false); neverEndGame = tempNeverEnd; } + GUILayout.EndVertical(); + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + } + +public static string GetESPNameTag(NetworkedPlayerInfo info, string originalName) + { + if (info == null) return originalName; + string newName = originalName; + bool suppressPlayerInfo = IsMeetingVoteUiActive(); + if (enableLocalNameSpoof && + PlayerControl.LocalPlayer != null && + info.PlayerId == PlayerControl.LocalPlayer.PlayerId && + !string.IsNullOrWhiteSpace(customNameInput)) + { + newName = BuildLocalNameRenderText(customNameInput); + } + + if (seeRoles && info.Role != null) + { + string roleName = info.Role.Role.ToString(); + int roleId = (int)info.Role.Role; + if (roleId == 8) roleName = "Noisemaker"; + else if (roleId == 9) roleName = "Phantom"; + else if (roleId == 10) roleName = "Tracker"; + else if (roleId == 12) roleName = "Detective"; + else if (roleId == 18) roleName = "Viper"; + else if (roleName == "GuardianAngel") roleName = "Guardian Angel"; + Color customColor = GetRoleColor(roleId, info.Role.TeamColor); + string roleColor = ColorUtility.ToHtmlStringRGB(customColor); + newName = $"{roleName}\n{newName}"; + } + if (showPlayerInfo && !suppressPlayerInfo) + { + string accentHex = GetMenuAccentHex(false); + string espLine = BuildESPInfoLine(info); + if (!string.IsNullOrWhiteSpace(espLine)) + newName = $"{espLine}\n{newName}"; + } + if (seeKillCooldown && !suppressPlayerInfo && info.Role != null && info.PlayerId != PlayerControl.LocalPlayer?.PlayerId) + { + int roleId = (int)info.Role.Role; + bool isImpTeam = roleId == 1 || roleId == 5 || roleId == 9 || roleId == 18; + if (isImpTeam) + { + float rem = GetRemainingKillCooldown(info.PlayerId); + string kcdText = rem > 0.01f ? $"KCD: {rem:F1}s" : "KCD: READY"; + string kcdColor = rem > 0.01f ? "FFAA33" : "55FF77"; + newName = $"{kcdText}\n{newName}"; + } + } + return newName; + } + +public static bool IsMeetingVoteUiActive() + { + try + { + return MeetingHud.Instance != null && MeetingHud.Instance.gameObject != null && MeetingHud.Instance.gameObject.activeInHierarchy; + } + catch + { + return MeetingHud.Instance != null; + } + } + +private static float GetConfiguredKillCooldown() + { + try + { + object opts = GameOptionsManager.Instance?.CurrentGameOptions; + if (opts == null) return 25f; + var m = opts.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(x => x.Name == "GetFloat" && x.GetParameters().Length == 1); + if (m != null) + { + Type enumType = m.GetParameters()[0].ParameterType; + if (enumType.IsEnum) + { + foreach (var val in Enum.GetValues(enumType)) + { + string n = val.ToString().ToLower(); + if (n.Contains("kill") && n.Contains("cool")) + { + object result = m.Invoke(opts, new object[] { val }); + return Convert.ToSingle(result); + } + } + } + } + } + catch { } + return 25f; + } + +private static float GetRemainingKillCooldown(byte playerId) + { + if (!lastKillTimestamps.ContainsKey(playerId)) return 0f; + float elapsed = Time.time - lastKillTimestamps[playerId]; + float rem = GetConfiguredKillCooldown() - elapsed; + return Mathf.Max(0f, rem); + } + +private static bool IsImpostorTeamForCooldown(PlayerControl pc) + { + try + { + if (pc == null || pc.Data == null) return false; + int roleId = pc.Data.Role != null ? (int)pc.Data.Role.Role : (int)pc.Data.RoleType; + return roleId == 1 || roleId == 5 || roleId == 9 || roleId == 18; + } + catch { return false; } + } + +public static void InitializeKillCooldownOnRoundStart() + { + try + { + lastKillTimestamps.Clear(); + if (PlayerControl.AllPlayerControls == null) return; + + float now = Time.time; + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.Data == null || pc.Data.Disconnected) continue; + if (!IsImpostorTeamForCooldown(pc)) continue; + lastKillTimestamps[pc.PlayerId] = now; + } + } + catch { } + } + +[HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] + public static class VersionShower_Start_Patch + { + public static void Postfix(VersionShower __instance) { if (__instance != null && __instance.text != null) __instance.text.text = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu Meowchelo & Carrot"); } + } + +[HarmonyPatch(typeof(PingTracker), nameof(PingTracker.Update))] + public static class PingTracker_Watermark_Patch + { + private static float _smoothFps = 0f; + private static int _smoothPing = 0; + private static float _updateTimer = 0f; + public static void Postfix(PingTracker __instance) + { + try + { + _updateTimer += Time.deltaTime; + if (_updateTimer >= 0.5f) { _smoothFps = 1f / Time.deltaTime; if (AmongUsClient.Instance != null) _smoothPing = AmongUsClient.Instance.Ping; _updateTimer = 0f; } + int num = Mathf.RoundToInt(_smoothFps); + string pingColor = ((_smoothPing < 80) ? "#00FF00" : ((_smoothPing < 400) ? "#FFFF00" : "#FF0000")); + + string finalString = $"PING: {_smoothPing} ms • FPS: {num}"; + + if (ElysiumModMenuGUI.showWatermark) + { + string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.5.1"); + finalString = $"{shimmerTitle} • " + finalString; + } + + if (AmongUsClient.Instance != null) + { + ClientData host = AmongUsClient.Instance.GetHost(); + if (host != null && host.Character != null) + { + string hostName = host.Character.Data.PlayerName ?? "Unknown"; + string shimmerHostName = ElysiumModMenuGUI.ApplyMenuShimmer(hostName); + finalString += $" • Host: {shimmerHostName}"; + if (AmongUsClient.Instance.AmHost) finalString += " (You)"; + } + } + __instance.text.text = finalString; + __instance.text.alignment = TMPro.TextAlignmentOptions.Center; + __instance.aspectPosition.enabled = false; + float zPos = MeetingHud.Instance != null && MeetingHud.Instance.gameObject.activeInHierarchy ? -100f : -10f; + __instance.transform.localPosition = new Vector3(0f, -2.3f, zPos); + } + catch { } + } + } + +[HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Update))] + public static class GameStartManager_Update_Patch + { + public static void Postfix(GameStartManager __instance) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; + if (ElysiumModMenuGUI.fakeStartCounterTroll) + { + try { sbyte[] arr = { -123, -111, -100, -69, -67, -52, -42, 0, 42, 52, 67, 69, 100, 111, 123 }; sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; PlayerControl.LocalPlayer.RpcSetStartCounter(b); __instance.SetStartCounter(b); } catch { } + } + else if (ElysiumModMenuGUI.fakeStartCounterCustom && int.TryParse(ElysiumModMenuGUI.fakeStartInput, out int custom)) + { + try { PlayerControl.LocalPlayer.RpcSetStartCounter(custom); __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); } catch { } + } + } + } + +[HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] + public static class InfiniteGamePatch { public static bool Prefix() { try { if (ElysiumModMenuGUI.neverEndGame && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) return false; } catch { } return true; } } + +[HarmonyPatch(typeof(IntroCutscene), "CoBegin")] + public static class IntroCutscene_CoBegin_Patch + { + public static void Prefix() + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + if (ElysiumModMenuGUI.enablePreGameRoleForce) + { + foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) + { var target = GameData.Instance.GetPlayerById(kvp.Key)?.Object; if (target != null && target.Data.RoleType != kvp.Value) target.RpcSetRole(kvp.Value); } + foreach (byte impId in ElysiumModMenuGUI.forcedImpostors) + { var target = GameData.Instance.GetPlayerById(impId)?.Object; if (target != null && target.Data.Role != null && !target.Data.Role.IsImpostor) target.RpcSetRole(RoleTypes.Impostor); } + } + } + } + +[HarmonyPatch(typeof(LogicRoleSelectionNormal), "AssignRolesForTeam")] + public static class RoleSelectionNormal_Patch + { + public static bool Prefix(Il2CppSystem.Collections.Generic.List players, IGameOptions opts, RoleTeamTypes team, ref int teamMax) + { + if (!ElysiumModMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; + try + { + if ((int)team == 1) + { + int numImps = opts.GetInt((Int32OptionNames)1); + var impRoleTypes = new HashSet { 1, 5, 9, 18 }; + List allForced = new List(ElysiumModMenuGUI.forcedImpostors); + foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) if (impRoleTypes.Contains((int)kvp.Value) && !allForced.Contains(kvp.Key)) allForced.Add(kvp.Key); + if (allForced.Count > 0) numImps = allForced.Count; + else { if (numImps >= players.Count) numImps = players.Count - 1; if (numImps < 1) numImps = 1; } + int assigned = 0; + foreach (byte impId in allForced) + { + if (players.Count == 0 || assigned >= numImps) break; + var targetInfo = players.ToArray().FirstOrDefault(p => p.PlayerId == impId); + if (targetInfo != null && targetInfo.Object != null) + { + RoleTypes role = ElysiumModMenuGUI.forcedPreGameRoles.ContainsKey(impId) ? ElysiumModMenuGUI.forcedPreGameRoles[impId] : RoleTypes.Impostor; + targetInfo.Object.RpcSetRole(role, false); + players.Remove(targetInfo); + assigned++; + } + } + while (assigned < numImps && players.Count > 0) + { + int idx = UnityEngine.Random.Range(0, players.Count); + players[idx].Object.RpcSetRole(RoleTypes.Impostor, false); + players.RemoveAt(idx); + assigned++; + } + return false; + } + else if ((int)team == 0) + { + var crewRoleTypes = new HashSet { 0, 2, 3, 4, 8, 10, 12 }; + for (int i = players.Count - 1; i >= 0; i--) + { + var p = players[i]; + if (p != null && p.Object != null) + { + RoleTypes role = RoleTypes.Crewmate; + if (ElysiumModMenuGUI.forcedPreGameRoles.ContainsKey(p.PlayerId) && crewRoleTypes.Contains((int)ElysiumModMenuGUI.forcedPreGameRoles[p.PlayerId])) + role = ElysiumModMenuGUI.forcedPreGameRoles[p.PlayerId]; + p.Object.RpcSetRole(role, false); + players.RemoveAt(i); + } + } + return false; + } + return true; + } + catch { return true; } + } + } + +[HarmonyPatch(typeof(LogicRoleSelectionHnS), "AssignRolesForTeam")] + public static class RoleSelectionHnS_Patch + { + public static bool Prefix(Il2CppSystem.Collections.Generic.List players, IGameOptions opts, RoleTeamTypes team, ref int teamMax) + { + if (!ElysiumModMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; + if ((int)team != 1) return true; + try + { + int numImps = opts.GetInt((Int32OptionNames)1); + var impRoleTypes = new HashSet { 1, 5, 9, 18 }; + List allForced = new List(ElysiumModMenuGUI.forcedImpostors); + foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) if (impRoleTypes.Contains((int)kvp.Value) && !allForced.Contains(kvp.Key)) allForced.Add(kvp.Key); + if (allForced.Count > 0) numImps = allForced.Count; + else { if (numImps >= players.Count) numImps = players.Count - 1; if (numImps < 1) numImps = 1; } + int assigned = 0; + foreach (byte impId in allForced) + { + if (players.Count == 0 || assigned >= numImps) break; + var targetInfo = players.ToArray().FirstOrDefault(p => p.PlayerId == impId); + if (targetInfo != null) { targetInfo.Object.RpcSetRole((RoleTypes)1, false); players.Remove(targetInfo); assigned++; } + } + while (assigned < numImps && players.Count > 0) + { + int idx = UnityEngine.Random.Range(0, players.Count); + players[idx].Object.RpcSetRole((RoleTypes)1, false); + players.RemoveAt(idx); + assigned++; + } + return false; + } + catch { return true; } + } + } +} +} diff --git a/features/FavoriteOutfitsPanel.cs b/features/FavoriteOutfitsPanel.cs new file mode 100644 index 0000000..390313f --- /dev/null +++ b/features/FavoriteOutfitsPanel.cs @@ -0,0 +1,634 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +public void Update() + { + bool isTypingOrBinding = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingGhostChatColor || isEditingBan || customChatInputFocused || + isWaitingForBind || isWaitBindMassMorph || isWaitBindSpawnLobby || + isWaitBindDespawnLobby || isWaitBindCloseMeeting || isWaitBindInstaStart || + isWaitBindEndCrew || isWaitBindEndImp || isWaitBindEndImpDC || isWaitBindEndHnsDC || + isWaitBindMagnetCursor || isWaitBindToggleTracers || isWaitBindToggleNoClip || + isWaitBindToggleFreecam || isWaitBindToggleCameraZoom || isWaitBindKillAll || + isWaitBindCallMeeting || isWaitBindTogglePlayerInfo || isWaitBindToggleSeeRoles || + isWaitBindToggleSeeGhosts || isWaitBindToggleFullBright || isWaitBindKickAll || + isWaitBindFixSabotages || isWaitBindSetAllGhost || isWaitBindSetAllGhostImp || + isWaitBindReviveAll; + + KeyCode activeMenuKey = menuToggleKey == KeyCode.None ? KeyCode.Insert : menuToggleKey; + if (!isTypingOrBinding && Input.GetKeyDown(activeMenuKey)) + { + showMenu = !showMenu; + if (!showMenu) SaveConfig(); + } + + if (!isTypingOrBinding) + { + if (bindMassMorph != KeyCode.None && Input.GetKeyDown(bindMassMorph)) + { + if (CanRunHostBind("Mass Morph")) + this.StartCoroutine(MassMorphCoroutine().WrapToIl2Cpp()); + } + + if (bindSpawnLobby != KeyCode.None && Input.GetKeyDown(bindSpawnLobby)) + { + if (CanRunHostBind("Spawn Lobby")) SpawnLobby(); + } + + if (bindDespawnLobby != KeyCode.None && Input.GetKeyDown(bindDespawnLobby)) + { + if (CanRunHostBind("Despawn Lobby")) DespawnLobby(); + } + + if (bindCloseMeeting != KeyCode.None && Input.GetKeyDown(bindCloseMeeting)) + { + if (CanRunHostBind("Close Meeting") && MeetingHud.Instance != null) + MeetingHud.Instance.RpcClose(); + } + + if (bindInstaStart != KeyCode.None && Input.GetKeyDown(bindInstaStart) && CanRunHostBind("Insta Start") && GameStartManager.Instance != null) + { + GameStartManager.Instance.startState = GameStartManager.StartingStates.Countdown; + GameStartManager.Instance.countDownTimer = 0f; + } + if (bindMagnetCursor != KeyCode.None && Input.GetKeyDown(bindMagnetCursor)) + { + autoFollowCursor = !autoFollowCursor; + ShowNotification(autoFollowCursor ? + "[MAGNET] Magnet Cursor: ON" : + "[MAGNET] Magnet Cursor: OFF"); + } + if (bindEndCrew != KeyCode.None && Input.GetKeyDown(bindEndCrew) && CanRunHostBind("End: Crewmate Win")) SmartEndGame("CrewWin"); + if (bindEndImp != KeyCode.None && Input.GetKeyDown(bindEndImp) && CanRunHostBind("End: Impostor Win")) SmartEndGame("ImpWin"); + if (bindEndImpDC != KeyCode.None && Input.GetKeyDown(bindEndImpDC) && CanRunHostBind("End: Imp Disconnect")) SmartEndGame("ImpDisconnect"); + if (bindEndHnsDC != KeyCode.None && Input.GetKeyDown(bindEndHnsDC) && CanRunHostBind("End: H&S Disconnect")) SmartEndGame("HnsImpDisconnect"); + if (bindToggleTracers != KeyCode.None && Input.GetKeyDown(bindToggleTracers)) + { + showTracers = !showTracers; + ShowNotification(showTracers ? "[TRACERS] ON" : "[TRACERS] OFF"); + } + if (bindToggleNoClip != KeyCode.None && Input.GetKeyDown(bindToggleNoClip)) + { + noClip = !noClip; + ShowNotification(noClip ? "[NOCLIP] ON" : "[NOCLIP] OFF"); + } + if (bindToggleFreecam != KeyCode.None && Input.GetKeyDown(bindToggleFreecam)) + { + freecam = !freecam; + ShowNotification(freecam ? "[FREECAM] ON" : "[FREECAM] OFF"); + } + if (bindToggleCameraZoom != KeyCode.None && Input.GetKeyDown(bindToggleCameraZoom)) + { + cameraZoom = !cameraZoom; + ShowNotification(cameraZoom ? "[ZOOM] ON" : "[ZOOM] OFF"); + } + if (bindTogglePlayerInfo != KeyCode.None && Input.GetKeyDown(bindTogglePlayerInfo)) + { + showPlayerInfo = !showPlayerInfo; + ShowNotification(showPlayerInfo ? "[PLAYER INFO] ON" : "[PLAYER INFO] OFF"); + } + if (bindToggleSeeRoles != KeyCode.None && Input.GetKeyDown(bindToggleSeeRoles)) + { + seeRoles = !seeRoles; + ShowNotification(seeRoles ? "[ROLES] ON" : "[ROLES] OFF"); + } + if (bindToggleSeeGhosts != KeyCode.None && Input.GetKeyDown(bindToggleSeeGhosts)) + { + seeGhosts = !seeGhosts; + ShowNotification(seeGhosts ? "[GHOSTS] ON" : "[GHOSTS] OFF"); + } + if (bindToggleFullBright != KeyCode.None && Input.GetKeyDown(bindToggleFullBright)) + { + fullBright = !fullBright; + ShowNotification(fullBright ? "[FULL BRIGHT] ON" : "[FULL BRIGHT] OFF"); + } + if (bindKillAll != KeyCode.None && Input.GetKeyDown(bindKillAll) && CanRunHostBind("Kill All")) KillAll(); + if (bindCallMeeting != KeyCode.None && Input.GetKeyDown(bindCallMeeting) && CanRunHostBind("Call Meeting")) callMeetingPublic(); + if (bindKickAll != KeyCode.None && Input.GetKeyDown(bindKickAll) && CanRunHostBind("Kick All")) KickAll(); + if (bindFixSabotages != KeyCode.None && Input.GetKeyDown(bindFixSabotages) && CanRunHostBind("Fix Sabotages")) FixAllSabotages(); + if (bindSetAllGhost != KeyCode.None && Input.GetKeyDown(bindSetAllGhost) && CanRunHostBind("Ghost All")) SetAllPlayersGhost(); + if (bindReviveAll != KeyCode.None && Input.GetKeyDown(bindReviveAll) && CanRunHostBind("Revive All")) ReviveAllPlayers(); + if (bindSetAllGhostImp != KeyCode.None && Input.GetKeyDown(bindSetAllGhostImp) && CanRunHostBind("All -> Ghost Imp")) SetAllPlayersGhost(true); + } + + ElysiumAutoHostService.Tick(); + ElysiumAutoLobbyReturn.UpdateLogic(); + ApplyFpsLimit(); + TryAutoGhostAfterStartTick(); + TryAutoBanCustomPlatformsTick(); + UpdateUnfixableLightsState(); + ApplyVentCheatsTick(); + TickRoleBuffImmortality(); + try { GetCurrentRoomCodeForStatus(); } catch { } + TrySendDiscordLaunchStatusTick(); + TryDetectLogBurstTick(); + if (votekickEveryone) + { + TickVotekickEveryoneRun(); + } + if (stylesInited && rgbMenuMode) + { + rgbMenuHue += Time.deltaTime * 0.2f; + if (rgbMenuHue > 1f) rgbMenuHue -= 1f; + UpdateAccentColor(Color.HSVToRGB(rgbMenuHue, 1f, 1f)); + } + + if (wasShowMenu && !showMenu) SaveConfig(); + wasShowMenu = showMenu; + + if (settingsDirty) + { + SaveConfig(); + settingsDirty = false; + } + + if (PlayerControl.LocalPlayer != null) + { + TryHostOnlyKillAuraTick(); + TryAutoBanBrokenFriendCodeTick(); + TryAutoKickLowLevelTick(); + + if (enableLocalNameSpoof && !isEditingName) + { + localNameRefreshTimer += Time.deltaTime; + if (localNameRefreshTimer >= 0.5f) + { + localNameRefreshTimer = 0f; + ApplyLocalNameSelf(customNameInput, false); + } + } + else + { + localNameRefreshTimer = 0f; + } + + if (enableLocalFriendCodeSpoof && !isEditingLocalFriendCode) + { + localFriendCodeRefreshTimer += Time.deltaTime; + if (localFriendCodeRefreshTimer >= 0.5f) + { + localFriendCodeRefreshTimer = 0f; + ApplyLocalFriendCodeSelf(localFriendCodeInput, false); + } + } + else + { + localFriendCodeRefreshTimer = 0f; + } + + if ((tpToCursor && Input.GetMouseButtonDown(1)) || + (dragToCursor && Input.GetMouseButton(2)) || + autoFollowCursor) + { + if (Camera.main != null) + { + Vector3 mPos = Camera.main.ScreenToWorldPoint(Input.mousePosition); + mPos.z = PlayerControl.LocalPlayer.transform.position.z; + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(mPos); + } + } + try + { + if (noTaskMode && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + if (GameOptionsManager.Instance != null && GameOptionsManager.Instance.CurrentGameOptions != null) + { + var opts = GameOptionsManager.Instance.CurrentGameOptions; + opts.SetInt(Int32OptionNames.NumCommonTasks, 0); + opts.SetInt(Int32OptionNames.NumLongTasks, 0); + opts.SetInt(Int32OptionNames.NumShortTasks, 0); + } + } + } + catch { } + if (autoChatEveryone && pendingAutoMeeting && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + try + { + if (PlayerControl.LocalPlayer != null && ShipStatus.Instance != null && !PlayerControl.LocalPlayer.Data.IsDead) + { + autoMeetingTimer += Time.deltaTime; + + if (autoMeetingTimer >= autoChatEveryoneDelay) + { + if (MeetingHud.Instance == null) + { + PlayerControl.LocalPlayer.CmdReportDeadBody(null); + } + else + { + MeetingHud.Instance.RpcClose(); + pendingAutoMeeting = false; + autoMeetingTimer = 0f; + ShowNotification("[CHAT EVERYONE] Игроки собраны в кафетерии!"); + } + } + } + } + catch { } + } + + if (customChatSpamEnabled) + { + customChatSpamTimer += Time.deltaTime; + if (customChatSpamTimer >= customChatSpamDelay) + { + customChatSpamTimer = 0f; + TrySendCustomChatMessage(customChatMessage); + } + } + else + { + customChatSpamTimer = 0f; + } + if (autoKickBugs && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && fortegreenTimer.Count > 0) + { + foreach (var kvp in fortegreenTimer.ToList()) + { + if (Time.time >= kvp.Value) + { + byte pid = kvp.Key; + var player = GameData.Instance.GetPlayerById(pid); + + if (player != null && !player.Disconnected && player.Object != null) + { + int currentColor = (int)player.DefaultOutfit.ColorId; + if (currentColor == 18 || currentColor >= Palette.PlayerColors.Length) + { + AmongUsClient.Instance.KickPlayer(player.ClientId, false); + ShowNotification($"[AUTO-KICK] Игрок {player.PlayerName} кикнут (Баг цвета)!"); + } + } + fortegreenTimer.Remove(pid); + } + } + } + if (PlayerControl.LocalPlayer != null) + { + try + { + if (AnimAsteroidsEnabled) + { + PlayerControl.LocalPlayer.PlayAnimation((byte)TaskTypes.ClearAsteroids); + RpcPlayAnimationMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, (byte)TaskTypes.ClearAsteroids); + AmongUsClient.Instance.LateBroadcastUnreliableMessage(Unsafe.As(rpcMessage)); + } + + if (AnimShieldsEnabled) + { + PlayerControl.LocalPlayer.PlayAnimation((byte)TaskTypes.PrimeShields); + RpcPlayAnimationMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, (byte)TaskTypes.PrimeShields); + AmongUsClient.Instance.LateBroadcastUnreliableMessage(Unsafe.As(rpcMessage)); + } + + if (IsScanning && !isScannerActiveFlag) + { + var count = ++PlayerControl.LocalPlayer.scannerCount; + PlayerControl.LocalPlayer.SetScanner(true, count); + RpcSetScannerMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, true, count); + AmongUsClient.Instance.LateBroadcastReliableMessage(Unsafe.As(rpcMessage)); + isScannerActiveFlag = true; + } + else if (!IsScanning && isScannerActiveFlag) + { + var count = ++PlayerControl.LocalPlayer.scannerCount; + PlayerControl.LocalPlayer.SetScanner(false, count); + RpcSetScannerMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, false, count); + AmongUsClient.Instance.LateBroadcastReliableMessage(Unsafe.As(rpcMessage)); + isScannerActiveFlag = false; + } + + if (ShipStatus.Instance != null) + { + if (AnimCamsInUseEnabled && !isCamsActiveFlag) + { + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Security, 1); + isCamsActiveFlag = true; + } + else if (!AnimCamsInUseEnabled && isCamsActiveFlag) + { + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Security, 0); + isCamsActiveFlag = false; + } + } + } + catch { } + } + try + { + if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.MyPhysics != null && PlayerControl.LocalPlayer.Data != null) + { + if (PlayerControl.LocalPlayer.Collider != null) + { + PlayerControl.LocalPlayer.Collider.enabled = !(noClip || PlayerControl.LocalPlayer.onLadder); + } + + if (PlayerControl.LocalPlayer.Data.IsDead) + { + PlayerControl.LocalPlayer.MyPhysics.GhostSpeed = 3f * walkSpeed; + } + else + { + PlayerControl.LocalPlayer.MyPhysics.Speed = 2.5f * walkSpeed; + } + } + } + catch { } + + if (SpoofMenuEnabled && PlayerControl.LocalPlayer != null) + { + uiSpoofTimer += Time.deltaTime; + if (uiSpoofTimer >= rpcSpoofDelay) + { + uiSpoofTimer = 0f; + byte rpc = spoofMenuRPCs[selectedSpoofMenuIndex]; + try + { + MessageWriter msg = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, rpc, SendOption.None, -1); + AmongUsClient.Instance.FinishRpcImmediately(msg); + } + catch { } + } + } + try + { + if (autoBanEnabled && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) + { + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.Data == null || pc.Data.Disconnected || pc == PlayerControl.LocalPlayer) continue; + + string fc = pc.Data.FriendCode; + if (!string.IsNullOrEmpty(fc)) + { + foreach (var entry in bannedEntries) + { + string[] parts = entry.Split('|'); + if (parts.Length > 0 && parts[0].Trim().ToLower() == fc.Trim().ToLower()) + { + AmongUsClient.Instance.KickPlayer(pc.OwnerId, true); + break; + } + } + } + } + } + } + catch { } + try + { + if (banBotsEnabled && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) + { + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.Data == null || pc.Data.Disconnected || pc == PlayerControl.LocalPlayer) continue; + + string botName = pc.Data.PlayerName ?? ""; + string botFc = pc.Data.FriendCode; + + bool isBot = IsBotName(botName) || (!string.IsNullOrEmpty(botFc) && IsBotBannedFc(botFc)); + if (!isBot) continue; + + string banFc = string.IsNullOrEmpty(botFc) ? "Unknown" : botFc; + string botPuid = "Unknown"; + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(pc); + if (client != null) botPuid = GetClientPuid(client); + } + catch { } + + AddToBotBanList(banFc, botPuid, string.IsNullOrEmpty(botName) ? "Unknown" : botName, "Bot nickname"); + AmongUsClient.Instance.KickPlayer(pc.OwnerId, true); + ShowNotification($"[BAN BOTS] {(string.IsNullOrEmpty(botName) ? "Unknown" : botName)} кикнут (бот)."); + } + } + } + catch { } + if (freecam) + { + if (!_freecamActive && Camera.main != null) + { + var cam = Camera.main.gameObject.GetComponent(); + if (cam != null) { cam.enabled = false; cam.Target = null; } + _freecamActive = true; + } + if (PlayerControl.LocalPlayer != null) PlayerControl.LocalPlayer.moveable = false; + Vector3 movement = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0.0f); + if (Camera.main != null) Camera.main.transform.position += movement * 15f * Time.deltaTime; + } + else if (_freecamActive) + { + if (PlayerControl.LocalPlayer != null) PlayerControl.LocalPlayer.moveable = true; + if (Camera.main != null) + { + var cam = Camera.main.gameObject.GetComponent(); + if (cam != null && PlayerControl.LocalPlayer != null) { cam.enabled = true; cam.SetTarget(PlayerControl.LocalPlayer); } + } + _freecamActive = false; + } + + try { ApplyCameraZoomTick(); } + catch { } + + try + { + if (PlayerControl.AllPlayerControls != null) + { + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null) HandleTracer(pc, showTracers); + } + } + } + catch { } + + + + if (enableLevelSpoof && !isEditingLevel && uint.TryParse(spoofLevelString, out uint parsedLvl)) + { + ApplyLevelSpoofValue(parsedLvl, false); + } + try + { + if (localRainbow || rainbowPlayers.Count > 0 || lobbyRainbowAll) + { + colorTimer += Time.deltaTime; + if (colorTimer > 0.15f) + { + colorTimer = 0f; + currentColorId++; + if (currentColorId > MaxOutfitColorId()) currentColorId = 0; + + if (localRainbow && PlayerControl.LocalPlayer != null) + PlayerControl.LocalPlayer.CmdCheckColor(currentColorId); + + if ((rainbowPlayers.Count > 0 || lobbyRainbowAll) && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) + { + foreach (var p in PlayerControl.AllPlayerControls) + { + if (p != null && p.Data != null && !p.Data.Disconnected && (lobbyRainbowAll || rainbowPlayers.Contains(p.PlayerId))) + p.RpcSetColor(currentColorId); + } + } + } + } + } + + + catch { } + + try + { + if (localRainbowFreeOnly && PlayerControl.LocalPlayer != null && AmongUsClient.Instance != null && !AmongUsClient.Instance.AmHost) + { + freeColorTimer += Time.deltaTime; + if (freeColorTimer > 0.5f) + { + freeColorTimer = 0f; + var freeColorsTick = GetFreeColorIds(); + if (freeColorsTick.Count > 0) + { + freeRainbowIndex++; + if (freeRainbowIndex >= freeColorsTick.Count) freeRainbowIndex = 0; + PlayerControl.LocalPlayer.CmdCheckColor((byte)freeColorsTick[freeRainbowIndex]); + } + } + } + } + catch { } + + + } + } + +public static List GetFreeColorIds() + { + HashSet used = new HashSet(); + try + { + byte localId = PlayerControl.LocalPlayer != null ? PlayerControl.LocalPlayer.PlayerId : byte.MaxValue; + if (PlayerControl.AllPlayerControls != null) + { + foreach (var p in PlayerControl.AllPlayerControls) + { + if (p == null || p.Data == null || p.Data.Disconnected) continue; + if (p.PlayerId == localId) continue; + try + { + int cid = p.Data.DefaultOutfit != null ? p.Data.DefaultOutfit.ColorId : -1; + if (cid >= 0 && cid <= 17) used.Add(cid); + } + catch { } + } + } + } + catch { } + + List free = new List(); + for (int i = 0; i <= 17; i++) + if (!used.Contains(i)) free.Add(i); + return free; + } + +public static readonly Dictionary colorNamesByLang = new Dictionary + { + ["en"] = new string[] { "Red", "Blue", "Green", "Pink", "Orange", "Yellow", "Black", "White", "Purple", "Brown", "Cyan", "Lime", "Maroon", "Rose", "Banana", "Gray", "Tan", "Coral" }, + ["ru"] = new string[] { "Красный", "Синий", "Зелёный", "Розовый", "Оранжевый", "Жёлтый", "Чёрный", "Белый", "Фиолетовый", "Коричневый", "Голубой", "Салатовый", "Бордовый", "Розочка", "Банановый", "Серый", "Бежевый", "Коралловый" }, + ["uk"] = new string[] { "Червоний", "Синій", "Зелений", "Рожевий", "Помаранчевий", "Жовтий", "Чорний", "Білий", "Фіолетовий", "Коричневий", "Блакитний", "Салатовий", "Бордовий", "Трояндовий", "Банановий", "Сірий", "Бежевий", "Кораловий" }, + ["de"] = new string[] { "Rot", "Blau", "Grün", "Pink", "Orange", "Gelb", "Schwarz", "Weiß", "Lila", "Braun", "Cyan", "Limette", "Kastanie", "Rosé", "Banane", "Grau", "Hellbraun", "Koralle" }, + ["fr"] = new string[] { "Rouge", "Bleu", "Vert", "Rose", "Orange", "Jaune", "Noir", "Blanc", "Violet", "Marron", "Cyan", "Citron vert", "Bordeaux", "Rose pâle", "Banane", "Gris", "Beige", "Corail" }, + ["es"] = new string[] { "Rojo", "Azul", "Verde", "Rosa", "Naranja", "Amarillo", "Negro", "Blanco", "Morado", "Marrón", "Cian", "Lima", "Granate", "Rosado", "Plátano", "Gris", "Beige", "Coral" }, + ["it"] = new string[] { "Rosso", "Blu", "Verde", "Rosa", "Arancione", "Giallo", "Nero", "Bianco", "Viola", "Marrone", "Ciano", "Lime", "Bordeaux", "Rosato", "Banana", "Grigio", "Beige", "Corallo" }, + ["pt"] = new string[] { "Vermelho", "Azul", "Verde", "Rosa", "Laranja", "Amarelo", "Preto", "Branco", "Roxo", "Marrom", "Ciano", "Lima", "Bordô", "Rosado", "Banana", "Cinza", "Bege", "Coral" }, + ["pl"] = new string[] { "Czerwony", "Niebieski", "Zielony", "Różowy", "Pomarańczowy", "Żółty", "Czarny", "Biały", "Fioletowy", "Brązowy", "Cyjan", "Limonkowy", "Bordowy", "Różany", "Bananowy", "Szary", "Beżowy", "Koralowy" }, + ["nl"] = new string[] { "Rood", "Blauw", "Groen", "Roze", "Oranje", "Geel", "Zwart", "Wit", "Paars", "Bruin", "Cyaan", "Limoen", "Kastanjebruin", "Rozerood", "Banaan", "Grijs", "Beige", "Koraal" }, + ["tr"] = new string[] { "Kırmızı", "Mavi", "Yeşil", "Pembe", "Turuncu", "Sarı", "Siyah", "Beyaz", "Mor", "Kahverengi", "Camgöbeği", "Limon", "Bordo", "Gül", "Muz", "Gri", "Bej", "Mercan" }, + ["cs"] = new string[] { "Červená", "Modrá", "Zelená", "Růžová", "Oranžová", "Žlutá", "Černá", "Bílá", "Fialová", "Hnědá", "Azurová", "Limetková", "Vínová", "Růžová", "Banánová", "Šedá", "Béžová", "Korálová" }, + ["ro"] = new string[] { "Roșu", "Albastru", "Verde", "Roz", "Portocaliu", "Galben", "Negru", "Alb", "Mov", "Maro", "Cyan", "Lime", "Vișiniu", "Roz pal", "Banană", "Gri", "Bej", "Coral" }, + ["hu"] = new string[] { "Piros", "Kék", "Zöld", "Rózsaszín", "Narancs", "Sárga", "Fekete", "Fehér", "Lila", "Barna", "Cián", "Lime", "Gesztenyebarna", "Rózsa", "Banán", "Szürke", "Drapp", "Korall" }, + ["sv"] = new string[] { "Röd", "Blå", "Grön", "Rosa", "Orange", "Gul", "Svart", "Vit", "Lila", "Brun", "Cyan", "Lime", "Vinröd", "Rosenröd", "Banan", "Grå", "Beige", "Korall" }, + ["da"] = new string[] { "Rød", "Blå", "Grøn", "Pink", "Orange", "Gul", "Sort", "Hvid", "Lilla", "Brun", "Cyan", "Lime", "Bordeaux", "Rosa", "Banan", "Grå", "Beige", "Koral" }, + ["fi"] = new string[] { "Punainen", "Sininen", "Vihreä", "Pinkki", "Oranssi", "Keltainen", "Musta", "Valkoinen", "Violetti", "Ruskea", "Syaani", "Limetti", "Viininpunainen", "Ruusu", "Banaani", "Harmaa", "Beige", "Koralli" }, + ["no"] = new string[] { "Rød", "Blå", "Grønn", "Rosa", "Oransje", "Gul", "Svart", "Hvit", "Lilla", "Brun", "Cyan", "Lime", "Vinrød", "Rosenrød", "Banan", "Grå", "Beige", "Korall" }, + ["el"] = new string[] { "Κόκκινο", "Μπλε", "Πράσινο", "Ροζ", "Πορτοκαλί", "Κίτρινο", "Μαύρο", "Λευκό", "Μωβ", "Καφέ", "Κυανό", "Λάιμ", "Βυσσινί", "Τριανταφυλλί", "Μπανάνα", "Γκρι", "Μπεζ", "Κοράλι" }, + ["zh"] = new string[] { "红色", "蓝色", "绿色", "粉色", "橙色", "黄色", "黑色", "白色", "紫色", "棕色", "青色", "青柠", "栗色", "玫瑰色", "香蕉色", "灰���", "棕褐色", "珊瑚色" }, + ["ja"] = new string[] { "赤", "青", "緑", "ピンク", "オレンジ", "黄", "黒", "白", "紫", "茶", "シアン", "ライム", "マルーン", "ローズ", "バナナ", "グレー", "タン", "コーラル" }, + ["ko"] = new string[] { "빨강", "파랑", "초록", "분홍", "주황", "노���", "검정", "흰색", "보라", "갈색", "청록", "라임", "적갈색", "장미", "바나나", "회색", "황갈색", "산호색" } + }; + +public static string SafeColorName(int id) + { + try + { + string lang = CurrentMenuLanguageCode(); + if (lang == "auto") + lang = ResolveAutoMenuLanguageCode(); + string[] names; + if (!colorNamesByLang.TryGetValue(lang, out names)) + colorNamesByLang.TryGetValue("en", out names); + if (names != null && id >= 0 && id < names.Length) + return names[id]; + } + catch { } + try { return Palette.GetColorName(id); } + catch { return "Color " + id; } + } + +public static void ForceSetScanner(PlayerControl player, bool toggle) + { + var count = ++player.scannerCount; + player.SetScanner(toggle, count); + RpcSetScannerMessage rpcMessage = new(player.NetId, toggle, count); + AmongUsClient.Instance.LateBroadcastReliableMessage(Unsafe.As(rpcMessage)); + } + +public static void ForcePlayAnimation(byte animationType) + { + PlayerControl.LocalPlayer.PlayAnimation(animationType); + RpcPlayAnimationMessage rpcMessage = new(PlayerControl.LocalPlayer.NetId, animationType); + AmongUsClient.Instance.LateBroadcastUnreliableMessage(Unsafe.As(rpcMessage)); + } +} +} diff --git a/features/GeneralPanel.cs b/features/GeneralPanel.cs new file mode 100644 index 0000000..3527828 --- /dev/null +++ b/features/GeneralPanel.cs @@ -0,0 +1,754 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +private bool DrawColoredActionButton(string text, Color color, float width, float height = 24f) + { + GUIStyle style = new GUIStyle(btnStyle); + Color themedColor = RgbMenuTextActive() ? GetMenuAccentColor() : (whiteMenuTheme ? GetThemeAccentColor(color) : color); + Color hoverColor = whiteMenuTheme + ? Color.Lerp(themedColor, Color.black, 0.18f) + : Color.Lerp(themedColor, Color.white, 0.22f); + + style.normal.textColor = themedColor; + style.hover.textColor = hoverColor; + style.focused.textColor = themedColor; + style.active.textColor = whiteMenuTheme ? Color.white : Color.black; + style.clipping = TextClipping.Overflow; + style.wordWrap = false; + + float minContentWidth = Mathf.Ceil(style.CalcSize(new GUIContent(text)).x) + 32f; + float finalButtonWidth = Mathf.Max(width, minContentWidth); + return GUILayout.Button(text, style, GUILayout.Width(finalButtonWidth), GUILayout.Height(height)); + } + +private bool DrawPseudoInputButton(string value, bool editing, float height = 28f, int maxChars = 52) + { + GUIStyle style = new GUIStyle(editing ? activeTabStyle : inputBlockStyle); + style.alignment = TextAnchor.MiddleLeft; + style.clipping = TextClipping.Clip; + style.wordWrap = false; + style.padding = CreateRectOffset(10, 10, 0, 0); + + Rect rect = GUILayoutUtility.GetRect(GUIContent.none, style, GUILayout.ExpandWidth(true), GUILayout.Height(height)); + return GUI.Button(rect, FormatInputPreview(value, editing, maxChars), style); + } + +private void DrawClippedHint(string text, float height = 13f) + { + GUIStyle style = new GUIStyle(toggleLabelStyle) + { + fontSize = 10, + clipping = TextClipping.Clip, + wordWrap = false, + alignment = TextAnchor.MiddleLeft + }; + + Rect rect = GUILayoutUtility.GetRect(GUIContent.none, style, GUILayout.ExpandWidth(true), GUILayout.Height(height)); + GUI.Label(rect, text, style); + } + +private void OpenExternalLink(string url, string label) + { + try + { + Application.OpenURL(url); + ShowNotification($"[LINK] {L("Opening", "Открываю")} {label}"); + } + catch + { + ShowNotification($"[LINK] {L("Failed to open link.", "Не удалось открыть ссылку.")}"); + } + } + +private void DrawGeneralInfoTab() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("ELYSIUM OVERVIEW", headerStyle); + GUILayout.Space(6); + + GUILayout.BeginHorizontal(); + for (int i = 0; i < generalInfoSubTabs.Length; i++) + { + GUIStyle tabStyle = currentGeneralInfoSubTab == i ? activeSubTabStyle : subTabStyle; + float tabWidth = Mathf.Max(116f, Mathf.Ceil(tabStyle.CalcSize(new GUIContent(generalInfoSubTabs[i])).x) + 28f); + if (GUILayout.Button(generalInfoSubTabs[i], tabStyle, GUILayout.Width(tabWidth), GUILayout.Height(24))) + { + currentGeneralInfoSubTab = i; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + GUILayout.Label(L("Menu language:", "Язык меню:"), toggleLabelStyle, GUILayout.MinWidth(128), GUILayout.ExpandWidth(false), GUILayout.Height(24)); + if (GUILayout.Button("<", btnStyle, GUILayout.Width(26), GUILayout.Height(24))) + { + currentMenuLanguageIndex--; + if (currentMenuLanguageIndex < 0) currentMenuLanguageIndex = menuLanguageNames.Length - 1; + SaveConfig(); + } + GUIStyle languageValueStyle = new GUIStyle(btnStyle) { normal = { background = null, textColor = GetMenuAccentColor() }, fontStyle = FontStyle.Bold, clipping = TextClipping.Overflow, wordWrap = false }; + string languageValue = menuLanguageNames[Mathf.Clamp(currentMenuLanguageIndex, 0, menuLanguageNames.Length - 1)]; + float languageValueWidth = Mathf.Max(132f, Mathf.Ceil(languageValueStyle.CalcSize(new GUIContent(languageValue)).x) + 24f); + GUILayout.Label(languageValue, languageValueStyle, GUILayout.Width(languageValueWidth), GUILayout.Height(24)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(26), GUILayout.Height(24))) + { + currentMenuLanguageIndex++; + if (currentMenuLanguageIndex >= menuLanguageNames.Length) currentMenuLanguageIndex = 0; + SaveConfig(); + } + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Space(8); + + string accentHex = GetMenuAccentHex(); + bool rgbText = RgbMenuTextActive(); + string githubHex = rgbText ? accentHex : ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(26, 188, 156, 255)) : new Color32(26, 188, 156, 255)); + string goldHex = rgbText ? accentHex : ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(255, 187, 54, 255)) : new Color32(255, 187, 54, 255)); + string leadHex = rgbText ? accentHex : ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(255, 92, 122, 255)) : new Color32(255, 92, 122, 255)); + string devHex = rgbText ? accentHex : ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(38, 194, 129, 255)) : new Color32(38, 194, 129, 255)); + string contributorHex = rgbText ? accentHex : ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(109, 138, 255, 255)) : new Color32(109, 138, 255, 255)); + string dangerHex = rgbText ? accentHex : ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(231, 76, 60, 255)) : new Color32(231, 76, 60, 255)); + string safeHex = rgbText ? accentHex : ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(57, 255, 20, 255)) : new Color32(57, 255, 20, 255)); + string versionText = "1.3.5.1"; + + GUIStyle textStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 12 }; + textStyle.normal.textColor = whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : new Color(0.85f, 0.85f, 0.85f, 1f); + + if (currentGeneralInfoSubTab == 0) + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label( + $"{L("Welcome to", "Добро пожаловать в")} ElysiumModMenu " + + $"v{versionText} {L("by", "от")} meowchelo!", + textStyle); + GUILayout.Space(4); + GUILayout.Label(L( + "ElysiumModMenu is a lightweight BepInEx IL2CPP utility for Among Us with lobby tools, visuals, spoofing and host-side controls.", + "ElysiumModMenu это легкий BepInEx IL2CPP мод для Among Us с инструментами для лобби, визуалом, спуфингом и хост-функциями."), textStyle); + GUILayout.Label(L( + "Use the buttons below to open the GitHub repository or jump straight to the latest public release.", + "Кнопки ниже открывают GitHub репозиторий и страницу с последним публичным релизом."), textStyle); + GUILayout.Space(6); + + GUILayout.BeginHorizontal(); + if (DrawColoredActionButton("GitHub", new Color32(26, 188, 156, 255), 110f)) + OpenExternalLink("https://github.com/meowchelo/ElysiumModMenu", "GitHub"); + GUILayout.Space(6); + if (DrawColoredActionButton("Check for Updates", new Color32(255, 187, 54, 255), 165f)) + OpenExternalLink("https://github.com/meowchelo/ElysiumModMenu/releases/latest", "Latest Release"); + GUILayout.Space(6); + if (DrawColoredActionButton("Discord", new Color32(88, 101, 242, 255), 110f)) + OpenExternalLink("https://discord.gg/bvwYBZZwUX", "Discord"); + GUILayout.EndHorizontal(); + + GUILayout.Space(8); + GUILayout.Label($"{L("Project", "Проект")}: meowchelo/ElysiumModMenu", textStyle); + GUILayout.Label($"{L("Main page", "Главная ссылка")}: https://github.com/meowchelo/ElysiumModMenu", textStyle); + GUILayout.Space(8); + GUILayout.Label($"{L("ElysiumModMenu is free and open-source software.", "ElysiumModMenu это бесплатный open-source проект.")}", textStyle); + GUILayout.Label($"{L("If you paid for this menu, demand a refund immediately.", "Если вы заплатили за это меню, требуйте возврат денег сразу.")}", textStyle); + GUILayout.Label($"{L("Make sure you are using the latest version from GitHub releases.", "Убедитесь, что используете последнюю версию из GitHub releases.")}", textStyle); + GUILayout.Space(8); + GUILayout.Label($"{L("Quick Hotkeys", "Быстрые клави��и")}", textStyle); + string menuKeyText = (menuToggleKey == KeyCode.None ? KeyCode.Insert : menuToggleKey).ToString(); + GUILayout.Label($"{L("Menu key", "Кнопка меню")}: {menuKeyText}", textStyle); + GUILayout.Label(L("Right Click: teleport to cursor", "ПКМ: телепорт к курсору"), textStyle); + GUILayout.Label(L("F9: magnet cursor", "F9: магнит курсора"), textStyle); + GUILayout.EndVertical(); + } + else + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label(L( + "ElysiumModMenu is an open-source project. Meet the people behind this build:", + "ElysiumModMenu это open-source проект. Ниже люди, которые стоят за этой сборкой:"), textStyle); + GUILayout.Space(8); + + GUILayout.Label($"LEAD DEVELOPER", textStyle); + GUILayout.Space(4); + if (DrawColoredActionButton("meowchelo", new Color32(255, 92, 122, 255), 150f)) + OpenExternalLink("https://github.com/meowchelo", "meowchelo"); + + GUILayout.Space(10); + GUILayout.Label($"DEVELOPERS", textStyle); + GUILayout.Space(4); + GUILayout.BeginHorizontal(); + if (DrawColoredActionButton("Carrot", new Color32(38, 194, 129, 255), 150f)) + OpenExternalLink("https://github.com/abobanamne", "Carrot"); + GUILayout.Space(6); + if (DrawColoredActionButton("wextikit", new Color32(109, 138, 255, 255), 150f)) + OpenExternalLink("https://github.com/wextikit", "wextikit"); + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + GUILayout.Label($"TESTERS", textStyle); + GUILayout.Space(4); + DrawColoredActionButton("Жена", new Color32(109, 138, 255, 255), 150f); + + GUILayout.Space(10); + GUILayout.Label($"{L("Repository", "Репозиторий")}", textStyle); + GUILayout.Label(L( + "The public source, releases and project updates are published on GitHub.", + "Публичный исходный код, релизы и обновления проекта публикуются на GitHub."), textStyle); + GUILayout.Space(4); + if (DrawColoredActionButton("Open ElysiumModMenu Repository", new Color32(26, 188, 156, 255), 220f)) + OpenExternalLink("https://github.com/meowchelo/ElysiumModMenu", "ElysiumModMenu Repository"); + + GUILayout.Space(10); + GUILayout.Label($"Found a bug or have a question?", textStyle); + GUILayout.Space(4); + if (DrawColoredActionButton("Join Discord", new Color32(88, 101, 242, 255), 150f)) + OpenExternalLink("https://discord.gg/bvwYBZZwUX", "Discord"); + + GUILayout.Space(10); + GUILayout.Label($"{L("Notes", "Примечание")}", textStyle); + GUILayout.Label(L( + "Thank you to everyone helping with ideas, testing and polishing the menu.", + "Спасибо всем, кто помогает идеями, тестами и полировкой меню."), textStyle); + GUILayout.EndVertical(); + } + + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + } + +[HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] + public static class ChatLogger_Patch + { + public static void Prefix(PlayerControl sourcePlayer, ref string chatText) + { + if (!ElysiumModMenuGUI.enableChatLog || string.IsNullOrWhiteSpace(chatText)) return; + + try + { + string time = System.DateTime.Now.ToString("HH:mm:ss"); + + string name = "System/Unknown"; + string levelStr = "?"; + string fc = "Hidden"; + string puid = "Unknown"; + string platformStr = "Unknown"; + + if (sourcePlayer != null && sourcePlayer.Data != null) + { + name = sourcePlayer.Data.PlayerName; + + uint rawLevel = sourcePlayer.Data.PlayerLevel; + if (rawLevel != uint.MaxValue && rawLevel < 10000) levelStr = (rawLevel + 1).ToString(); + + fc = GetDisplayedFriendCode(sourcePlayer.Data, "Hidden"); + + var client = AmongUsClient.Instance?.GetClientFromCharacter(sourcePlayer); + if (client != null) + { + puid = GetClientPuid(client); + platformStr = ElysiumModMenuGUI.GetPlatform(client); + } + } + + string cleanText = System.Text.RegularExpressions.Regex.Replace(chatText, "<.*?>", string.Empty); + + string logLine = $"[{time}] [{name}] [Lv:{levelStr}] [FC:{fc}] [ID:{puid}] [{platformStr}] : {cleanText}\n"; + + string chatLogPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ChatLog.txt"); + System.IO.File.AppendAllText(chatLogPath, logLine); + } + catch { } + } + } + +private void DrawSelfTab() + { + if (currentSelfSubTab == 0) currentSelfSubTab = 1; + + float selfColumnWidth = Mathf.Max(270f, (windowRect.width - 186f) * 0.5f); + + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(GUILayout.Width(selfColumnWidth)); + DrawSelfSpoof(); + GUILayout.EndVertical(); + + GUILayout.Space(8); + + GUILayout.BeginVertical(menuCardStyle, GUILayout.Width(selfColumnWidth), GUILayout.ExpandHeight(false)); + DrawMenuSectionHeader("SELF TOOLS"); + GUILayout.Space(4); + + GUILayout.BeginHorizontal(); + for (int i = 0; i < selfOtherTabs.Length; i++) + { + int tabIndex = i + 1; + if (GUILayout.Button(selfOtherTabs[i], currentSelfSubTab == tabIndex ? activeSubTabStyle : subTabStyle, GUILayout.Height(22))) + { + currentSelfSubTab = tabIndex; + scrollPosition = Vector2.zero; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(8); + + if (currentSelfSubTab == 1) DrawPlayerMovementCompact(); + else if (currentSelfSubTab == 2) DrawRolesCompact(); + else if (currentSelfSubTab == 3) DrawChatSettingsCompact(); + + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + +private void DrawPlayerMovementCompact() + { + GUILayout.BeginVertical(); + DrawMenuSectionHeader("MOVEMENT & TELEPORT"); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"Engine: {Mathf.Round(engineSpeed)}x", toggleLabelStyle, GUILayout.Width(86)); + engineSpeed = GUILayout.HorizontalSlider(engineSpeed, 1f, 555f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + if (GUILayout.Button("R", btnStyle, GUILayout.Width(28), GUILayout.Height(22))) engineSpeed = 1f; + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + GUILayout.Label($"Walk: {Mathf.Round(walkSpeed)}x", toggleLabelStyle, GUILayout.Width(86)); + walkSpeed = GUILayout.HorizontalSlider(walkSpeed, 1f, 30f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + if (GUILayout.Button("R", btnStyle, GUILayout.Width(28), GUILayout.Height(22))) walkSpeed = 1f; + GUILayout.EndHorizontal(); + + GUILayout.Space(8); + tpToCursor = DrawToggle(tpToCursor, "TP To Cursor", 230); + GUILayout.Space(3); + dragToCursor = DrawToggle(dragToCursor, "Drag To Cursor", 230); + GUILayout.Space(3); + autoFollowCursor = DrawToggle(autoFollowCursor, $"Magnet Cursor ({bindMagnetCursor})", 230); + GUILayout.Space(3); + noClip = DrawToggle(noClip, "True NoClip", 230); + + GUILayout.EndVertical(); + } + +private void DrawRolesCompact() + { + GUILayout.BeginVertical(); + DrawMenuSectionHeader("ROLE TOOLS"); + + GUIStyle roleMidStyle = new GUIStyle(btnStyle) + { + fontStyle = FontStyle.Bold, + normal = { background = null, textColor = GetMenuAccentColor() }, + alignment = TextAnchor.MiddleCenter + }; + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("<", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) + { + fakeRoleIdx--; + if (fakeRoleIdx < 0) fakeRoleIdx = forceRoleOptions.Length - 1; + } + GUILayout.Label(forceRoleOptions[fakeRoleIdx].ToString(), roleMidStyle, GUILayout.ExpandWidth(true), GUILayout.Height(24)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(28), GUILayout.Height(24))) + { + fakeRoleIdx++; + if (fakeRoleIdx >= forceRoleOptions.Length) fakeRoleIdx = 0; + } + if (GUILayout.Button("Set", activeTabStyle, GUILayout.Width(42), GUILayout.Height(24))) + RoleManager.Instance?.SetRole(PlayerControl.LocalPlayer, forceRoleOptions[fakeRoleIdx]); + GUILayout.EndHorizontal(); + + GUILayout.Space(8); + DrawRoleBuffSubTabs(); + GUILayout.Space(8); + + if (currentRoleBuffSubTab == 0) DrawNonHostRoleBuffs(230); + else DrawHostRoleBuffs(230); + + GUILayout.EndVertical(); + } + +private void DrawRoleBuffSubTabs() + { + GUILayout.BeginHorizontal(); + if (GUILayout.Button("NON HOST", currentRoleBuffSubTab == 0 ? activeSubTabStyle : subTabStyle, GUILayout.Height(22))) + currentRoleBuffSubTab = 0; + if (GUILayout.Button("HOST", currentRoleBuffSubTab == 1 ? activeSubTabStyle : subTabStyle, GUILayout.Height(22))) + currentRoleBuffSubTab = 1; + GUILayout.EndHorizontal(); + } + +private void DrawNonHostRoleBuffs(int width) + { + DrawMenuSectionHeader("IMPOSTOR"); + killReach = DrawToggle(killReach, "Kill Reach", width); + GUILayout.Space(3); + killAnyone = DrawToggle(killAnyone, "Kill Anyone", width); + GUILayout.Space(3); + allowTasksAsImpostor = DrawToggle(allowTasksAsImpostor, "Allow Tasks (Imp)", width); + GUILayout.Space(3); + spamReportBodies = DrawToggle(spamReportBodies, "Spam Report Bodies", width); + + GUILayout.Space(8); + DrawMenuSectionHeader("SHAPESHIFTER"); + NoShapeshiftAnim = DrawToggle(NoShapeshiftAnim, "No Ss Animation", width); + GUILayout.Space(3); + endlessSsDuration = DrawToggle(endlessSsDuration, "Endless Ss Duration", width); + + GUILayout.Space(8); + DrawMenuSectionHeader("TRACKER"); + EndlessTracking = DrawToggle(EndlessTracking, "Endless Tracking", width); + GUILayout.Space(3); + NoTrackingCooldown = DrawToggle(NoTrackingCooldown, "No Track Cooldown", width); + + GUILayout.Space(8); + DrawMenuSectionHeader("ENGINEER"); + endlessVentTime = DrawToggle(endlessVentTime, "Endless Vent Time", width); + GUILayout.Space(3); + noVentCooldown = DrawToggle(noVentCooldown, "No Vent Cooldown", width); + GUILayout.Space(3); + unlockVents = DrawToggle(unlockVents, "Unlock Vents", width); + GUILayout.Space(3); + walkInVents = DrawToggle(walkInVents, "Walk In Vents", width); + GUILayout.Space(3); + noMapCooldowns = DrawToggle(noMapCooldowns, "No Map Cooldowns", width); + + GUILayout.Space(8); + DrawMenuSectionHeader("SCIENTIST"); + endlessBattery = DrawToggle(endlessBattery, "Endless Battery", width); + GUILayout.Space(3); + noVitalsCooldown = DrawToggle(noVitalsCooldown, "No Vitals Cooldown", width); + + GUILayout.Space(8); + DrawMenuSectionHeader("DETECTIVE"); + UnlimitedInterrogateRange = DrawToggle(UnlimitedInterrogateRange, "Interrogate Reach", width); + + GUILayout.Space(8); + DrawMenuSectionHeader("GLOBAL"); + roleBuffImmortality = DrawToggle(roleBuffImmortality, "Immortality", width); + } + +private void DrawHostRoleBuffs(int width) + { + DrawMenuSectionHeader("IMPOSTOR HOST"); + killAuraHostOnly = DrawToggle(killAuraHostOnly, "Kill Aura", width); + GUILayout.Space(3); + noKillCooldownHostOnly = DrawToggle(noKillCooldownHostOnly, "Kill Cooldown 0", width); + GUILayout.Space(3); + killWhileVanishedHostOnly = DrawToggle(killWhileVanishedHostOnly, "Kill While Vanished", width); + } + +private void DrawChatSettingsCompact() + { + GUILayout.BeginVertical(); + DrawMenuSectionHeader(L("CHAT SETTINGS", "НАСТРОЙКИ ЧАТА")); + + alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 230); + GUILayout.Space(3); + readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 230); + GUILayout.Space(4); + DrawGhostChatColorControl(230f); + GUILayout.Space(3); + enableExtendedChat = DrawToggle(enableExtendedChat, L("Extended Chat", "Длинный чат"), 230); + GUILayout.Space(3); + enableFastChat = DrawToggle(enableFastChat, L("Fast Chat", "Быстрый чат"), 230); + GUILayout.Space(3); + allowLinksAndSymbols = DrawToggle(allowLinksAndSymbols, L("Unlock Extra Characters", "Все символы"), 230); + GUILayout.Space(3); + enableSpellCheck = DrawToggle(enableSpellCheck, L("Spell Check", "Проверка орфографии"), 230); + + GUILayout.Space(8); + DrawMenuSectionHeader(L("CHAT UTILITY", "УТИЛИТЫ ЧАТА")); + enableChatHistory = DrawToggle(enableChatHistory, L("Chat History", "История чата"), 230); + GUILayout.Space(3); + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("History:", "История:")} {chatHistoryLimit}", toggleLabelStyle, GUILayout.MinWidth(106), GUILayout.ExpandWidth(false)); + chatHistoryLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(chatHistoryLimit, 5f, 80f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)), 5, 80); + TrimChatHistoryToLimit(); + GUILayout.EndHorizontal(); + GUILayout.Space(3); + enableClipboard = DrawToggle(enableClipboard, L("Clipboard", "Буфер обмена"), 230); + GUILayout.Space(3); + enableChatMessageDoubleClickCopy = DrawToggle(enableChatMessageDoubleClickCopy, L("Double-click Copy", "Дабл-клик копирует"), 230); + GUILayout.Space(3); + enableChatNameColorCopy = DrawToggle(enableChatNameColorCopy, L("Copy Name", "Копировать ник"), 230); + GUILayout.Space(3); + enableChatLog = DrawToggle(enableChatLog, L("Save Chat Log", "Сохранять лог чата"), 230); + GUILayout.Space(3); + enableChatDarkMode = DrawToggle(enableChatDarkMode, L("Dark Chat Theme", "Темная тема чата"), 230); + GUILayout.Space(3); + if (enableChatDarkMode && GUILayout.Button(L("Turn Off Dark Chat", "Выключить темный чат"), btnStyle, GUILayout.Height(24))) + { + enableChatDarkMode = false; + SaveConfig(); + } + GUILayout.Space(3); + enableColorCommand = DrawToggle(enableColorCommand, L("Enable /color", "Разрешить /color"), 230); + GUILayout.Space(3); + blockFortegreenChat = DrawToggle(blockFortegreenChat, L("Block Fortegreen", "Блок Fortegreen"), 230); + GUILayout.Space(3); + blockRainbowChat = DrawToggle(blockRainbowChat, L("Block Rainbow", "Блок Rainbow"), 230); + + GUILayout.Space(8); + DrawMenuSectionHeader(L("CHAT SENDER", "ОТПРАВКА ЧАТА")); + GUILayout.Space(4); + + GUIStyle fieldStyle = new GUIStyle(GUI.skin.textField) + { + fontSize = 12, + alignment = TextAnchor.MiddleLeft, + clipping = TextClipping.Clip + }; + fieldStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); + + Rect chatInputRect = GUILayoutUtility.GetRect(10f, 32f, GUILayout.ExpandWidth(true), GUILayout.Height(32)); + GUI.Box(chatInputRect, string.Empty, fieldStyle); + + string drawText = string.IsNullOrEmpty(customChatMessage) + ? L("Type a message...", "Введите сообщение...") + : customChatMessage; + if (customChatInputFocused && (Time.unscaledTime % 1f) < 0.5f) drawText += "|"; + + GUIStyle chatInputTextStyle = new GUIStyle(GUI.skin.label) + { + alignment = TextAnchor.MiddleLeft, + clipping = TextClipping.Clip, + richText = false, + fontSize = 12 + }; + chatInputTextStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); + GUI.Label(new Rect(chatInputRect.x + 9f, chatInputRect.y + 3f, chatInputRect.width - 18f, chatInputRect.height - 6f), drawText, chatInputTextStyle); + + Event e = Event.current; + if (e != null) + { + if (e.type == EventType.MouseDown) + { + customChatInputFocused = chatInputRect.Contains(e.mousePosition); + if (customChatInputFocused) e.Use(); + } + else if (customChatInputFocused && e.type == EventType.KeyDown) + { + if (HandleClipboardShortcut(e, ref customChatMessage, 120)) + { + } + else if (e.keyCode == KeyCode.Backspace) + { + if (!string.IsNullOrEmpty(customChatMessage)) + customChatMessage = customChatMessage.Substring(0, customChatMessage.Length - 1); + e.Use(); + } + else if (e.keyCode == KeyCode.Escape) + { + customChatInputFocused = false; + e.Use(); + } + else if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) + { + TrySendCustomChatMessage(customChatMessage); + e.Use(); + } + else if (!char.IsControl(e.character)) + { + if (customChatMessage == null) customChatMessage = string.Empty; + if (customChatMessage.Length < 120) customChatMessage += e.character; + e.Use(); + } + } + } + + GUILayout.Space(6); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(L("Send", "Отправить"), btnStyle, GUILayout.Height(28))) + TrySendCustomChatMessage(customChatMessage); + GUILayout.Space(6); + string spamBtnText = customChatSpamEnabled ? L("Spam ON", "Спам ВКЛ") : L("Spam OFF", "Спам ВЫКЛ"); + if (GUILayout.Button(spamBtnText, customChatSpamEnabled ? activeTabStyle : btnStyle, GUILayout.Height(28))) + customChatSpamEnabled = !customChatSpamEnabled; + GUILayout.EndHorizontal(); + + GUILayout.Space(6); + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Delay:", "Задержка:")} {Mathf.Round(customChatSpamDelay * 10f) / 10f}s", toggleLabelStyle, GUILayout.Width(92)); + customChatSpamDelay = GUILayout.HorizontalSlider(customChatSpamDelay, 0.5f, 10f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + } + +private void DrawGhostChatColorControl(float width) + { + GUILayout.BeginHorizontal(GUILayout.Width(width)); + GUILayout.Label(L("Ghost Chat:", "Ghost Chat:"), new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Width(74), GUILayout.Height(24)); + if (DrawPseudoInputButton(ghostChatColorHex, isEditingGhostChatColor, 24f, 16)) + { + isEditingGhostChatColor = !isEditingGhostChatColor; + if (isEditingGhostChatColor) + { + ghostChatColorHex = FilterHexInput(ghostChatColorHex, 7); + } + isEditingName = false; + isEditingLevel = false; + isEditingFriendCode = false; + isEditingLocalFriendCode = false; + isEditingBan = false; + ResetAllBindWaits(); + } + if (GUILayout.Button(L("Apply", "OK"), btnStyle, GUILayout.Width(48), GUILayout.Height(24))) + { + isEditingGhostChatColor = false; + ghostChatColorHex = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); + SaveConfig(); + } + GUILayout.EndHorizontal(); + + string previewHex = GetGhostChatColorHex(); + GUILayout.Label($"{L("Preview ghost chat color", "Пример цвета чата призраков")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = false, clipping = TextClipping.Clip }, GUILayout.Width(width), GUILayout.Height(16f)); + } + +private void DrawPlayerMovement() + { + GUILayout.BeginVertical(boxStyle); + try + { + GUILayout.Label("MOVEMENT & TELEPORT", headerStyle); + + GUILayout.BeginHorizontal(); + try + { + GUILayout.Label($"Engine Speed: {Mathf.Round(engineSpeed)}x", GUILayout.Width(130)); + engineSpeed = GUILayout.HorizontalSlider(engineSpeed, 1f, 555f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + GUILayout.Space(10); + if (GUILayout.Button("Reset", btnStyle, GUILayout.Width(50), GUILayout.Height(20))) engineSpeed = 1f; + } + finally { GUILayout.EndHorizontal(); } + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + try + { + GUILayout.Label($"Walk Speed: {Mathf.Round(walkSpeed)}x", GUILayout.Width(130)); + walkSpeed = GUILayout.HorizontalSlider(walkSpeed, 1f, 30f, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + GUILayout.Space(10); + if (GUILayout.Button("Reset", btnStyle, GUILayout.Width(50), GUILayout.Height(20))) walkSpeed = 1f; + } + finally { GUILayout.EndHorizontal(); } + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + try + { + tpToCursor = DrawToggle(tpToCursor, "TP To Cursor", 160); + dragToCursor = DrawToggle(dragToCursor, "Drag To Cursor", 160); + GUILayout.FlexibleSpace(); + } + finally { GUILayout.EndHorizontal(); } + + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + try + { + autoFollowCursor = DrawToggle(autoFollowCursor, $"Magnet Cursor ({bindMagnetCursor})", 160); + noClip = DrawToggle(noClip, "True NoClip", 160); + GUILayout.FlexibleSpace(); + } + finally { GUILayout.EndHorizontal(); } + } + finally { GUILayout.EndVertical(); } + } + +private void SmartEndGame(string outcome) + { + if (GameManager.Instance == null || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + + bool isHns = GameManager.Instance.IsHideAndSeek(); + int reasonCode = 0; + + switch (outcome) + { + case "CrewWin": reasonCode = isHns ? 7 : 0; break; + case "ImpWin": reasonCode = isHns ? 8 : 3; break; + case "ImpDisconnect": + case "HnsImpDisconnect": reasonCode = 5; break; + } + + bool tempBlock = neverEndGame; + neverEndGame = false; + GameManager.Instance.RpcEndGame((GameOverReason)reasonCode, false); + neverEndGame = tempBlock; + } + +private static string SanitizeSpoofFriendCode(string input) + { + if (string.IsNullOrWhiteSpace(input)) return ""; + + string clean = ""; + foreach (char c in input.ToLowerInvariant()) + { + if (char.IsWhiteSpace(c)) break; + if (char.IsLetterOrDigit(c)) clean += c; + if (clean.Length >= 10) break; + } + return clean; + } + +private static string SanitizeHexColor(string input, string fallback) + { + string value = (input ?? string.Empty).Trim(); + if (value.StartsWith("#")) value = value.Substring(1); + + string clean = ""; + foreach (char c in value) + { + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) + { + clean += char.ToUpperInvariant(c); + if (clean.Length >= 6) break; + } + } + + return clean.Length == 6 ? "#" + clean : fallback; + } +} +} diff --git a/features/KeybindPanel.cs b/features/KeybindPanel.cs new file mode 100644 index 0000000..1d3f993 --- /dev/null +++ b/features/KeybindPanel.cs @@ -0,0 +1,918 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +private static string FilterHexInput(string input, int maxChars) + { + string value = (input ?? string.Empty).Trim(); + string clean = ""; + bool hasHash = false; + + foreach (char c in value) + { + if (c == '#' && clean.Length == 0 && !hasHash) + { + hasHash = true; + clean = "#"; + continue; + } + + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) + { + if (clean.Length == 0) clean = "#"; + clean += char.ToUpperInvariant(c); + if (clean.Length >= maxChars) break; + } + } + + return clean.Length == 0 ? "#" : clean; + } + +public static string GetGhostChatColorHex() + { + if (isEditingGhostChatColor) + { + return SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); + } + + ghostChatColorHex = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); + return ghostChatColorHex; + } + +private static string BuildLocalNameRenderText(string input) + { + string value = (input ?? string.Empty).Replace("\r\n", "\n").Replace('\r', '\n'); + if (string.IsNullOrWhiteSpace(value)) return string.Empty; + + string trimmed = value.TrimStart(); + if (trimmed.StartsWith("shimmer:", StringComparison.OrdinalIgnoreCase)) + return ApplyMenuShimmer(trimmed.Substring("shimmer:".Length).TrimStart()); + + Match hexPrefix = Regex.Match(trimmed, @"^#([0-9A-Fa-f]{6})(.*)$"); + if (hexPrefix.Success) + { + string payload = hexPrefix.Groups[2].Value.TrimStart(' ', ':', '|', '-', '>'); + if (!string.IsNullOrEmpty(payload)) + return $"{payload}"; + } + + return value; + } + +private static string GetDisplayedFriendCode(NetworkedPlayerInfo data, string emptyValue = "Hidden") + { + if (data == null) return emptyValue; + + string value = data.FriendCode; + if (enableLocalFriendCodeSpoof && + PlayerControl.LocalPlayer != null && + data.PlayerId == PlayerControl.LocalPlayer.PlayerId && + !string.IsNullOrEmpty(localFriendCodeInput)) + { + value = localFriendCodeInput; + } + + return string.IsNullOrEmpty(value) ? emptyValue : value; + } + +public static bool PrepareLocalFriendCodeForSerialize(NetworkedPlayerInfo data, out string restoreValue) + { + restoreValue = null; + try + { + if (!enableLocalFriendCodeSpoof || enableFriendCodeSpoof) return false; + if (data == null || PlayerControl.LocalPlayer == null || data.PlayerId != PlayerControl.LocalPlayer.PlayerId) return false; + + restoreValue = data.FriendCode; + TrySetStringMember(data, "FriendCode", originalLocalFriendCode ?? string.Empty); + return true; + } + catch + { + restoreValue = null; + return false; + } + } + +public static void RestoreLocalFriendCodeAfterSerialize(NetworkedPlayerInfo data, string restoreValue) + { + try + { + if (data == null || restoreValue == null) return; + TrySetStringMember(data, "FriendCode", restoreValue); + } + catch { } + } + +private static string FormatInputPreview(string value, bool editing, int maxChars = 52) + { + string preview = value ?? string.Empty; + if (preview.Length > maxChars) + preview = "..." + preview.Substring(preview.Length - (maxChars - 3)); + + if (editing) preview += "_"; + return string.IsNullOrEmpty(preview) ? " " : preview; + } + +private static bool HandleClipboardShortcut(Event e, ref string target, int maxLength = -1) + { + if (e == null || e.type != EventType.KeyDown) return false; + + bool ctrlOrCmd = e.control || e.command; + bool pasteAlt = e.shift && e.keyCode == KeyCode.Insert; + if (!ctrlOrCmd && !pasteAlt) return false; + + target ??= string.Empty; + + if (ctrlOrCmd && e.keyCode == KeyCode.C) + { + GUIUtility.systemCopyBuffer = target; + e.Use(); + return true; + } + + if (ctrlOrCmd && e.keyCode == KeyCode.X) + { + GUIUtility.systemCopyBuffer = target; + target = string.Empty; + e.Use(); + return true; + } + + if ((ctrlOrCmd && e.keyCode == KeyCode.V) || pasteAlt) + { + string paste = (GUIUtility.systemCopyBuffer ?? string.Empty).Replace("\r\n", "\n").Replace('\r', '\n'); + if (paste.Length > 0) + { + target += paste; + if (maxLength >= 0 && target.Length > maxLength) + target = target.Substring(0, maxLength); + } + e.Use(); + return true; + } + + return false; + } + +private static bool IsBrokenFriendCode(string friendCode) + { + if (string.IsNullOrWhiteSpace(friendCode)) return true; + if (friendCode.Contains(" ")) return true; + if (friendCode.Contains("<") || friendCode.Contains(">")) return true; + if (!friendCode.Contains("#")) return true; + + string[] parts = friendCode.Split('#'); + if (parts.Length != 2) return true; + if (string.IsNullOrWhiteSpace(parts[0]) || string.IsNullOrWhiteSpace(parts[1])) return true; + if (parts[0].Length < 3 || parts[0].Length > 16) return true; + if (parts[1].Length < 3 || parts[1].Length > 8) return true; + if (!parts[0].All(char.IsLetterOrDigit)) return true; + if (!parts[1].All(char.IsDigit)) return true; + + return false; + } + +private void TryAutoBanBrokenFriendCodeTick() + { + try + { + if (!autoBanBrokenFriendCode) + { + brokenFcScanTimer = 0f; + brokenFcPunishedOwners.Clear(); + return; + } + + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) + { + brokenFcScanTimer = 0f; + return; + } + + if (PlayerControl.AllPlayerControls.Count <= 1) + brokenFcPunishedOwners.Clear(); + + brokenFcScanTimer += Time.deltaTime; + if (brokenFcScanTimer < 0.8f) return; + brokenFcScanTimer = 0f; + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; + + string fc = pc.Data.FriendCode ?? ""; + if (!IsBrokenFriendCode(fc)) continue; + + int owner = (int)pc.OwnerId; + if (brokenFcPunishedOwners.Contains(owner)) continue; + brokenFcPunishedOwners.Add(owner); + + string name = string.IsNullOrWhiteSpace(pc.Data.PlayerName) ? "Unknown" : pc.Data.PlayerName; + string puid = "Unknown"; + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(pc); + if (client != null) puid = GetClientPuid(client); + } + catch { } + + AddToBanList(string.IsNullOrWhiteSpace(fc) ? "Unknown" : fc, puid, name, "Broken FriendCode"); + AmongUsClient.Instance.KickPlayer(owner, true); + ShowNotification($"[ANTICHEAT] {name} banned: broken FC"); + } + } + catch { } + } + +private void TryAutoKickLowLevelTick() + { + try + { + if (!autoKickLowLevelEnabled) + { + lowLevelKickScanTimer = 0f; + lowLevelKickPunishedOwners.Clear(); + return; + } + + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) + { + lowLevelKickScanTimer = 0f; + return; + } + + if (PlayerControl.AllPlayerControls.Count <= 1) + lowLevelKickPunishedOwners.Clear(); + + lowLevelKickScanTimer += Time.deltaTime; + if (lowLevelKickScanTimer < 0.8f) return; + lowLevelKickScanTimer = 0f; + + int minLevel = Mathf.Clamp(autoKickMinLevel, 1, 300); + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; + + int level = 1; + try + { + uint rawLevel = pc.Data.PlayerLevel; + if (rawLevel != uint.MaxValue && rawLevel < 10000) level = (int)rawLevel + 1; + } + catch { } + + if (level >= minLevel) continue; + + int owner = (int)pc.OwnerId; + if (lowLevelKickPunishedOwners.Contains(owner)) continue; + lowLevelKickPunishedOwners.Add(owner); + + string name = string.IsNullOrWhiteSpace(pc.Data.PlayerName) ? "Unknown" : pc.Data.PlayerName; + AmongUsClient.Instance.KickPlayer(owner, false); + ShowNotification($"[LEVEL KICK] {name} kicked: level {level} < {minLevel}"); + } + } + catch { } + } + +private static void TryAutoGhostAfterStartTick() + { + try + { + bool gameStarted = AmongUsClient.Instance != null && AmongUsClient.Instance.IsGameStarted; + if (!gameStarted) + { + wasGameStartedForAutoGhost = false; + autoGhostAppliedThisGame = false; + return; + } + + if (!wasGameStartedForAutoGhost) + { + wasGameStartedForAutoGhost = true; + autoGhostAppliedThisGame = false; + } + + if (!autoGhostAfterStart || autoGhostAppliedThisGame || PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) + return; + + if (PlayerControl.LocalPlayer.Data.IsDead) + { + autoGhostAppliedThisGame = true; + return; + } + + MakePlayerGhost(PlayerControl.LocalPlayer, false, false); + autoGhostAppliedThisGame = true; + ShowNotification($"[AUTO HOST] {L("Auto ghost applied.", "Авто-призрак применен.")}"); + } + catch { } + } + +private static void EnsurePlatformBanListLoaded() + { + try + { + if (string.IsNullOrEmpty(platformBanListPath)) + platformBanListPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ElysiumPlatformBanList.txt"); + + if (!System.IO.File.Exists(platformBanListPath)) + System.IO.File.WriteAllText(platformBanListPath, "# One custom platform token per line. Matching PlatformName values are host-banned when enabled.\n# Example: github\n"); + + if (Time.unscaledTime < platformBanListNextLoadAt) return; + platformBanListNextLoadAt = Time.unscaledTime + 3f; + + customPlatformBanTokens.Clear(); + foreach (string rawLine in System.IO.File.ReadAllLines(platformBanListPath)) + { + string line = rawLine.Trim(); + if (line.Length == 0 || line.StartsWith("#")) continue; + customPlatformBanTokens.Add(line); + } + } + catch { } + } + +private static bool IsCustomPlatformName(ClientData client, out string platformName) + { + platformName = ""; + try + { + if (client == null || client.PlatformData == null) return false; + platformName = client.PlatformData.PlatformName ?? ""; + if (string.IsNullOrWhiteSpace(platformName)) return false; + + string enumName = client.PlatformData.Platform.ToString(); + if (platformName.Equals("TESTNAME", StringComparison.OrdinalIgnoreCase)) return false; + return !platformName.Equals(enumName, StringComparison.OrdinalIgnoreCase) && + !platformName.Equals(GetPlatform(client), StringComparison.OrdinalIgnoreCase); + } + catch { } + + return false; + } + +private static bool IsInvalidPlatformData(ClientData client, out string reason) + { + reason = ""; + try + { + if (client == null || client.PlatformData == null) return false; + + var platform = client.PlatformData; + string pName = platform.PlatformName ?? ""; + ulong xuid = platform.XboxPlatformId; + ulong psid = platform.PsnPlatformId; + bool isValid = true; + + switch (platform.Platform) + { + case Platforms.StandaloneEpicPC: + case Platforms.StandaloneSteamPC: + case Platforms.StandaloneMac: + case Platforms.StandaloneItch: + case Platforms.IPhone: + case Platforms.Android: + isValid = (pName == "TESTNAME" && xuid == 0 && psid == 0); + break; + case Platforms.StandaloneWin10: + isValid = (pName == "TESTNAME" && xuid != 0 && psid == 0); + break; + case Platforms.Xbox: + isValid = (pName != "TESTNAME" && pName.Length >= 3 && xuid != 0 && psid == 0); + break; + case Platforms.Playstation: + isValid = (pName != "TESTNAME" && xuid == 0 && psid != 0); + break; + case Platforms.Switch: + isValid = (pName != "TESTNAME" && xuid == 0 && psid == 0); + break; + } + + if (!isValid) + { + reason = $"Platform Spoof detected ({platform.Platform})"; + return true; + } + } + catch { } + + return false; + } + +private static bool MatchesPlatformBanTxt(ClientData client, out string platformName, out string matchedToken) + { + platformName = ""; + matchedToken = ""; + EnsurePlatformBanListLoaded(); + + if (!IsCustomPlatformName(client, out platformName) || customPlatformBanTokens.Count == 0) + return false; + + foreach (string token in customPlatformBanTokens) + { + if (platformName.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0) + { + matchedToken = token; + return true; + } + } + + return false; + } + +private static void HostBanForPlatform(PlayerControl player, string reason) + { + try + { + if (player == null || player == PlayerControl.LocalPlayer || player.Data == null || + AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + return; + + int owner = (int)player.OwnerId; + if (platformSpoofPunishedOwners.Contains(owner)) return; + platformSpoofPunishedOwners.Add(owner); + + string name = string.IsNullOrWhiteSpace(player.Data.PlayerName) ? "Unknown" : player.Data.PlayerName; + string fc = string.IsNullOrWhiteSpace(player.Data.FriendCode) ? "Unknown" : player.Data.FriendCode; + string puid = "Unknown"; + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(player); + if (client != null) puid = GetClientPuid(client); + } + catch { } + + AddToBanList(fc, puid, name, reason); + AmongUsClient.Instance.KickPlayer(owner, true); + ShowNotification($"[PLATFORM BAN] {name}: {reason}"); + } + catch { } + } + +private static void TryAutoBanCustomPlatformsTick() + { + try + { + if ((!autoBanPlatformSpoof && !banCustomPlatformsFromTxt) || + AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) + { + platformBanScanTimer = 0f; + return; + } + + platformBanScanTimer += Time.deltaTime; + if (platformBanScanTimer < 1f) return; + platformBanScanTimer = 0f; + + foreach (PlayerControl pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected) continue; + + ClientData client = null; + try { client = AmongUsClient.Instance.GetClientFromCharacter(pc); } catch { } + if (client == null) continue; + + if (banCustomPlatformsFromTxt && MatchesPlatformBanTxt(client, out string platformName, out string token)) + { + HostBanForPlatform(pc, $"Custom platform TXT match '{token}' ({platformName})"); + continue; + } + + if (autoBanPlatformSpoof && IsInvalidPlatformData(client, out string reason)) + HostBanForPlatform(pc, reason); + } + } + catch { } + } + +private void DrawSelfSpoof() + { + GUILayout.BeginVertical(); + GUIStyle greenHeader = new GUIStyle(headerStyle); + greenHeader.normal.textColor = GetMenuAccentColor(); + GUILayout.Label("ACCOUNT SPOOFER", greenHeader); + + GUILayout.Space(4); + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader("LEVEL SPOOF"); + bool newLevelSpoof = DrawToggle(enableLevelSpoof, enableLevelSpoof ? "Level Spoof: ON" : "Level Spoof: OFF", 180); + if (newLevelSpoof != enableLevelSpoof) + { + enableLevelSpoof = newLevelSpoof; + if (enableLevelSpoof && uint.TryParse(spoofLevelString, out uint toggleParsedLvl)) + ApplyLevelSpoofValue(toggleParsedLvl); + else if (!enableLevelSpoof) + RestoreLevelSpoofDefault(); + SaveConfig(); + } + GUILayout.Space(4); + GUILayout.BeginHorizontal(); + GUILayout.Label("Fake Level", btnStyle, GUILayout.Width(86), GUILayout.Height(28)); + if (DrawPseudoInputButton(spoofLevelString, isEditingLevel, 28f, 32)) + { + isEditingLevel = !isEditingLevel; + isEditingName = false; + isEditingFriendCode = false; + isEditingLocalFriendCode = false; + isEditingGhostChatColor = false; + } + if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) + { + isEditingLevel = false; + if (uint.TryParse(spoofLevelString, out uint parsedLvl)) + { + enableLevelSpoof = true; + ApplyLevelSpoofValue(parsedLvl); + } + SaveConfig(); + } + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(6); + + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader("LOCAL NAME SPOOF"); + bool newLocalNameToggle = DrawToggle(enableLocalNameSpoof, "Keep Local Nick", 180); + if (newLocalNameToggle != enableLocalNameSpoof) + { + enableLocalNameSpoof = newLocalNameToggle; + if (enableLocalNameSpoof) ApplyLocalNameSelf(customNameInput, false); + else RestoreLocalNameSelf(); + SaveConfig(); + } + GUILayout.Space(2); + GUILayout.BeginHorizontal(); + GUILayout.Label("Nick", btnStyle, GUILayout.Width(58), GUILayout.Height(28)); + if (DrawPseudoInputButton(customNameInput, isEditingName, 28f, 54)) + { + isEditingName = !isEditingName; + isEditingLevel = false; + isEditingFriendCode = false; + isEditingLocalFriendCode = false; + isEditingGhostChatColor = false; + } + if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) + { + isEditingName = false; + ApplyLocalNameSelf(customNameInput, true); + SaveConfig(); + } + GUILayout.EndHorizontal(); + DrawClippedHint("Local only: no RPC broadcast. Supports shimmer:Text, #68B6E7Text and raw rich text."); + GUILayout.EndVertical(); + + GUILayout.Space(6); + + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader("LOCAL FAKE FRIEND CODE"); + bool newLocalFcToggle = DrawToggle(enableLocalFriendCodeSpoof, "Keep Fake FC Local", 180); + if (newLocalFcToggle != enableLocalFriendCodeSpoof) + { + enableLocalFriendCodeSpoof = newLocalFcToggle; + if (enableLocalFriendCodeSpoof) ApplyLocalFriendCodeSelf(localFriendCodeInput, false); + else RestoreLocalFriendCodeSelf(); + SaveConfig(); + } + GUILayout.Space(2); + GUILayout.BeginHorizontal(); + GUILayout.Label("Fake FC", btnStyle, GUILayout.Width(58), GUILayout.Height(28)); + if (DrawPseudoInputButton(localFriendCodeInput, isEditingLocalFriendCode, 28f, 54)) + { + isEditingLocalFriendCode = !isEditingLocalFriendCode; + isEditingName = false; + isEditingLevel = false; + isEditingFriendCode = false; + isEditingGhostChatColor = false; + } + if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) + { + isEditingLocalFriendCode = false; + ApplyLocalFriendCodeSelf(localFriendCodeInput, true); + SaveConfig(); + } + GUILayout.EndHorizontal(); + DrawClippedHint("Local only: any text, any symbols. Used in this client UI only."); + GUILayout.EndVertical(); + + GUILayout.Space(6); + + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader("FRIEND CODE SPOOF"); + enableFriendCodeSpoof = DrawToggle(enableFriendCodeSpoof, "Enable FC Spoof", 180); + GUILayout.Space(2); + GUILayout.BeginHorizontal(); + if (DrawPseudoInputButton(spoofFriendCodeInput, isEditingFriendCode, 28f, 54)) + { + isEditingFriendCode = !isEditingFriendCode; + isEditingName = false; + isEditingLevel = false; + isEditingLocalFriendCode = false; + isEditingGhostChatColor = false; + } + if (GUILayout.Button("Apply", btnStyle, GUILayout.Width(56), GUILayout.Height(28))) + { + isEditingFriendCode = false; + spoofFriendCodeInput = SanitizeSpoofFriendCode(spoofFriendCodeInput); + SaveConfig(); + } + GUILayout.EndHorizontal(); + DrawClippedHint("Guest-style code: <=10, [a-z0-9], no spaces"); + GUILayout.EndVertical(); + + GUILayout.Space(6); + + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader("PLATFORM SPOOF"); + if (GUILayout.Button("Spoof Platform", enablePlatformSpoof ? activeTabStyle : btnStyle, GUILayout.Height(26))) + { + enablePlatformSpoof = !enablePlatformSpoof; + SaveConfig(); + } + GUILayout.Space(2); + string hexColor = GetMenuAccentHex(); + GUILayout.Label($"Platform: {platformNames[currentPlatformIndex]}", new GUIStyle(toggleLabelStyle) { fontSize = 12, richText = true }, GUILayout.Height(23)); + int newPlatIdx = (int)GUILayout.HorizontalSlider(currentPlatformIndex, 0, platformNames.Length - 1, sliderStyle, sliderThumbStyle, GUILayout.ExpandWidth(true)); + if (newPlatIdx != currentPlatformIndex) + { + currentPlatformIndex = newPlatIdx; + SaveConfig(); + } + GUILayout.EndVertical(); + + GUILayout.Space(8); + DrawMenuSectionHeader("TASKS"); + if (GUILayout.Button("Complete My Tasks", btnStyle, GUILayout.Height(30))) + { + if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.myTasks != null) + foreach (var task in PlayerControl.LocalPlayer.myTasks) + if (task != null && !task.IsComplete) PlayerControl.LocalPlayer.RpcCompleteTask((uint)task.Id); + } + GUILayout.EndVertical(); + } + +private void DrawVisualsTab() + { + GUILayout.BeginHorizontal(); + for (int i = 0; i < visualsSubTabs.Length; i++) + if (GUILayout.Button(visualsSubTabs[i], currentVisualsSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) + { currentVisualsSubTab = i; scrollPosition = Vector2.zero; } + GUILayout.EndHorizontal(); + GUILayout.Space(8); + if (currentVisualsSubTab == 0) DrawVisualsInGame(); + else if (currentVisualsSubTab == 1) DrawOutfitsTab(); + } + +[HarmonyPatch(typeof(PlayerBanData), nameof(PlayerBanData.BanPoints), MethodType.Setter)] + public static class RemoveDisconnectPenalty_Patch + { + public static bool Prefix(PlayerBanData __instance, ref float value) + { + if (!ElysiumModMenuGUI.removePenalty) return true; + if (AmongUsClient.Instance == null || AmongUsClient.Instance.NetworkMode != NetworkModes.OnlineGame) + return true; + + value = 0f; + return false; + } + } + +[HarmonyPatch(typeof(DisconnectPopup), nameof(DisconnectPopup.DoShow))] + public static class DisconnectPopup_CopyRoomCode_Patch + { + public static void Postfix(DisconnectPopup __instance) + { + try + { + if (!ElysiumModMenuGUI.TryCopyRoomCodeToClipboard(false)) return; + if (__instance != null && __instance._textArea != null) + __instance.SetText(__instance._textArea.text + "\n\nRoom code copied to clipboard"); + } + catch { } + } + } + +[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.ExitGame))] + public static class AmongUsClient_ExitGame_CopyRoomCode_Patch + { + public static void Prefix() + { + ElysiumModMenuGUI.TryCopyRoomCodeToClipboard(true); + } + } + +[HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Start))] + public static class ShowLobbyTimer_Patch + { + public static void Postfix(GameStartManager __instance) + { + if (!ElysiumModMenuGUI.alwaysShowLobbyTimer) return; + + if (__instance == null || GameData.Instance == null || AmongUsClient.Instance == null) return; + if (AmongUsClient.Instance.NetworkMode == NetworkModes.LocalGame || !AmongUsClient.Instance.AmHost) return; + + if (HudManager.Instance != null) + { + HudManager.Instance.ShowLobbyTimer(600); + } + } + } + +public static bool IsCursorOverMenu() + { + try + { + if (!showMenu || !hardMenu) return false; + Vector2 guiPos = new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y); + return windowRect.Contains(guiPos); + } + catch { return false; } + } + +public static bool IsCursorOverVisibleMenu() + { + try + { + if (!showMenu) return false; + Vector2 guiPos = new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y); + return windowRect.Contains(guiPos); + } + catch { return false; } + } + +private static bool IsChatOpenForZoomBlock() + { + try + { + ChatController chat = HudManager.Instance?.Chat; + return chat != null && chat.IsOpenOrOpening; + } + catch { return false; } + } + +private static bool IsCameraZoomScrollAllowed() + { + try + { + if (IsCursorOverVisibleMenu()) return false; + if (IsChatOpenForZoomBlock()) return false; + if (MeetingHud.Instance != null) return false; + if (Minigame.Instance != null) return false; + if (UnityEngine.EventSystems.EventSystem.current != null && + UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject()) + return false; + if (UnityEngine.Object.FindObjectOfType() != null) return false; + if (PlayerCustomizationMenu.Instance != null) return false; + if (FriendsListUI.Instance != null && FriendsListUI.Instance.IsOpen) return false; + if (LobbyBehaviour.Instance != null && GameStartManager.Instance != null) + { + try + { + if (GameStartManager.Instance.LobbyInfoPane != null && + GameStartManager.Instance.LobbyInfoPane.LobbyViewSettingsPane != null && + GameStartManager.Instance.LobbyInfoPane.LobbyViewSettingsPane.gameObject.active) + return false; + } + catch { } + + try + { + if (GameStartManager.Instance.RulesEditPanel != null) + return false; + } + catch { } + } + } + catch { } + + return true; + } + +private static bool TryGetAspectDistance(object aspectPosition, out Vector3 distance) + { + distance = Vector3.zero; + if (aspectPosition == null) return false; + + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + try + { + Type type = aspectPosition.GetType(); + FieldInfo field = type.GetField("DistanceFromEdge", flags) ?? type.GetField("distanceFromEdge", flags); + if (field != null && field.GetValue(aspectPosition) is Vector3 fieldValue) + { + distance = fieldValue; + return true; + } + + PropertyInfo property = type.GetProperty("DistanceFromEdge", flags) ?? type.GetProperty("distanceFromEdge", flags); + if (property != null && property.GetValue(aspectPosition, null) is Vector3 propertyValue) + { + distance = propertyValue; + return true; + } + } + catch { } + + return false; + } + +private static bool TrySetAspectDistance(object aspectPosition, Vector3 distance) + { + if (aspectPosition == null) return false; + + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + try + { + Type type = aspectPosition.GetType(); + FieldInfo field = type.GetField("DistanceFromEdge", flags) ?? type.GetField("distanceFromEdge", flags); + if (field != null) + { + field.SetValue(aspectPosition, distance); + return true; + } + + PropertyInfo property = type.GetProperty("DistanceFromEdge", flags) ?? type.GetProperty("distanceFromEdge", flags); + if (property != null && property.CanWrite) + { + property.SetValue(aspectPosition, distance, null); + return true; + } + } + catch { } + + return false; + } + +private static object GetHudAspectPosition(HudManager hud) + { + if (hud == null) return null; + + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + try + { + Type type = hud.GetType(); + FieldInfo field = type.GetField("aspectPosition", flags) ?? type.GetField("AspectPosition", flags); + if (field != null) return field.GetValue(hud); + + PropertyInfo property = type.GetProperty("aspectPosition", flags) ?? type.GetProperty("AspectPosition", flags); + if (property != null) return property.GetValue(hud, null); + } + catch { } + + return null; + } + +private static void RefreshHudResolutionForZoom() + { + try + { + ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen); + } + catch { } + } +} +} diff --git a/features/LevelSpoof.cs b/features/LevelSpoof.cs new file mode 100644 index 0000000..a2b05a3 --- /dev/null +++ b/features/LevelSpoof.cs @@ -0,0 +1,467 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +private struct FavoriteOutfitSnapshot + { + public int ColorId; + public string HatId; + public string SkinId; + public string VisorId; + public string NamePlateId; + public string PetId; + + public FavoriteOutfitSnapshot(int colorId, string hatId, string skinId, string visorId, string namePlateId, string petId) + { + ColorId = colorId; + HatId = hatId ?? string.Empty; + SkinId = skinId ?? string.Empty; + VisorId = visorId ?? string.Empty; + NamePlateId = namePlateId ?? string.Empty; + PetId = petId ?? string.Empty; + } + } + +private void DrawOutfitsTab() + { + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader(L("FAVORITE OUTFITS", "ИЗБРАННЫЕ ОБРАЗЫ")); + + PlayerControl selected = SelectedOutfitSourcePlayer(); + for (int i = 0; i < FavoriteOutfitSlotCount; i++) + { + bool hasOutfit = TryDeserializeFavoriteOutfit(favoriteOutfitSlots[i], out FavoriteOutfitSnapshot outfit); + GUILayout.BeginVertical(); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Slot", "Слот")} {i + 1}", toggleLabelStyle, GUILayout.Width(52), GUILayout.Height(22)); + GUILayout.Label(hasOutfit ? FavoriteOutfitSummary(outfit) : L("Empty", "Пусто"), new GUIStyle(GUI.skin.label) { fontSize = 11, clipping = TextClipping.Clip, alignment = TextAnchor.MiddleLeft }, GUILayout.ExpandWidth(true), GUILayout.Height(22)); + GUI.enabled = hasOutfit; + if (GUILayout.Button(L("Apply", "Надеть"), btnStyle, GUILayout.Width(58), GUILayout.Height(22))) + ApplyFavoriteOutfitSlot(i, outfit, hasOutfit); + GUI.enabled = true; + if (GUILayout.Button("X", btnStyle, GUILayout.Width(28), GUILayout.Height(22))) + ClearFavoriteOutfitSlot(i); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + GUILayout.Space(52); + if (GUILayout.Button(L("Save Mine", "Сохр. мой"), btnStyle, GUILayout.Width(100), GUILayout.Height(22))) + SaveFavoriteOutfitSlot(i, PlayerControl.LocalPlayer); + if (GUILayout.Button(L("Save Selected", "Сохр. выбран"), btnStyle, GUILayout.Width(120), GUILayout.Height(22))) + SaveFavoriteOutfitSlot(i, selected); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + GUILayout.Space(4); + } + + GUILayout.Space(12); + DrawMenuSectionHeader("COPY OUTFIT FROM LOBBY"); + + List outfitPlayers = GetLobbyOutfitPlayers(); + if (outfitPlayers.Count > 0) + { + foreach (PlayerControl pc in outfitPlayers) + { + if (pc == null || pc.Data == null || pc.Data.DefaultOutfit == null) continue; + + GUILayout.BeginHorizontal(boxStyle, GUILayout.Height(36)); + try + { + string pName = CleanOutfitPlayerName(pc.Data.PlayerName ?? "Unknown"); + string colorName = SafeColorName(pc.Data.DefaultOutfit.ColorId); + GUILayout.Label(pName, new GUIStyle(toggleLabelStyle) { alignment = TextAnchor.MiddleLeft, clipping = TextClipping.Clip }, GUILayout.ExpandWidth(true), GUILayout.Height(30)); + GUILayout.Label(colorName, new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleRight, clipping = TextClipping.Clip, fontSize = 11 }, GUILayout.Width(105), GUILayout.Height(30)); + + if (GUILayout.Button("Copy Outfit", btnStyle, GUILayout.Width(120), GUILayout.Height(30))) + CopyOutfitFromPlayer(pc); + } + finally { GUILayout.EndHorizontal(); } + GUILayout.Space(4); + } + } + else + { + GUILayout.Label("Нет игроков для копирования."); + } + GUILayout.EndVertical(); + + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader(L("RAINBOW \u2014 FREE COLORS (NO HOST)", "РАДУГА \u2014 СВОБОДНЫЕ ЦВЕТА (БЕЗ ХОСТА)")); + + bool prevFreeRainbow = localRainbowFreeOnly; + localRainbowFreeOnly = DrawToggle(localRainbowFreeOnly, L("Rainbow (only free colors)", "Радуга (только свободные цвета)"), 260); + if (localRainbowFreeOnly && !prevFreeRainbow) localRainbow = false; + + GUILayout.Space(4); + GUILayout.Label(L("Cycles your color only through colors that are free in the room, so the host anti-cheat will not ban you for taking an occupied color.", "Переливает ваш цвет только по свободным цветам в комнате, чтобы анти-чит хоста не забанил за занятый цвет."), menuDescStyle); + + GUILayout.Space(12); + + var freeColors = GetFreeColorIds(); + int freeCount = freeColors.Count; + int clampedFreeIdx = freeCount > 0 ? Mathf.Clamp(selectedFreeColorIndex, 0, freeCount - 1) : 0; + string freeColorLabel = freeCount > 0 ? SafeColorName(freeColors[clampedFreeIdx]) : L("none", "нет"); + + GUILayout.Label(L("Pick a free color:", "Выберите свободный цвет:"), new GUIStyle(toggleLabelStyle) { fontStyle = FontStyle.Bold }); + GUILayout.Space(6); + + GUILayout.BeginHorizontal(); + GUI.enabled = freeCount > 0; + + Color prevSwatchCol = GUI.color; + if (freeCount > 0) { try { GUI.color = Palette.PlayerColors[freeColors[clampedFreeIdx]]; } catch { } } + bool swatchClicked = GUILayout.Button(GUIContent.none, menuSwatchSquareStyle, GUILayout.Width(28), GUILayout.Height(28)); + GUI.color = prevSwatchCol; + if (swatchClicked && freeCount > 0) selectedFreeColorIndex = (selectedFreeColorIndex + 1) % freeCount; + + GUILayout.Space(10); + GUILayout.Label(freeColorLabel, new GUIStyle(toggleLabelStyle) { alignment = TextAnchor.MiddleLeft, fontStyle = FontStyle.Bold, fontSize = 13 }, GUILayout.Height(28)); + + GUILayout.FlexibleSpace(); + GUI.enabled = true; + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + GUI.enabled = freeCount > 0; + if (GUILayout.Button(L("Apply free color", "Применить свободный цвет"), btnStyle, GUILayout.Height(32))) + { + if (freeCount > 0 && PlayerControl.LocalPlayer != null) + { + int pick = freeColors[Mathf.Clamp(selectedFreeColorIndex, 0, freeCount - 1)]; + PlayerControl.LocalPlayer.CmdCheckColor((byte)pick); + ShowNotification(L("Applied free color: ", "Применён свободный цвет: ") + SafeColorName(pick)); + } + } + GUI.enabled = true; + + GUILayout.Space(8); + GUILayout.Label(L("Free colors right now: ", "Свободных цветов сейчас: ") + freeCount, menuDescStyle); + + GUILayout.EndVertical(); + } + +private static PlayerControl SelectedOutfitSourcePlayer() + { + try + { + if (lockedPlayersList != null) + { + foreach (PlayerControl pc in lockedPlayersList) + { + if (pc != null && pc != PlayerControl.LocalPlayer && pc.Data != null && !pc.Data.Disconnected) + return pc; + } + } + } + catch { } + + return PlayerControl.LocalPlayer; + } + +private static List GetLobbyOutfitPlayers() + { + List players = new List(); + try + { + if (PlayerControl.AllPlayerControls == null) + return players; + + foreach (PlayerControl pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null || pc.Data.Disconnected || pc.Data.DefaultOutfit == null) + continue; + + players.Add(pc); + } + } + catch { } + + return players.OrderBy(p => p.PlayerId).ToList(); + } + +private static void CopyOutfitFromPlayer(PlayerControl source) + { + try + { + if (PlayerControl.LocalPlayer == null || source == null || source.Data == null || source.Data.DefaultOutfit == null) + return; + + var outfit = source.Data.DefaultOutfit; + PlayerControl.LocalPlayer.RpcSetSkin(outfit.SkinId); + PlayerControl.LocalPlayer.RpcSetHat(outfit.HatId); + PlayerControl.LocalPlayer.RpcSetVisor(outfit.VisorId); + PlayerControl.LocalPlayer.RpcSetNamePlate(outfit.NamePlateId); + PlayerControl.LocalPlayer.RpcSetPet(outfit.PetId); + ShowNotification($"[OUTFIT] Copied {CleanOutfitPlayerName(source.Data.PlayerName ?? "player")}"); + } + catch { } + } + +private static string CleanOutfitPlayerName(string value) + { + try + { + string clean = Regex.Replace(value ?? string.Empty, "<.*?>", string.Empty).Trim(); + return string.IsNullOrWhiteSpace(clean) ? "Unknown" : clean; + } + catch { return "Unknown"; } + } + +public static void ApplyLevelSpoofValue(uint displayLevel, bool save = true) + { + CaptureLevelSpoofRestoreLevel(displayLevel); + uint targetLevel = displayLevel > 0 ? displayLevel - 1 : 0; + SetStoredLevelRaw(targetLevel, save); + } + + private static void SetStoredLevelRaw(uint rawLevel, bool save = true) + { + try + { + AmongUs.Data.DataManager.Player.stats.level = rawLevel; + } + catch + { + try { AmongUs.Data.DataManager.Player.Stats.Level = rawLevel; } + catch { } + } + + if (save) + { + try { AmongUs.Data.DataManager.Player.Save(); } catch { } + } + } + + private static bool TryGetStoredLevelRaw(out uint rawLevel) + { + rawLevel = 0; + try + { + rawLevel = AmongUs.Data.DataManager.Player.stats.level; + return true; + } + catch + { + try + { + rawLevel = AmongUs.Data.DataManager.Player.Stats.Level; + return true; + } + catch { return false; } + } + } + + private static void CaptureLevelSpoofRestoreLevel(uint displayLevel) + { + if (hasLevelSpoofRestoreLevel) + return; + + if (PlayerPrefs.HasKey("M_LevelSpoofRestoreLevel")) + { + levelSpoofRestoreLevel = (uint)Mathf.Max(0, PlayerPrefs.GetInt("M_LevelSpoofRestoreLevel")); + hasLevelSpoofRestoreLevel = true; + return; + } + + if (!TryGetStoredLevelRaw(out uint currentRaw)) + currentRaw = 0; + + uint spoofRaw = displayLevel > 0 ? displayLevel - 1 : 0; + levelSpoofRestoreLevel = currentRaw == spoofRaw ? 0u : currentRaw; + hasLevelSpoofRestoreLevel = true; + PlayerPrefs.SetInt("M_LevelSpoofRestoreLevel", (int)Mathf.Min(levelSpoofRestoreLevel, int.MaxValue)); + PlayerPrefs.Save(); + } + + public static void RestoreLevelSpoofDefault() + { + uint restoreRaw = 0; + if (hasLevelSpoofRestoreLevel) + restoreRaw = levelSpoofRestoreLevel; + else if (PlayerPrefs.HasKey("M_LevelSpoofRestoreLevel")) + restoreRaw = (uint)Mathf.Max(0, PlayerPrefs.GetInt("M_LevelSpoofRestoreLevel")); + + SetStoredLevelRaw(restoreRaw); + hasLevelSpoofRestoreLevel = false; + levelSpoofRestoreLevel = 0; + PlayerPrefs.DeleteKey("M_LevelSpoofRestoreLevel"); + PlayerPrefs.Save(); + } + + private static int MaxOutfitColorId() + { + try { return Palette.PlayerColors != null ? Mathf.Max(0, Palette.PlayerColors.Length - 1) : 18; } + catch { return 18; } + } + + private static bool TryCaptureFavoriteOutfit(PlayerControl source, out FavoriteOutfitSnapshot outfit) + { + outfit = default; + try + { + if (source == null || source.Data == null || source.Data.DefaultOutfit == null) return false; + var sourceOutfit = source.Data.DefaultOutfit; + outfit = new FavoriteOutfitSnapshot( + Mathf.Clamp(sourceOutfit.ColorId, 0, MaxOutfitColorId()), + sourceOutfit.HatId, + sourceOutfit.SkinId, + sourceOutfit.VisorId, + sourceOutfit.NamePlateId, + sourceOutfit.PetId); + return true; + } + catch { } + + return false; + } + + private static void ApplyFavoriteOutfit(PlayerControl target, FavoriteOutfitSnapshot outfit) + { + if (target == null) return; + target.RpcSetColor((byte)Mathf.Clamp(outfit.ColorId, 0, MaxOutfitColorId())); + target.RpcSetSkin(outfit.SkinId ?? string.Empty); + target.RpcSetHat(outfit.HatId ?? string.Empty); + target.RpcSetVisor(outfit.VisorId ?? string.Empty); + target.RpcSetNamePlate(outfit.NamePlateId ?? string.Empty); + target.RpcSetPet(outfit.PetId ?? string.Empty); + } + + private static string SerializeFavoriteOutfit(FavoriteOutfitSnapshot outfit) + { + return string.Join("\t", new[] + { + Mathf.Clamp(outfit.ColorId, 0, MaxOutfitColorId()).ToString(), + CleanFavoriteOutfitPart(outfit.HatId), + CleanFavoriteOutfitPart(outfit.SkinId), + CleanFavoriteOutfitPart(outfit.VisorId), + CleanFavoriteOutfitPart(outfit.NamePlateId), + CleanFavoriteOutfitPart(outfit.PetId) + }); + } + + private static bool TryDeserializeFavoriteOutfit(string value, out FavoriteOutfitSnapshot outfit) + { + outfit = default; + if (string.IsNullOrWhiteSpace(value)) return false; + + string[] parts = value.Split('\t'); + if (parts.Length < 6 || !int.TryParse(parts[0], out int colorId)) return false; + + outfit = new FavoriteOutfitSnapshot(Mathf.Clamp(colorId, 0, MaxOutfitColorId()), parts[1], parts[2], parts[3], parts[4], parts[5]); + return true; + } + + private static string CleanFavoriteOutfitPart(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value.Replace("\t", " ").Replace("\r", " ").Replace("\n", " ").Trim(); + } + + private static string FavoriteOutfitSummary(FavoriteOutfitSnapshot outfit) + { + string color = "Color " + outfit.ColorId; + try { color = Palette.GetColorName(outfit.ColorId); } catch { } + return $"{color} | {ShortOutfitId(outfit.HatId)}"; + } + + private static string ShortOutfitId(string value) + { + if (string.IsNullOrWhiteSpace(value)) return "-"; + string cleaned = value.Trim(); + return cleaned.Length <= 10 ? cleaned : cleaned.Substring(0, 10); + } + + private void SaveFavoriteOutfitSlot(int index, PlayerControl source) + { + if (index < 0 || index >= favoriteOutfitSlots.Length) return; + if (!TryCaptureFavoriteOutfit(source, out FavoriteOutfitSnapshot outfit)) + { + ShowNotification($"[OUTFIT] {L("Player outfit is not ready.", "Образ игрока еще не готов.")}"); + return; + } + + favoriteOutfitSlots[index] = SerializeFavoriteOutfit(outfit); + SaveConfig(); + ShowNotification($"[OUTFIT] {L("Saved slot", "Сохранен слот")} {index + 1}"); + } + + private void ApplyFavoriteOutfitSlot(int index, FavoriteOutfitSnapshot outfit, bool hasOutfit) + { + if (!hasOutfit) + { + ShowNotification($"[OUTFIT] {L("Slot is empty.", "Слот пуст.")}"); + return; + } + + try + { + ApplyFavoriteOutfit(PlayerControl.LocalPlayer, outfit); + ShowNotification($"[OUTFIT] {L("Applied slot", "Надет слот")} {index + 1}"); + } + catch { } + } + + private void ClearFavoriteOutfitSlot(int index) + { + if (index < 0 || index >= favoriteOutfitSlots.Length) return; + favoriteOutfitSlots[index] = string.Empty; + SaveConfig(); + ShowNotification($"[OUTFIT] {L("Cleared slot", "Очищен слот")} {index + 1}"); + } + public static bool removePenalty = true; + +public static bool alwaysShowLobbyTimer = false; + +public static bool enableChatLog = true; + +public static bool enableFastChat = true; + +public static bool allowLinksAndSymbols = false; + +private static readonly System.Collections.Generic.Dictionary CachedSprites = new(); +} +} diff --git a/features/LocalIdentity.cs b/features/LocalIdentity.cs new file mode 100644 index 0000000..c5c2eb5 --- /dev/null +++ b/features/LocalIdentity.cs @@ -0,0 +1,833 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +public static class AmongUsClientUtils + { + public static IEnumerator CreatePlayer(AmongUsClient __instance, ClientData clientData) + { + if (clientData.IsBeingCreated || clientData.Character) + { + yield break; + } + if (!__instance.AmHost) + { + __instance.logger.Debug("Waiting for host to make my player", null); + yield break; + } + clientData.IsBeingCreated = true; + bool isOwnerOfPlayerData = (__instance.NetworkMode == NetworkModes.LocalGame || __instance.AmModdedHost || (__instance).NetworkMode == NetworkModes.FreePlay); + sbyte b; + if (isOwnerOfPlayerData) + { + b = (GameData.Instance.HasPlayer(clientData) ? GameData.Instance.GetPlayerIdFromClient(clientData) : GameData.Instance.GetAvailableId()); + if (b == -1) + { + (__instance).SendLateRejection(clientData.Id, DisconnectReasons.GameFull); + __instance.logger.Info("Overfilled room.", null); + clientData.IsBeingCreated = false; + yield break; + } + } + else + { + yield return new WaitUntil((Func)(() => GameData.Instance.HasPlayer(clientData))); + b = GameData.Instance.GetPlayerIdFromClient(clientData); + } + Vector2 vector = Vector2.zero; + if (DestroyableSingleton.InstanceExists) + { + vector = new Vector2(-1.9f, 3.25f); + } + PlayerControl pc = Object.Instantiate(__instance.PlayerPrefab, vector, Quaternion.identity); + pc.PlayerId = (byte)b; + pc.FriendCode = clientData.FriendCode; + pc.Puid = clientData.ProductUserId; + clientData.Character = pc; + (__instance).UpdateCachedClients(clientData, clientData.Character); + if (ShipStatus.Instance) + { + ShipStatus.Instance.SpawnPlayer(pc, Palette.PlayerColors.Length, initialSpawn: false); + } + if (isOwnerOfPlayerData) + { + NetworkedPlayerInfo netObjParent = GameData.Instance.AddPlayer(pc, clientData); + __instance.Spawn(netObjParent); + } + else + { + while (GameData.Instance.GetPlayerByClient(clientData) == null) + { + yield return null; + } + } + AmongUsClient.Instance.Spawn(pc, clientData.Id, SpawnFlags.IsClientCharacter); + if (isOwnerOfPlayerData) + { + GameData.Instance.DirtyAllData(); + } + if (GameManager.Instance.LogicOptions.IsDefaults) + { + GameManager.Instance.LogicOptions.SetRecommendations(GameData.Instance.PlayerCount, (AmongUsClient.Instance).NetworkMode); + } + clientData.IsBeingCreated = false; + } + + public static SpawnGameDataMessage CreateSpawnMessage(InnerNetObject netObjParent, int ownerId, SpawnFlags flags) + { + InnerNetObject[] array = netObjParent.GetComponentsInChildren(); + InnerNetObject[] array2 = array; + foreach (InnerNetObject innerNetObject in array2) + { + if (innerNetObject is CustomNetworkTransform) + { + innerNetObject.OwnerId = (AmongUsClient.Instance).ClientId; + } + else + { + innerNetObject.OwnerId = ownerId; + } + innerNetObject.SpawnFlags = flags; + if (innerNetObject.NetId == 0) + { + AmongUsClient instance = AmongUsClient.Instance; + uint netIdCnt = instance.NetIdCnt; + instance.NetIdCnt = netIdCnt + 1; + innerNetObject.NetId = netIdCnt; + lock (AmongUsClient.Instance.allObjects) + { + AmongUsClient.Instance.allObjects.TryAddNetObject(innerNetObject); + } + } + } + return new SpawnGameDataMessage(netObjParent, ownerId, flags, array); + } + + public static SpawnGameDataMessage CreateSpawnMessage(AmongUsClient __instance, InnerNetObject netObjParent, int ownerId, SpawnFlags flags) + { + InnerNetObject[] array = netObjParent.GetComponentsInChildren(); + InnerNetObject[] array2 = array; + foreach (InnerNetObject innerNetObject in array2) + { + innerNetObject.OwnerId = ownerId; + innerNetObject.SpawnFlags = flags; + if (innerNetObject.NetId == 0) + { + uint netIdCnt = (__instance).NetIdCnt; + (__instance).NetIdCnt = netIdCnt + 1; + innerNetObject.NetId = netIdCnt; + lock ((__instance).allObjects) + { + (__instance).allObjects.TryAddNetObject(innerNetObject); + } + } + } + return new SpawnGameDataMessage(netObjParent, ownerId, flags, array); + } + + public static IEnumerator CoOnPlayerChangedScene(InnerNetClient __instance, ClientData client, string currentScene) + { + client.InScene = true; + if (GameData.Instance == null) + { + GameData.Instance = Object.Instantiate(AmongUsClient.Instance.GameDataPrefab); + } + GameData.Instance.RemoveDisconnectedPlayers(); + if (!__instance.AmHost) + { + yield break; + } + if (VoteBanSystem.Instance == null) + { + VoteBanSystem.Instance = Object.Instantiate(AmongUsClient.Instance.VoteBanPrefab); + __instance.Spawn(VoteBanSystem.Instance); + } + if (currentScene.Equals("Tutorial")) + { + GameManager.DestroyInstance(); + GameManager netObjParent = GameManagerCreator.CreateGameManager(GameOptionsManager.Instance.CurrentGameOptions.GameMode); + __instance.Spawn(netObjParent); + int index = ((AmongUsClient.Instance.TutorialMapId == 0 && AprilFoolsMode.ShouldFlipSkeld()) ? 3 : AmongUsClient.Instance.TutorialMapId); + AmongUsClient.Instance.ShipLoadingAsyncHandle = AmongUsClient.Instance.ShipPrefabs[index].InstantiateAsync(null, false); + yield return AmongUsClient.Instance.ShipLoadingAsyncHandle; + AsyncOperationHandle test = AmongUsClient.Instance.ShipLoadingAsyncHandle; + GameObject result = test.Result; + AmongUsClient.Instance.ShipLoadingAsyncHandle = null; + __instance.Spawn(result.GetComponent()); + yield return AmongUsClient.Instance.CreatePlayer(client); + } + else + { + if (!currentScene.Equals("OnlineGame")) + { + yield break; + } + if (client.Id != __instance.ClientId) + { + __instance.SendInitialData(client.Id); + } + else + { + if (__instance.NetworkMode == NetworkModes.LocalGame) + { + __instance.StartCoroutine(AmongUsClient.Instance.CoBroadcastManager()); + } + GameManager.DestroyInstance(); + GameManager netObjParent2 = GameManagerCreator.CreateGameManager(GameOptionsManager.Instance.CurrentGameOptions.GameMode); + __instance.Spawn(netObjParent2); + } + yield return CreatePlayer(AmongUsClient.Instance, client); + } + } + } + +[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] + public static class Shield_PetSpam_Patch + { + public static bool Prefix(PlayerPhysics __instance, byte callId, Hazel.MessageReader reader) + { + if (!ElysiumModMenuGUI.enablePasosLimit) return true; + + if (callId == 49 || callId == 50) + { + try + { + if (__instance == null || __instance.myPlayer == null) return true; + + if (__instance.myPlayer == PlayerControl.LocalPlayer) return true; + + return false; + + return false; + } + catch { } + } + + return true; + } + } + +public static int GetColorIdByName(string name) + { + string[] names = { "red", "blue", "green", "pink", "orange", "yellow", "black", "white", "purple", "brown", "cyan", "lime", "maroon", "rose", "banana", "gray", "tan", "coral", "fortegreen" }; + for (int i = 0; i < names.Length; i++) + if (names[i] == name.ToLower().Trim()) return i; + return -1; + } + +private IEnumerator AttemptShapeshiftFrame(PlayerControl target, PlayerControl morphInto) + { + if (target == null || morphInto == null || PlayerControl.LocalPlayer == null || AmongUsClient.Instance == null) yield break; + + bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); + + if (target.Data.RoleType != RoleTypes.Shapeshifter && hasAnticheat) + { + RoleTypes currentRole = target.Data.RoleType; + target.RpcSetRole(RoleTypes.Shapeshifter, true); + + yield return new WaitForSeconds(0.5f); + + target.RpcShapeshift(morphInto, true); + + yield return new WaitForSeconds(0.5f); + + target.RpcSetRole(currentRole, true); + } + else + { + target.RpcShapeshift(morphInto, true); + } + ShowNotification($"[MORPH] {target.Data.PlayerName} превращен в {morphInto.Data.PlayerName}!"); + } + +private IEnumerator MassMorphCoroutine() + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls == null) yield break; + + bool hasAnticheat = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame && !Constants.IsVersionModded(); + + Dictionary originalRoles = new Dictionary(); + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + originalRoles[pc.PlayerId] = pc.Data.RoleType; + + if (hasAnticheat && pc.Data.RoleType != RoleTypes.Shapeshifter) + { + pc.RpcSetRole(RoleTypes.Shapeshifter, true); + } + } + } + + if (hasAnticheat) yield return new UnityEngine.WaitForSeconds(0.5f); + + PlayerControl targetToMorphInto = null; + if (selectedMorphTargetId != 255) + { + targetToMorphInto = GameData.Instance.GetPlayerById(selectedMorphTargetId)?.Object; + } + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + PlayerControl morphTarget = targetToMorphInto != null ? targetToMorphInto : pc; + pc.RpcShapeshift(morphTarget, true); + } + } + + + if (hasAnticheat) yield return new UnityEngine.WaitForSeconds(0.5f); + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + if (hasAnticheat && originalRoles.ContainsKey(pc.PlayerId)) + { + pc.RpcSetRole(originalRoles[pc.PlayerId], true); + } + } + } + + string notifText = targetToMorphInto != null ? targetToMorphInto.Data.PlayerName : "Egg"; + ShowNotification($"[MASS MORPH] {notifText}"); + } + +private void ForceMeetingAsPlayer(PlayerControl target) + { + if (target == null || AmongUsClient.Instance == null) return; + if (!AmongUsClient.Instance.AmHost) return; + + try + { + MeetingRoomManager.Instance.AssignSelf(target, null); + target.RpcStartMeeting(null); + DestroyableSingleton.Instance.OpenMeetingRoom(target); + } + catch { } + } + +private void KillAll() + { + if (PlayerControl.LocalPlayer == null || PlayerControl.AllPlayerControls == null) return; + Vector3 op = PlayerControl.LocalPlayer.transform.position; + foreach (var t in PlayerControl.AllPlayerControls) + { + if (t != null && t != PlayerControl.LocalPlayer && !t.Data.IsDead && !t.Data.Disconnected) + { + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(t.transform.position); + PlayerControl.LocalPlayer.CmdCheckMurder(t); + PlayerControl.LocalPlayer.RpcMurderPlayer(t, true); + } + } + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(op); + } + +private void KickAll() + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && PlayerControl.AllPlayerControls != null) + { + foreach (var pc in PlayerControl.AllPlayerControls) + if (pc != null && pc != PlayerControl.LocalPlayer && !pc.Data.Disconnected) + AmongUsClient.Instance.KickPlayer((int)pc.OwnerId, false); + } + } + +private void DespawnLobby() + { + try + { + if (LobbyBehaviour.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + LobbyBehaviour.Instance.Cast().Despawn(); + } + } + catch { } + } + +private void SpawnLobby() + { + try + { + if (GameStartManager.Instance != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + LobbyBehaviour newLobby = UnityEngine.Object.Instantiate(GameStartManager.Instance.LobbyPrefab); + AmongUsClient.Instance.Spawn(newLobby.Cast(), -2, SpawnFlags.None); + } + } + catch { } + } + +public static void UnlockCosmetics() + { + if (HatManager.Instance == null) return; + try + { + foreach (var h in HatManager.Instance.allHats) h.Free = true; + foreach (var s in HatManager.Instance.allSkins) s.Free = true; + foreach (var v in HatManager.Instance.allVisors) v.Free = true; + foreach (var p in HatManager.Instance.allPets) p.Free = true; + foreach (var n in HatManager.Instance.allNamePlates) n.Free = true; + } + catch { } + } + +public static void ChangeNameGlobalHost(PlayerControl target, string newName) + { + if (target == null) return; + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + try + { + target.RpcSetName(newName); + var netObj = GameData.Instance.GetComponent(); + if (netObj != null) netObj.SetDirtyBit(1U << (int)target.PlayerId); + } + catch { } + } + +private static void ApplyLocalNameSelf(string newName, bool notify = true) + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null) + { + if (notify) ShowNotification("[LOCAL NAME] Local player not found."); + return; + } + + string renderName = BuildLocalNameRenderText(newName); + if (originalLocalName == null) + { + originalLocalName = local.CurrentOutfit != null && !string.IsNullOrWhiteSpace(local.CurrentOutfit.PlayerName) + ? local.CurrentOutfit.PlayerName + : local.Data?.PlayerName; + } + + if (local.cosmetics != null) + local.cosmetics.SetName(renderName); + + TrySetPlayerNameObject(local.Data, renderName); + if (local.Data != null) + { + TrySetPlayerNameObject(local.Data.DefaultOutfit, renderName); + TrySetPlayerNameObject(local.CurrentOutfit, renderName); + } + + if (notify) + ShowNotification($"[LOCAL NAME] {L("Applied locally:", "Локально применен:")} {newName}"); + } + catch { } + } + + private static void RestoreLocalNameSelf() + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null || local.cosmetics == null) return; + + string baseName = !string.IsNullOrWhiteSpace(originalLocalName) + ? originalLocalName + : (local.Data?.PlayerName ?? local.CurrentOutfit?.PlayerName); + if (!string.IsNullOrWhiteSpace(baseName)) + { + local.cosmetics.SetName(baseName); + TrySetPlayerNameObject(local.Data, baseName); + if (local.Data != null) + { + TrySetPlayerNameObject(local.Data.DefaultOutfit, baseName); + TrySetPlayerNameObject(local.CurrentOutfit, baseName); + } + } + + originalLocalName = null; + } + catch { } + } + + private static void ApplyLocalFriendCodeSelf(string fakeFriendCode, bool notify = true) + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null || local.Data == null) + { + if (notify) ShowNotification("[LOCAL FC] Local player data not found."); + return; + } + + fakeFriendCode ??= string.Empty; + string current = local.Data.FriendCode ?? string.Empty; + if (originalLocalFriendCode == null && current != fakeFriendCode) + originalLocalFriendCode = current; + + TrySetStringMember(local.Data, "FriendCode", fakeFriendCode); + + if (notify) + ShowNotification($"[LOCAL FC] {L("Applied locally:", "Локально применен:")} {fakeFriendCode}"); + } + catch { } + } + + private static void RestoreLocalFriendCodeSelf() + { + try + { + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null || originalLocalFriendCode == null) return; + TrySetStringMember(PlayerControl.LocalPlayer.Data, "FriendCode", originalLocalFriendCode); + originalLocalFriendCode = null; + } + catch { } + } + + private static void TrySetPlayerNameObject(object target, string newName) + { + TrySetStringMember(target, "PlayerName", newName); + } + + private static void TrySetStringMember(object target, string memberName, string value) + { + if (target == null || string.IsNullOrEmpty(memberName)) return; + + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + Type type = target.GetType(); + + try + { + PropertyInfo property = type.GetProperty(memberName, flags); + if (property != null && property.CanWrite) + { + property.SetValue(target, value, null); + return; + } + } + catch { } + + try + { + FieldInfo field = type.GetField(memberName, flags); + if (field != null) field.SetValue(target, value); + } + catch { } + } + + private static void TryInvokeStringMethod(object target, string methodName, string value) + { + if (target == null) return; + + try + { + MethodInfo method = target.GetType().GetMethod( + methodName, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + null, + new[] { typeof(string) }, + null); + + if (method != null) + method.Invoke(target, new object[] { value }); + } + catch { } + } + + public static bool showWatermark = true; + +public static bool whiteMenuTheme = false; + +private static void SaveBool(string key, bool value) + { + PlayerPrefs.SetInt(key, value ? 1 : 0); + } + +private static bool LoadBool(string key, bool defaultValue) + { + return PlayerPrefs.HasKey(key) ? PlayerPrefs.GetInt(key) == 1 : defaultValue; + } + +private static int LoadInt(string key, int defaultValue) + { + return PlayerPrefs.HasKey(key) ? PlayerPrefs.GetInt(key) : defaultValue; + } + +private static float LoadFloat(string key, float defaultValue) + { + return PlayerPrefs.HasKey(key) ? PlayerPrefs.GetFloat(key) : defaultValue; + } + +private void SaveConfig() + { + try + { + PlayerPrefs.SetInt("M_BndMagnet", (int)bindMagnetCursor); + Plugin.SpoofedLevel.Value = spoofLevelString; + Plugin.EnableLevelSpoofConfig.Value = enableLevelSpoof; + SaveBool("M_EnableLevelSpoof", enableLevelSpoof); + Plugin.EnableFriendCodeSpoofConfig.Value = enableFriendCodeSpoof; + Plugin.SpoofFriendCodeConfig.Value = spoofFriendCodeInput; + Plugin.EnablePlatformSpoof.Value = enablePlatformSpoof; + Plugin.AutoBanBrokenFriendCodeConfig.Value = autoBanBrokenFriendCode; + Plugin.PlatformIndex.Value = currentPlatformIndex; + Plugin.ShowWatermarkConfig.Value = showWatermark; + Plugin.UnlockCosmeticsConfig.Value = unlockCosmetics; + Plugin.MoreLobbyInfoConfig.Value = moreLobbyInfo; + Plugin.EnableChatDarkModeConfig.Value = enableChatDarkMode; + Plugin.GhostChatColorConfig.Value = SanitizeHexColor(ghostChatColorHex, "#D7B8FF"); + Plugin.EnableAnomalyLogReportsConfig.Value = enableAnomalyLogReports; + Plugin.ShowEspFriendCodeConfig.Value = showEspFriendCode; + Plugin.RpcSpoofDelayConfig.Value = rpcSpoofDelay; + Plugin.MenuColorIndexConfig.Value = currentMenuColorIndex; + Plugin.RgbMenuModeConfig.Value = rgbMenuMode; + Plugin.RgbMenuTextConfig.Value = rgbMenuText; + Plugin.BoldMenuTextConfig.Value = boldMenuText; + if (menuToggleKey == KeyCode.None) menuToggleKey = KeyCode.Insert; + Plugin.MenuKeybind.Value = menuToggleKey; + PlayerPrefs.SetInt("M_MenuToggleKey", (int)menuToggleKey); + SaveBool("M_WhiteTheme", whiteMenuTheme); + SaveBool("M_RgbMenuText", rgbMenuText); + SaveBool("M_BoldMenuText", boldMenuText); + PlayerPrefs.SetInt("M_MenuLanguageIndex", currentMenuLanguageIndex); + PlayerPrefs.SetInt("M_FpsLimit", fpsLimit); + SaveBool("M_EnableBackground", enableBackground); + SaveBool("M_HardMenu", hardMenu); + SaveBool("M_AutoCopyCodeAndLeave", autoCopyCodeAndLeave); + SaveBool("M_EnableCustomNotifs", EnableCustomNotifs); + SaveBool("M_LogAllRPCs", LogAllRPCs); + PlayerPrefs.SetInt("M_SelectedSpoofMenuIndex", selectedSpoofMenuIndex); + PlayerPrefs.SetFloat("M_MenuWindowX", windowRect.x); + PlayerPrefs.SetFloat("M_MenuWindowY", windowRect.y); + PlayerPrefs.SetFloat("M_MenuWindowW", windowRect.width); + PlayerPrefs.SetFloat("M_MenuWindowH", windowRect.height); + PlayerPrefs.SetInt("M_CurrentTab", currentTab); + PlayerPrefs.SetInt("M_TargetTab", targetTabIndex); + PlayerPrefs.SetInt("M_CurrentGeneralSubTab", currentGeneralSubTab); + PlayerPrefs.SetInt("M_CurrentGeneralInfoSubTab", currentGeneralInfoSubTab); + PlayerPrefs.SetInt("M_CurrentSelfSubTab", currentSelfSubTab); + PlayerPrefs.SetInt("M_CurrentVisualsSubTab", currentVisualsSubTab); + PlayerPrefs.SetInt("M_CurrentPlayersSubTab", currentPlayersSubTab); + PlayerPrefs.SetInt("M_CurrentHostOnlySubTab", currentHostOnlySubTab); + PlayerPrefs.SetInt("M_CurrentAutoHostSubTab", currentAutoHostSubTab); + PlayerPrefs.SetInt("M_BndMMorph", (int)bindMassMorph); + PlayerPrefs.SetInt("M_BndSpawn", (int)bindSpawnLobby); + PlayerPrefs.SetInt("M_BndDespawn", (int)bindDespawnLobby); + PlayerPrefs.SetInt("M_BndCloseMtg", (int)bindCloseMeeting); + PlayerPrefs.SetInt("M_BndInstaStart", (int)bindInstaStart); + PlayerPrefs.SetInt("M_BndEndCrew", (int)bindEndCrew); + PlayerPrefs.SetInt("M_BndEndImp", (int)bindEndImp); + PlayerPrefs.SetInt("M_BndEndImpDC", (int)bindEndImpDC); + PlayerPrefs.SetInt("M_BndEndHnsDC", (int)bindEndHnsDC); + PlayerPrefs.SetInt("M_BndToggleTracers", (int)bindToggleTracers); + PlayerPrefs.SetInt("M_BndToggleNoClip", (int)bindToggleNoClip); + PlayerPrefs.SetInt("M_BndToggleFreecam", (int)bindToggleFreecam); + PlayerPrefs.SetInt("M_BndToggleCameraZoom", (int)bindToggleCameraZoom); + PlayerPrefs.SetInt("M_BndKillAll", (int)bindKillAll); + PlayerPrefs.SetInt("M_BndCallMeeting", (int)bindCallMeeting); + PlayerPrefs.SetInt("M_BndTogglePlayerInfo", (int)bindTogglePlayerInfo); + PlayerPrefs.SetInt("M_BndToggleSeeRoles", (int)bindToggleSeeRoles); + PlayerPrefs.SetInt("M_BndToggleSeeGhosts", (int)bindToggleSeeGhosts); + PlayerPrefs.SetInt("M_BndToggleFullBright", (int)bindToggleFullBright); + PlayerPrefs.SetInt("M_BndKickAll", (int)bindKickAll); + PlayerPrefs.SetInt("M_BndFixSabotages", (int)bindFixSabotages); + PlayerPrefs.SetInt("M_BndSetAllGhost", (int)bindSetAllGhost); + PlayerPrefs.SetInt("M_BndSetAllGhostImp", (int)bindSetAllGhostImp); + PlayerPrefs.SetInt("M_BndReviveAll", (int)bindReviveAll); + SaveBool("M_AutoKickBugs", autoKickBugs); + PlayerPrefs.SetFloat("M_AutoKickTimer", autoKickTimer); + SaveBool("M_DisableVoteKicks", disableVoteKicks); + SaveBool("M_LocalNameSpoof", enableLocalNameSpoof); + SaveBool("M_LocalFakeFCEnabled", enableLocalFriendCodeSpoof); + PlayerPrefs.SetString("M_LocalFakeFC", localFriendCodeInput); + + SaveBool("M_ShowPlayerInfo", showPlayerInfo); + SaveBool("M_SeeGhosts", seeGhosts); + SaveBool("M_SeeRoles", seeRoles); + SaveBool("M_RevealMeetingRoles", revealMeetingRoles); + SaveBool("M_ShowTracers", showTracers); + SaveBool("M_FullBright", fullBright); + SaveBool("M_SeeProtections", seeProtections); + SaveBool("M_SeeKillCooldown", seeKillCooldown); + SaveBool("M_ExtendedLobby", extendedLobby); + SaveBool("M_MoreLobbyInfo", moreLobbyInfo); + SaveBool("M_AlwaysChat", alwaysChat); + SaveBool("M_LobbyRainbowAll", lobbyRainbowAll); + SaveBool("M_LobbyAllColor", lobbyAllColor); + PlayerPrefs.SetInt("M_LobbyAllColorId", lobbyAllColorId); + SaveBool("M_ReadGhostChat", readGhostChat); + SaveBool("M_EnableExtendedChat", enableExtendedChat); + SaveBool("M_EnableFastChat", enableFastChat); + SaveBool("M_AllowLinksAndSymbols", allowLinksAndSymbols); + SaveBool("M_EnableChatHistory", enableChatHistory); + PlayerPrefs.SetInt("M_ChatHistoryLimit", chatHistoryLimit); + SaveBool("M_EnableClipboard", enableClipboard); + SaveBool("M_EnableChatMessageDoubleClickCopy", enableChatMessageDoubleClickCopy); + SaveBool("M_EnableChatNameColorCopy", enableChatNameColorCopy); + SaveBool("M_EnableChatLog", enableChatLog); + SaveBool("M_EnableColorCommand", enableColorCommand); + SaveBool("M_BlockRainbowChat", blockRainbowChat); + SaveBool("M_BlockFortegreenChat", blockFortegreenChat); + SaveBool("M_SpoofMenuEnabled", SpoofMenuEnabled); + SaveBool("M_NoClip", noClip); + SaveBool("M_TpToCursor", tpToCursor); + SaveBool("M_DragToCursor", dragToCursor); + SaveBool("M_AutoFollowCursor", autoFollowCursor); + SaveBool("M_Freecam", freecam); + SaveBool("M_CameraZoom", cameraZoom); + SaveBool("M_RevealVotes", RevealVotesEnabled); + SaveBool("M_NoTaskMode", noTaskMode); + SaveBool("M_NoMapCooldowns", noMapCooldowns); + SaveBool("M_UnlockVents", unlockVents); + SaveBool("M_WalkInVents", walkInVents); + SaveBool("M_AllowTasksAsImpostor", allowTasksAsImpostor); + SaveBool("M_KillWhileVanishedHostOnly", killWhileVanishedHostOnly); + SaveBool("M_RoleBuffImmortality", roleBuffImmortality); + SaveBool("M_NeverEndGame", neverEndGame); + SaveBool("M_RemovePenalty", removePenalty); + SaveBool("M_AlwaysShowLobbyTimer", alwaysShowLobbyTimer); + SaveBool("M_AutoBanEnabled", autoBanEnabled); + SaveBool("M_AllowDuplicateColors", allowDuplicateColors); + SaveBool("M_BlockSpoofRPC", blockSpoofRPC); + SaveBool("M_AutoBanPlatformSpoof", autoBanPlatformSpoof); + SaveBool("M_BanCustomPlatformsFromTxt", banCustomPlatformsFromTxt); + SaveBool("M_AutoKickLowLevel", autoKickLowLevelEnabled); + PlayerPrefs.SetInt("M_AutoKickMinLevel", Mathf.Clamp(autoKickMinLevel, 1, 300)); + SaveBool("M_BlockSabotageRPC", blockSabotageRPC); + PlayerPrefs.SetInt("M_PunishmentMode", punishmentMode); + SaveBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); + SaveBool("M_BlockChatFloodRpc", blockChatFloodRpc); + SaveBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); + SaveBool("M_UnfixableLights", unfixableLights); + SaveBool("M_PasosLimit", enablePasosLimit); + SaveBool("M_AntiPasosLocalBan", enableLocalPasosBan); + SaveBool("M_AntiPasosHostBan", enableHostPasosBan); + SaveBool("M_MalformedPacketGuard", enableMalformedPacketGuard); + SaveBool("M_BanMalformedPacketSender", banMalformedPacketSender); + SaveBool("M_QuickChatEmptyGuard", enableQuickChatEmptyGuard); + SaveBool("M_BanQuickChatEmptySpammer", banQuickChatEmptySpammer); + SaveBool("M_UnownedSpawnGuard", enableUnownedSpawnGuard); + SaveBool("M_AutoHostEnabled", AutoHostEnabled); + SaveBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); + SaveBool("M_AutoHostNotifications", AutoHostNotifications); + SaveBool("M_AutoHostForceLastMinute", AutoHostForceLastMinute); + SaveBool("M_AutoHostWaitLoadedPlayers", AutoHostWaitLoadedPlayers); + SaveBool("M_AutoHostCancelBelowMin", AutoHostCancelBelowMin); + SaveBool("M_AutoHostInstantStart", AutoHostInstantStart); + SaveBool("M_AutoGhostAfterStart", autoGhostAfterStart); + PlayerPrefs.SetInt("M_AutoHostMinPlayers", AutoHostMinPlayers); + PlayerPrefs.SetFloat("M_AutoHostStartDelaySeconds", AutoHostStartDelaySeconds); + PlayerPrefs.SetInt("M_AutoHostFastStartPlayers", AutoHostFastStartPlayers); + PlayerPrefs.SetFloat("M_AutoHostFastStartDelaySeconds", AutoHostFastStartDelaySeconds); + PlayerPrefs.SetFloat("M_WalkSpeed", walkSpeed); + PlayerPrefs.SetFloat("M_EngineSpeed", engineSpeed); + + Plugin.MenuConfig.Save(); + + PlayerPrefs.SetString("M_SpoofName", customNameInput); + for (int i = 0; i < favoriteOutfitSlots.Length; i++) + PlayerPrefs.SetString($"M_FavoriteOutfit_{i}", favoriteOutfitSlots[i] ?? string.Empty); + PlayerPrefs.Save(); + } + catch { } + } + +private void DrawAutoHostTab() + { + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader(L("AUTO HOST SYSTEM", "СИСТЕМА АВТО-ХОСТА")); + + var snapshot = ElysiumAutoHostService.GetStatusSnapshot(); + GUILayout.Label($"{L("Status:", "Статус:")} {snapshot.State}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 13 }); + GUILayout.Space(10); + + AutoHostEnabled = DrawToggle(AutoHostEnabled, L("Enable Auto Host", "Включить Авто-Хост"), 250); + GUILayout.Space(5); + AutoReturnLobbyAfterMatch = DrawToggle(AutoReturnLobbyAfterMatch, L("Auto Return To Lobby", "Авто-возврат в лобби"), 250); + GUILayout.Space(5); + AutoHostNotifications = DrawToggle(AutoHostNotifications, L("Show Notifications", "Показывать уведомления"), 250); + GUILayout.Space(5); + AutoHostWaitLoadedPlayers = DrawToggle(AutoHostWaitLoadedPlayers, L("Wait For Players To Load", "Ждать прогрузки игроков"), 250); + GUILayout.Space(5); + AutoHostCancelBelowMin = DrawToggle(AutoHostCancelBelowMin, L("Cancel Countdown If Player Leaves", "Отмена отсчета, если игрок вышел"), 250); + GUILayout.Space(5); + AutoHostInstantStart = DrawToggle(AutoHostInstantStart, L("Instant Start (No 5s Wait)", "Мгновенный старт (Без 5с)"), 250); + GUILayout.Space(5); + autoGhostAfterStart = DrawToggle(autoGhostAfterStart, L("Auto Ghost After Start", "Авто-призрак после старта"), 250); + GUILayout.Space(5); + AutoHostForceLastMinute = DrawToggle(AutoHostForceLastMinute, L("Force Start Last Minute", "Форс-старт на последней минуте"), 250); + + GUILayout.Space(15); + + string hexColor = GetMenuAccentHex(); + GUIStyle sliderLabelStyle = new GUIStyle(toggleLabelStyle) { richText = true }; + + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Min Players:", "Мин. игроков:")} {AutoHostMinPlayers}", sliderLabelStyle, GUILayout.Width(175)); + AutoHostMinPlayers = (int)GUILayout.HorizontalSlider(AutoHostMinPlayers, 1f, 15f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Start Delay:", "Задержка старта:")} {Mathf.Round(AutoHostStartDelaySeconds)}s", sliderLabelStyle, GUILayout.Width(175)); + AutoHostStartDelaySeconds = GUILayout.HorizontalSlider(AutoHostStartDelaySeconds, 0f, 180f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Fast Start Players:", "Игроков для фаст-старта:")} {AutoHostFastStartPlayers}", sliderLabelStyle, GUILayout.Width(175)); + AutoHostFastStartPlayers = (int)GUILayout.HorizontalSlider(AutoHostFastStartPlayers, 0f, 15f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("Fast Start Delay:", "Задержка фаст-старта:")} {Mathf.Round(AutoHostFastStartDelaySeconds)}s", sliderLabelStyle, GUILayout.Width(175)); + AutoHostFastStartDelaySeconds = GUILayout.HorizontalSlider(AutoHostFastStartDelaySeconds, 0f, 60f, sliderStyle, sliderThumbStyle, GUILayout.Width(335)); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + } +} +} diff --git a/features/MenuControls.cs b/features/MenuControls.cs new file mode 100644 index 0000000..ce19203 --- /dev/null +++ b/features/MenuControls.cs @@ -0,0 +1,680 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +public static Color GetMenuAccentColor(bool allowRgbText = true) + { + return allowRgbText && RgbMenuTextActive() + ? GetThemeAccentColor(currentAccentColor) + : GetThemeAccentColor(GetStableMenuAccentSource()); + } + + public static string GetMenuAccentHex(bool allowRgbText = true) + { + return ColorUtility.ToHtmlStringRGB(GetMenuAccentColor(allowRgbText)); + } + + public static Color GetMenuControlAccentColor() + { + return rgbMenuMode + ? GetThemeAccentColor(currentAccentColor) + : GetThemeAccentColor(GetStableMenuAccentSource()); + } + + public static string GetMenuControlAccentHex() + { + return ColorUtility.ToHtmlStringRGB(GetMenuControlAccentColor()); + } + + private Color GetStableControlAccentColor(Color fallback) + { + Color source = fallback; + try + { + if (rgbMenuMode) + source = GetStableMenuAccentSource(); + } + catch { } + + return GetThemeAccentColor(source); + } + + private void UpdateAccentColor(Color color) + { + currentAccentColor = color; + Color effectiveColor = GetThemeAccentColor(color); + Color controlColor = rgbMenuMode ? effectiveColor : GetStableControlAccentColor(color); + if (texAccent != null) + { + int size = texAccent.width; + Color[] pix = new Color[size * size]; + float center = size / 2f; + float radius = 6f; + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); + float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); + float dist = Mathf.Sqrt(dx * dx + dy * dy); + float alpha = Mathf.Clamp01(radius - dist + 0.5f); + Color c = controlColor; c.a = alpha; + pix[y * size + x] = c; + } + } + texAccent.SetPixels(pix); texAccent.Apply(); + } + if (texSliderThumb != null) + { + int size = texSliderThumb.width; + Color[] pix = new Color[size * size]; + float center = size / 2f; + float radius = 10f; + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); + float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); + float dist = Mathf.Sqrt(dx * dx + dy * dy); + float alpha = Mathf.Clamp01(radius - dist + 0.5f); + Color c = controlColor; c.a = alpha; + pix[y * size + x] = c; + } + } + texSliderThumb.SetPixels(pix); texSliderThumb.Apply(); + } + if (texScrollThumb != null) + { + int size = texScrollThumb.width; + Color[] pix = new Color[size * size]; + float center = size / 2f; + float radius = 4f; + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); + float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); + float dist = Mathf.Sqrt(dx * dx + dy * dy); + float alpha = Mathf.Clamp01(radius - dist + 0.5f); + Color c = controlColor; c.a = alpha; + pix[y * size + x] = c; + } + } + texScrollThumb.SetPixels(pix); texScrollThumb.Apply(); + } + if (texToggleOn != null) UpdateSwitchTex(texToggleOn, true, controlColor); + if (texTrackOn != null) UpdateTrackTex(texTrackOn, true, controlColor); + bool rgbText = RgbMenuTextActive(); + Color menuHeadingText = rgbText ? effectiveColor : (whiteMenuTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : color); + if (windowStyle != null) windowStyle.normal.textColor = rgbText ? effectiveColor : (whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : color); + if (headerStyle != null) headerStyle.normal.textColor = menuHeadingText; + if (menuSectionTitleStyle != null) menuSectionTitleStyle.normal.textColor = menuHeadingText; + if (menuBadgeStyle != null) menuBadgeStyle.normal.textColor = menuHeadingText; + if (activeSidebarBtnStyle != null) { activeSidebarBtnStyle.normal.textColor = effectiveColor; activeSidebarBtnStyle.hover.textColor = effectiveColor; } + if (activeTabStyle != null) activeTabStyle.normal.background = texAccent; + if (activeSubTabStyle != null) activeSubTabStyle.normal.background = texAccent; + if (btnStyle != null) btnStyle.active.background = texAccent; + if (inputBlockStyle != null) inputBlockStyle.normal.textColor = rgbText ? effectiveColor : (whiteMenuTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : color); + } + + private void InitStyles() + { + bool isLightTheme = whiteMenuTheme; + FontStyle menuTextStyle = boldMenuText ? FontStyle.Bold : FontStyle.Normal; + Color accent = GetMenuAccentColor(); + Color darkBg = isLightTheme ? new Color(0.97f, 0.97f, 0.97f, 0.78f) : new Color(0.12f, 0.12f, 0.12f, 0.90f); + Color sidebarBg = new Color(0.0f, 0.0f, 0.0f, 0.0f); + Color boxBg = new Color(0f, 0f, 0f, 0f); + Color btnCol = isLightTheme ? new Color32(252, 247, 240, 255) : new Color(0.23f, 0.23f, 0.23f, 1f); + Color sliderBgCol = isLightTheme ? new Color(0.78f, 0.78f, 0.78f, 0.68f) : new Color(0.08f, 0.08f, 0.08f, 1f); + Color textMain = isLightTheme ? new Color(0.18f, 0.18f, 0.18f, 1f) : new Color(0.78f, 0.78f, 0.78f, 1f); + Color textMuted = isLightTheme ? new Color(0.33f, 0.33f, 0.33f, 1f) : new Color(0.6f, 0.6f, 0.6f, 1f); + Color textHover = isLightTheme ? new Color(0.06f, 0.06f, 0.06f, 1f) : Color.white; + Color headerText = RgbMenuTextActive() ? accent : (isLightTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : accent); + Color inputBgCol = isLightTheme ? new Color(1f, 1f, 1f, 0.86f) : new Color(0.08f, 0.08f, 0.08f, 0.85f); + + texWindowBg = MakeRoundedTex(64, darkBg, 12f); + texSidebarBg = MakeRoundedTex(64, sidebarBg, 0f); + texBoxBg = MakeRoundedTex(64, boxBg, 0f); + texBtnBg = MakeRoundedTex(64, btnCol, 6f); + texAccent = MakeRoundedTex(64, accent, 6f); + texSliderBg = MakeRoundedTex(64, sliderBgCol, 4f); + texSliderThumb = MakeRoundedTex(20, accent, 10f); + texInputBg = MakeRoundedTex(64, inputBgCol, 6f); + texColorBtn = MakeRoundedTex(64, Color.white, 12f); + + texMenuCard = MakeRoundedTex(64, isLightTheme ? new Color(1f, 1f, 1f, 0.55f) : new Color(1f, 1f, 1f, 0.045f), 12f); + + menuCardStyle = new GUIStyle(); + menuCardStyle.normal.background = texMenuCard; + menuCardStyle.border = CreateRectOffset(12, 12, 12, 12); + menuCardStyle.padding = CreateRectOffset(14, 14, 12, 14); + menuCardStyle.margin = CreateRectOffset(0, 0, 0, 10); + + menuAccentBarStyle = new GUIStyle(); + menuAccentBarStyle.normal.background = texAccent; + + menuSectionTitleStyle = new GUIStyle(); + menuSectionTitleStyle.normal.textColor = headerText; + menuSectionTitleStyle.fontStyle = menuTextStyle; + menuSectionTitleStyle.fontSize = 13; + menuSectionTitleStyle.alignment = TextAnchor.MiddleLeft; + menuSectionTitleStyle.richText = true; + + menuDescStyle = new GUIStyle(); + menuDescStyle.normal.textColor = textMuted; + menuDescStyle.fontSize = 11; + menuDescStyle.fontStyle = menuTextStyle; + menuDescStyle.richText = true; + menuDescStyle.wordWrap = true; + menuDescStyle.padding = CreateRectOffset(2, 0, 2, 0); + + menuBadgeStyle = new GUIStyle(); + menuBadgeStyle.normal.background = texInputBg; + menuBadgeStyle.normal.textColor = headerText; + menuBadgeStyle.fontStyle = menuTextStyle; + menuBadgeStyle.fontSize = 12; + menuBadgeStyle.alignment = TextAnchor.MiddleCenter; + menuBadgeStyle.border = CreateRectOffset(6, 6, 6, 6); + menuBadgeStyle.padding = CreateRectOffset(8, 8, 3, 3); + + menuSwatchStyle = new GUIStyle(); + menuSwatchStyle.normal.background = texColorBtn; + menuSwatchStyle.border = CreateRectOffset(8, 8, 8, 8); + + texSwatchSquare = MakeRoundedTex(32, Color.white, 6f); + menuSwatchSquareStyle = new GUIStyle(); + menuSwatchSquareStyle.normal.background = texSwatchSquare; + menuSwatchSquareStyle.border = CreateRectOffset(6, 6, 6, 6); + + texToggleOff = new Texture2D(30, 16, TextureFormat.RGBA32, false); + texToggleOff.hideFlags = HideFlags.HideAndDontSave; + texToggleOn = new Texture2D(30, 16, TextureFormat.RGBA32, false); + texToggleOn.hideFlags = HideFlags.HideAndDontSave; + UpdateSwitchTex(texToggleOff, false, Color.white); + UpdateSwitchTex(texToggleOn, true, accent); + texTrackOff = new Texture2D(30, 16, TextureFormat.RGBA32, false); + texTrackOff.hideFlags = HideFlags.HideAndDontSave; + texTrackOn = new Texture2D(30, 16, TextureFormat.RGBA32, false); + texTrackOn.hideFlags = HideFlags.HideAndDontSave; + UpdateTrackTex(texTrackOff, false, Color.white); + UpdateTrackTex(texTrackOn, true, accent); + texKnobWhite = MakeRoundedTex(16, Color.white, 8f); + + safeLineStyle = new GUIStyle(); + safeLineStyle.normal.background = MakeRoundedTex(2, isLightTheme ? new Color(0.75f, 0.75f, 0.75f, 1f) : Color.white, 0f); + + windowStyle = new GUIStyle(); + windowStyle.normal.background = texWindowBg; + windowStyle.normal.textColor = accent; + windowStyle.fontStyle = menuTextStyle; + windowStyle.fontSize = 14; + windowStyle.padding = CreateRectOffset(0, 0, 0, 0); + windowStyle.border = CreateRectOffset(12, 12, 12, 12); + + boxStyle = new GUIStyle(); + boxStyle.normal.background = texBoxBg; + boxStyle.padding = CreateRectOffset(0, 0, 0, 0); + boxStyle.margin = CreateRectOffset(0, 0, 4, 8); + + btnStyle = new GUIStyle(GUI.skin.button); + btnStyle.normal.background = texBtnBg; + btnStyle.hover.background = texBtnBg; + btnStyle.normal.textColor = textMain; + btnStyle.hover.textColor = textHover; + btnStyle.active.background = texAccent; + btnStyle.active.textColor = Color.black; + btnStyle.alignment = TextAnchor.MiddleCenter; + btnStyle.border = CreateRectOffset(6, 6, 6, 6); + btnStyle.fontSize = 12; + btnStyle.fontStyle = menuTextStyle; + btnStyle.clipping = TextClipping.Overflow; + btnStyle.wordWrap = false; + + activeTabStyle = new GUIStyle(btnStyle); + activeTabStyle.normal.background = texAccent; + activeTabStyle.normal.textColor = Color.black; + + subTabStyle = new GUIStyle(btnStyle); + subTabStyle.padding = CreateRectOffset(8, 8, 2, 2); + subTabStyle.clipping = TextClipping.Overflow; + subTabStyle.wordWrap = false; + activeSubTabStyle = new GUIStyle(activeTabStyle); + activeSubTabStyle.padding = CreateRectOffset(8, 8, 2, 2); + activeSubTabStyle.clipping = TextClipping.Overflow; + activeSubTabStyle.wordWrap = false; + + inputBlockStyle = new GUIStyle(btnStyle); + inputBlockStyle.normal.background = texInputBg; + inputBlockStyle.hover.background = texInputBg; + inputBlockStyle.active.background = texAccent; + inputBlockStyle.normal.textColor = isLightTheme ? new Color(0.15f, 0.15f, 0.15f, 1f) : accent; + inputBlockStyle.alignment = TextAnchor.MiddleCenter; + inputBlockStyle.fontStyle = menuTextStyle; + + headerStyle = new GUIStyle(); + headerStyle.normal.background = texBtnBg; + headerStyle.normal.textColor = headerText; + headerStyle.fontStyle = menuTextStyle; + headerStyle.alignment = TextAnchor.MiddleLeft; + headerStyle.padding = CreateRectOffset(6, 6, 4, 4); + headerStyle.margin = CreateRectOffset(0, 0, 4, 4); + headerStyle.fontSize = 13; + headerStyle.clipping = TextClipping.Overflow; + headerStyle.wordWrap = false; + + sidebarStyle = new GUIStyle(); + sidebarStyle.normal.background = texSidebarBg; + sidebarStyle.padding = CreateRectOffset(0, 0, 5, 0); + + sidebarBtnStyle = new GUIStyle(); + sidebarBtnStyle.normal.textColor = textMuted; + sidebarBtnStyle.hover.textColor = textHover; + sidebarBtnStyle.padding = CreateRectOffset(12, 0, 6, 6); + sidebarBtnStyle.alignment = TextAnchor.MiddleLeft; + sidebarBtnStyle.fontSize = 13; + sidebarBtnStyle.fontStyle = menuTextStyle; + + activeSidebarBtnStyle = new GUIStyle(sidebarBtnStyle); + activeSidebarBtnStyle.normal.textColor = accent; + activeSidebarBtnStyle.hover.textColor = accent; + + toggleOffStyle = new GUIStyle(); + toggleOffStyle.normal.background = texToggleOff; + toggleOnStyle = new GUIStyle(); + toggleOnStyle.normal.background = texToggleOn; + trackOffStyle = new GUIStyle(); + trackOffStyle.normal.background = texTrackOff; + trackOnStyle = new GUIStyle(); + trackOnStyle.normal.background = texTrackOn; + knobStyle = new GUIStyle(); + knobStyle.normal.background = texKnobWhite; + + toggleLabelStyle = new GUIStyle(); + toggleLabelStyle.normal.textColor = textMain; + toggleLabelStyle.alignment = TextAnchor.MiddleLeft; + toggleLabelStyle.padding = CreateRectOffset(4, 0, 0, 0); + toggleLabelStyle.fontSize = 12; + toggleLabelStyle.fontStyle = menuTextStyle; + toggleLabelStyle.clipping = TextClipping.Overflow; + toggleLabelStyle.wordWrap = false; + toggleLabelStyle.richText = true; + + sliderStyle = new GUIStyle(); + sliderStyle.normal.background = texSliderBg; + sliderStyle.border = CreateRectOffset(6, 6, 6, 6); + sliderStyle.fixedHeight = 10f; + sliderStyle.margin = CreateRectOffset(0, 0, 8, 8); + + sliderThumbStyle = new GUIStyle(); + sliderThumbStyle.normal.background = texSliderThumb; + sliderThumbStyle.fixedWidth = 18f; + sliderThumbStyle.fixedHeight = 18f; + sliderThumbStyle.margin = CreateRectOffset(0, 0, -4, 0); + + titleStyle = new GUIStyle(); + titleStyle.normal.textColor = accent; + titleStyle.fontStyle = menuTextStyle; + titleStyle.fontSize = 14; + titleStyle.richText = true; + titleStyle.padding = CreateRectOffset(10, 0, 8, 0); + + Texture2D texScrollBg = MakeRoundedTex(8, new Color(0.1f, 0.1f, 0.1f, 0.2f), 4f); + texScrollThumb = MakeRoundedTex(8, accent, 4f); + + GUIStyle scrollBarStyle = new GUIStyle(GUI.skin.verticalScrollbar); + scrollBarStyle.normal.background = texScrollBg; + scrollBarStyle.fixedWidth = 8f; + scrollBarStyle.border = CreateRectOffset(0, 0, 4, 4); + scrollBarStyle.margin = CreateRectOffset(2, 2, 2, 2); + + GUIStyle scrollBarThumbStyle = new GUIStyle(GUI.skin.verticalScrollbarThumb); + scrollBarThumbStyle.normal.background = texScrollThumb; + scrollBarThumbStyle.hover.background = texScrollThumb; + scrollBarThumbStyle.active.background = texScrollThumb; + scrollBarThumbStyle.fixedWidth = 8f; + scrollBarThumbStyle.border = CreateRectOffset(0, 0, 4, 4); + + GUI.skin.verticalScrollbar = scrollBarStyle; + GUI.skin.verticalScrollbarThumb = scrollBarThumbStyle; + GUI.skin.horizontalScrollbar.normal.background = null; + GUI.skin.horizontalScrollbarThumb.normal.background = null; + GUI.skin.label.normal.textColor = textMain; + GUI.skin.box.normal.textColor = textMain; + + stylesInited = true; + } + public static bool autoCopyCodeAndLeave = false; + +public static HashSet votedPlayerIds = new HashSet(); + +private void LoadBackgroundImage() + { + try + { + string bgPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "MenuBG.png"); + if (!System.IO.File.Exists(bgPath)) bgPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "MenuBG.jpg"); + if (System.IO.File.Exists(bgPath)) + { + byte[] fileData = System.IO.File.ReadAllBytes(bgPath); + Texture2D tempTex = new Texture2D(2, 2); + ImageConversion.LoadImage(tempTex, fileData); + customMenuBg = new Texture2D(tempTex.width, tempTex.height, TextureFormat.RGBA32, false); + customMenuBg.hideFlags = HideFlags.HideAndDontSave; + Color[] pix = tempTex.GetPixels(); + UnityEngine.Object.Destroy(tempTex); + int w = customMenuBg.width, h = customMenuBg.height; + float targetRadius = 12f, rx = targetRadius * (w / windowRect.width), ry = targetRadius * (h / windowRect.height); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + float dx = 0f, dy = 0f; + if (x < rx) dx = rx - x; + else if (x > w - rx) dx = x - (w - rx); + if (y < ry) dy = ry - y; + else if (y > h - ry) dy = y - (h - ry); + if (dx > 0 && dy > 0) + { + float nx = dx / rx, ny = dy / ry; + float dist = Mathf.Sqrt(nx * nx + ny * ny); + if (dist > 1f) { Color c = pix[y * w + x]; c.a = 0f; pix[y * w + x] = c; } + else + { + float alphaMult = Mathf.Clamp01((1f - dist) * Mathf.Max(rx, ry)); + Color c = pix[y * w + x]; c.a *= alphaMult; pix[y * w + x] = c; + } + } + } + customMenuBg.SetPixels(pix); customMenuBg.Apply(); + } + else enableBackground = false; + } + catch { enableBackground = false; } + } + +public static string ApplyMenuShimmer(string text) + { + string result = ""; + Color baseColor = GetMenuControlAccentColor(); + Color glowColor = whiteMenuTheme ? Color.Lerp(baseColor, Color.white, 0.45f) : Color.white; + for (int i = 0; i < text.Length; i++) + { + if (text[i] == ' ') { result += " "; continue; } + float wave = Mathf.Sin(Time.unscaledTime * 6f - (i * 0.4f)) * 0.5f + 0.5f; + Color c = Color.Lerp(baseColor, glowColor, wave); + result += $"{text[i]}"; + } + return result; + } + +private static readonly Dictionary toggleAnimStates = new Dictionary(); + +private void DrawAnimatedSwitch(Rect boxRect, bool value, string animKey) + { + string key = animKey ?? ""; + float anim; + if (!toggleAnimStates.TryGetValue(key, out anim)) { anim = value ? 1f : 0f; toggleAnimStates[key] = anim; } + if (Event.current.type != EventType.Repaint) return; + + anim = Mathf.MoveTowards(anim, value ? 1f : 0f, Time.unscaledDeltaTime * 8f); + toggleAnimStates[key] = anim; + float eased = Mathf.SmoothStep(0f, 1f, anim); + + float knob = boxRect.height - 4f; + float knobX = Mathf.Lerp(boxRect.x + 2f, boxRect.xMax - knob - 2f, eased); + Rect knobRect = new Rect(knobX, boxRect.y + 2f, knob, knob); + + Color prevBg = GUI.backgroundColor; + GUI.backgroundColor = Color.Lerp(new Color(0.80f, 0.80f, 0.84f, 1f), Color.white, eased); + GUI.Box(knobRect, "", knobStyle); + GUI.backgroundColor = prevBg; + } + +private bool DrawToggle(bool value, string text, int width = 0) + { + GUILayout.BeginHorizontal(GUILayout.MinWidth(width > 0 ? width : 200), GUILayout.Height(20)); + + Rect animSwitchRect = GUILayoutUtility.GetRect(30f, 16f, GUILayout.Width(30f), GUILayout.Height(16f)); + bool clickedBox = GUI.Button(animSwitchRect, "", value ? trackOnStyle : trackOffStyle); + DrawAnimatedSwitch(animSwitchRect, value, text); + + GUILayout.Space(6); + + GUIStyle toggleTextStyle = new GUIStyle(toggleLabelStyle) + { + clipping = TextClipping.Overflow, + wordWrap = false, + richText = true, + stretchWidth = false, + alignment = TextAnchor.MiddleLeft + }; + + GUIContent toggleContent = new GUIContent(text); + float toggleTextWidth = Mathf.Ceil(toggleTextStyle.CalcSize(toggleContent).x) + 8f; + Rect textRect = GUILayoutUtility.GetRect(toggleTextWidth, 18f, GUILayout.Width(toggleTextWidth), GUILayout.Height(18f)); + GUI.Label(textRect, toggleContent, toggleTextStyle); + + bool clickedText = Event.current.type == EventType.MouseDown && textRect.Contains(Event.current.mousePosition); + if (clickedText) Event.current.Use(); + + GUILayout.FlexibleSpace(); + + GUILayout.EndHorizontal(); + + if (clickedBox || clickedText) settingsDirty = true; + return (clickedBox || clickedText) ? !value : value; + } + + private bool DrawBindableButton(string label, string bindKey, float width) + { + bool clicked = false; + GUILayout.BeginVertical(GUILayout.Width(width)); + if (GUILayout.Button(label, btnStyle, GUILayout.Height(25), GUILayout.Width(width))) clicked = true; + string bindTxt = bindingAction == bindKey ? "Press Key..." : (keyBinds.ContainsKey(bindKey) ? $"[{keyBinds[bindKey]}]" : "[Bind Key]"); + GUIStyle bindStyle = new GUIStyle(btnStyle) { fontSize = 10, normal = { textColor = new Color(0.6f, 0.6f, 0.6f) } }; + if (bindingAction == bindKey) bindStyle.normal.textColor = GetMenuAccentColor(); + if (GUILayout.Button(bindTxt, bindStyle, GUILayout.Height(15), GUILayout.Width(width))) bindingAction = bindKey; + GUILayout.EndVertical(); + return clicked; + } + + private bool DrawHostToggle(bool value, string text, float totalWidth = 250f) + { + GUILayout.BeginHorizontal(GUILayout.MinWidth(totalWidth), GUILayout.Height(20)); + Rect animSwitchRect = GUILayoutUtility.GetRect(30f, 16f, GUILayout.Width(30f), GUILayout.Height(16f)); + bool clickedBox = GUI.Button(animSwitchRect, "", value ? trackOnStyle : trackOffStyle); + DrawAnimatedSwitch(animSwitchRect, value, text); + GUILayout.Space(6); + + GUIStyle hostToggleTextStyle = new GUIStyle(toggleLabelStyle) + { + clipping = TextClipping.Overflow, + wordWrap = false, + richText = true, + stretchWidth = false, + alignment = TextAnchor.MiddleLeft + }; + + GUIContent hostToggleContent = new GUIContent(text); + float hostToggleTextWidth = Mathf.Ceil(hostToggleTextStyle.CalcSize(hostToggleContent).x) + 8f; + Rect textRect = GUILayoutUtility.GetRect(hostToggleTextWidth, 16f, GUILayout.Width(hostToggleTextWidth), GUILayout.Height(16f)); + GUI.Label(textRect, hostToggleContent, hostToggleTextStyle); + + bool clickedText = Event.current.type == EventType.MouseDown && textRect.Contains(Event.current.mousePosition); + if (clickedText) Event.current.Use(); + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + if (clickedBox || clickedText) settingsDirty = true; + return (clickedBox || clickedText) ? !value : value; + } + private void DrawBindsTab() + { + GUILayout.BeginVertical(menuCardStyle); + try + { + DrawMenuSectionHeader("CUSTOM KEYBINDS"); + GUILayout.Label(L("Menu toggle is configurable. Right Shift stays disabled.", "Кнопку меню можно менять. Right Shift выключен."), menuDescStyle); + GUILayout.Space(10); + + DrawKeybindRow("Menu Toggle:", ref menuToggleKey, ref isWaitingForBind); + DrawKeybindRow("Magnet Cursor:", ref bindMagnetCursor, ref isWaitBindMagnetCursor); + DrawKeybindRow("Mass Morph:", ref bindMassMorph, ref isWaitBindMassMorph); + DrawKeybindRow("Spawn Lobby:", ref bindSpawnLobby, ref isWaitBindSpawnLobby); + DrawKeybindRow("Despawn Lobby:", ref bindDespawnLobby, ref isWaitBindDespawnLobby); + DrawKeybindRow("Close Meeting:", ref bindCloseMeeting, ref isWaitBindCloseMeeting); + DrawKeybindRow("Insta Start:", ref bindInstaStart, ref isWaitBindInstaStart); + DrawKeybindRow("End: Crewmate Win:", ref bindEndCrew, ref isWaitBindEndCrew); + DrawKeybindRow("End: Impostor Win:", ref bindEndImp, ref isWaitBindEndImp); + DrawKeybindRow("End: Imp Disconnect:", ref bindEndImpDC, ref isWaitBindEndImpDC); + DrawKeybindRow("End: H&S Disconnect:", ref bindEndHnsDC, ref isWaitBindEndHnsDC); + DrawKeybindRow("Toggle Tracers:", ref bindToggleTracers, ref isWaitBindToggleTracers); + DrawKeybindRow("Toggle NoClip:", ref bindToggleNoClip, ref isWaitBindToggleNoClip); + DrawKeybindRow("Toggle Freecam:", ref bindToggleFreecam, ref isWaitBindToggleFreecam); + DrawKeybindRow("Toggle Camera Zoom:", ref bindToggleCameraZoom, ref isWaitBindToggleCameraZoom); + DrawKeybindRow("Toggle Player Info:", ref bindTogglePlayerInfo, ref isWaitBindTogglePlayerInfo); + DrawKeybindRow("Toggle See Roles:", ref bindToggleSeeRoles, ref isWaitBindToggleSeeRoles); + DrawKeybindRow("Toggle See Ghosts:", ref bindToggleSeeGhosts, ref isWaitBindToggleSeeGhosts); + DrawKeybindRow("Toggle Full Bright:", ref bindToggleFullBright, ref isWaitBindToggleFullBright); + DrawKeybindRow("Kill All:", ref bindKillAll, ref isWaitBindKillAll); + DrawKeybindRow("Call Meeting:", ref bindCallMeeting, ref isWaitBindCallMeeting); + DrawKeybindRow("Kick All:", ref bindKickAll, ref isWaitBindKickAll); + DrawKeybindRow("Fix Sabotages:", ref bindFixSabotages, ref isWaitBindFixSabotages); + DrawKeybindRow("Ghost All:", ref bindSetAllGhost, ref isWaitBindSetAllGhost); + DrawKeybindRow("Revive All:", ref bindReviveAll, ref isWaitBindReviveAll); + DrawKeybindRow("All -> Ghost Imp:", ref bindSetAllGhostImp, ref isWaitBindSetAllGhostImp); + } + finally { GUILayout.EndVertical(); } + } + + private void DrawKeybindRow(string label, ref KeyCode currentKey, ref bool isWaiting) + { + GUILayout.BeginHorizontal(); + GUILayout.Space(10); + GUIStyle alignedLabel = new GUIStyle(toggleLabelStyle) { alignment = TextAnchor.MiddleLeft, margin = CreateRectOffset(0, 0, 4, 0) }; + GUILayout.Label(label, alignedLabel, GUILayout.Width(220), GUILayout.Height(25)); + + string bindText = isWaiting ? "Press any key..." : (currentKey == KeyCode.None ? "NONE" : currentKey.ToString()); + if (GUILayout.Button(bindText, isWaiting ? activeTabStyle : btnStyle, GUILayout.Width(120), GUILayout.Height(25))) + { + ResetAllBindWaits(); + isWaiting = true; + } + + if (GUILayout.Button("Clear", btnStyle, GUILayout.Width(50), GUILayout.Height(25))) + { + currentKey = KeyCode.None; + isWaiting = false; + SaveConfig(); + } + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + } + public static bool AnimShieldsEnabled = false; + +public static bool AnimAsteroidsEnabled = false; + +public static bool AnimCamsInUseEnabled = false; + +public static bool IsScanning = false; + +private void ResetAllBindWaits() + { + isWaitingForBind = false; + isWaitBindMassMorph = false; + isWaitBindSpawnLobby = false; + isWaitBindDespawnLobby = false; + isWaitBindCloseMeeting = false; + isWaitBindInstaStart = false; + isWaitBindEndCrew = false; + isWaitBindEndImp = false; + isWaitBindEndImpDC = false; + isWaitBindEndHnsDC = false; + isWaitBindMagnetCursor = false; + isWaitBindToggleTracers = false; + isWaitBindToggleNoClip = false; + isWaitBindToggleFreecam = false; + isWaitBindToggleCameraZoom = false; + isWaitBindKillAll = false; + isWaitBindCallMeeting = false; + isWaitBindTogglePlayerInfo = false; + isWaitBindToggleSeeRoles = false; + isWaitBindToggleSeeGhosts = false; + isWaitBindToggleFullBright = false; + isWaitBindKickAll = false; + isWaitBindFixSabotages = false; + isWaitBindSetAllGhost = false; + isWaitBindSetAllGhostImp = false; + isWaitBindReviveAll = false; + } + +private void DrawGeneralTab() + { + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader(L("GENERAL", "ГЛАВНОЕ")); + GUILayout.Space(4); + + GUILayout.BeginHorizontal(); + for (int i = 0; i < generalSubTabs.Length; i++) + { + if (GUILayout.Button(generalSubTabs[i], currentGeneralSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(22))) + { + currentGeneralSubTab = i; + scrollPosition = Vector2.zero; + } + } + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(8); + + if (currentGeneralSubTab == 0) DrawGeneralInfoTab(); + else if (currentGeneralSubTab == 1) DrawBindsTab(); + } +} +} diff --git a/features/MenuTheme.cs b/features/MenuTheme.cs new file mode 100644 index 0000000..fc78526 --- /dev/null +++ b/features/MenuTheme.cs @@ -0,0 +1,424 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +private void LoadConfig() + { + try + { + spoofLevelString = Plugin.SpoofedLevel.Value; + enableLevelSpoof = LoadBool("M_EnableLevelSpoof", Plugin.EnableLevelSpoofConfig.Value); + enableFriendCodeSpoof = Plugin.EnableFriendCodeSpoofConfig.Value; + spoofFriendCodeInput = Plugin.SpoofFriendCodeConfig.Value; + enablePlatformSpoof = Plugin.EnablePlatformSpoof.Value; + autoBanBrokenFriendCode = Plugin.AutoBanBrokenFriendCodeConfig.Value; + currentPlatformIndex = Plugin.PlatformIndex.Value; + showWatermark = Plugin.ShowWatermarkConfig.Value; + unlockCosmetics = Plugin.UnlockCosmeticsConfig.Value; + moreLobbyInfo = Plugin.MoreLobbyInfoConfig.Value; + enableChatDarkMode = Plugin.EnableChatDarkModeConfig.Value; + ghostChatColorHex = SanitizeHexColor(Plugin.GhostChatColorConfig.Value, "#D7B8FF"); + enableAnomalyLogReports = Plugin.EnableAnomalyLogReportsConfig.Value; + showEspFriendCode = Plugin.ShowEspFriendCodeConfig.Value; + rpcSpoofDelay = Plugin.RpcSpoofDelayConfig.Value; + currentMenuColorIndex = Plugin.MenuColorIndexConfig.Value; + rgbMenuMode = Plugin.RgbMenuModeConfig.Value; + rgbMenuText = LoadBool("M_RgbMenuText", Plugin.RgbMenuTextConfig.Value); + boldMenuText = LoadBool("M_BoldMenuText", Plugin.BoldMenuTextConfig.Value); + whiteMenuTheme = LoadBool("M_WhiteTheme", whiteMenuTheme); + currentMenuLanguageIndex = Mathf.Clamp(LoadInt("M_MenuLanguageIndex", currentMenuLanguageIndex), 0, menuLanguageNames.Length - 1); + fpsLimit = Mathf.Clamp(LoadInt("M_FpsLimit", fpsLimit), 60, 240); + autoCopyCodeAndLeave = LoadBool("M_AutoCopyCodeAndLeave", autoCopyCodeAndLeave); + ApplyFpsLimit(); + autoKickBugs = LoadBool("M_AutoKickBugs", autoKickBugs); + if (PlayerPrefs.HasKey("M_AutoKickTimer")) autoKickTimer = PlayerPrefs.GetFloat("M_AutoKickTimer"); + disableVoteKicks = LoadBool("M_DisableVoteKicks", disableVoteKicks); + enableLocalNameSpoof = LoadBool("M_LocalNameSpoof", enableLocalNameSpoof); + enableLocalFriendCodeSpoof = LoadBool("M_LocalFakeFCEnabled", enableLocalFriendCodeSpoof); + if (PlayerPrefs.HasKey("M_LocalFakeFC")) localFriendCodeInput = PlayerPrefs.GetString("M_LocalFakeFC"); + if (PlayerPrefs.HasKey("M_BndMagnet")) bindMagnetCursor = (KeyCode)PlayerPrefs.GetInt("M_BndMagnet"); + menuToggleKey = Plugin.MenuKeybind.Value == KeyCode.None ? KeyCode.Insert : Plugin.MenuKeybind.Value; + if (PlayerPrefs.HasKey("M_MenuToggleKey")) menuToggleKey = (KeyCode)PlayerPrefs.GetInt("M_MenuToggleKey"); + if (menuToggleKey == KeyCode.None) menuToggleKey = KeyCode.Insert; + if (PlayerPrefs.HasKey("M_BndMMorph")) bindMassMorph = (KeyCode)PlayerPrefs.GetInt("M_BndMMorph"); + if (PlayerPrefs.HasKey("M_BndSpawn")) bindSpawnLobby = (KeyCode)PlayerPrefs.GetInt("M_BndSpawn"); + if (PlayerPrefs.HasKey("M_BndDespawn")) bindDespawnLobby = (KeyCode)PlayerPrefs.GetInt("M_BndDespawn"); + if (PlayerPrefs.HasKey("M_BndCloseMtg")) bindCloseMeeting = (KeyCode)PlayerPrefs.GetInt("M_BndCloseMtg"); + if (PlayerPrefs.HasKey("M_BndInstaStart")) bindInstaStart = (KeyCode)PlayerPrefs.GetInt("M_BndInstaStart"); + if (PlayerPrefs.HasKey("M_BndEndCrew")) bindEndCrew = (KeyCode)PlayerPrefs.GetInt("M_BndEndCrew"); + if (PlayerPrefs.HasKey("M_BndEndImp")) bindEndImp = (KeyCode)PlayerPrefs.GetInt("M_BndEndImp"); + if (PlayerPrefs.HasKey("M_BndEndImpDC")) bindEndImpDC = (KeyCode)PlayerPrefs.GetInt("M_BndEndImpDC"); + if (PlayerPrefs.HasKey("M_BndEndHnsDC")) bindEndHnsDC = (KeyCode)PlayerPrefs.GetInt("M_BndEndHnsDC"); + if (PlayerPrefs.HasKey("M_BndToggleTracers")) bindToggleTracers = (KeyCode)PlayerPrefs.GetInt("M_BndToggleTracers"); + if (PlayerPrefs.HasKey("M_BndToggleNoClip")) bindToggleNoClip = (KeyCode)PlayerPrefs.GetInt("M_BndToggleNoClip"); + if (PlayerPrefs.HasKey("M_BndToggleFreecam")) bindToggleFreecam = (KeyCode)PlayerPrefs.GetInt("M_BndToggleFreecam"); + if (PlayerPrefs.HasKey("M_BndToggleCameraZoom")) bindToggleCameraZoom = (KeyCode)PlayerPrefs.GetInt("M_BndToggleCameraZoom"); + if (PlayerPrefs.HasKey("M_BndKillAll")) bindKillAll = (KeyCode)PlayerPrefs.GetInt("M_BndKillAll"); + if (PlayerPrefs.HasKey("M_BndCallMeeting")) bindCallMeeting = (KeyCode)PlayerPrefs.GetInt("M_BndCallMeeting"); + if (PlayerPrefs.HasKey("M_BndTogglePlayerInfo")) bindTogglePlayerInfo = (KeyCode)PlayerPrefs.GetInt("M_BndTogglePlayerInfo"); + if (PlayerPrefs.HasKey("M_BndToggleSeeRoles")) bindToggleSeeRoles = (KeyCode)PlayerPrefs.GetInt("M_BndToggleSeeRoles"); + if (PlayerPrefs.HasKey("M_BndToggleSeeGhosts")) bindToggleSeeGhosts = (KeyCode)PlayerPrefs.GetInt("M_BndToggleSeeGhosts"); + if (PlayerPrefs.HasKey("M_BndToggleFullBright")) bindToggleFullBright = (KeyCode)PlayerPrefs.GetInt("M_BndToggleFullBright"); + if (PlayerPrefs.HasKey("M_BndKickAll")) bindKickAll = (KeyCode)PlayerPrefs.GetInt("M_BndKickAll"); + if (PlayerPrefs.HasKey("M_BndFixSabotages")) bindFixSabotages = (KeyCode)PlayerPrefs.GetInt("M_BndFixSabotages"); + if (PlayerPrefs.HasKey("M_BndSetAllGhost")) bindSetAllGhost = (KeyCode)PlayerPrefs.GetInt("M_BndSetAllGhost"); + if (PlayerPrefs.HasKey("M_BndSetAllGhostImp")) bindSetAllGhostImp = (KeyCode)PlayerPrefs.GetInt("M_BndSetAllGhostImp"); + if (PlayerPrefs.HasKey("M_BndReviveAll")) bindReviveAll = (KeyCode)PlayerPrefs.GetInt("M_BndReviveAll"); + + if (!rgbMenuMode && currentMenuColorIndex >= 0 && currentMenuColorIndex < menuColors.Length) + { + currentAccentColor = menuColors[currentMenuColorIndex]; + } + + showPlayerInfo = LoadBool("M_ShowPlayerInfo", showPlayerInfo); + seeGhosts = LoadBool("M_SeeGhosts", seeGhosts); + seeRoles = LoadBool("M_SeeRoles", seeRoles); + revealMeetingRoles = LoadBool("M_RevealMeetingRoles", revealMeetingRoles); + showTracers = LoadBool("M_ShowTracers", showTracers); + fullBright = LoadBool("M_FullBright", fullBright); + seeProtections = LoadBool("M_SeeProtections", seeProtections); + seeKillCooldown = LoadBool("M_SeeKillCooldown", seeKillCooldown); + extendedLobby = LoadBool("M_ExtendedLobby", extendedLobby); + moreLobbyInfo = LoadBool("M_MoreLobbyInfo", moreLobbyInfo); + alwaysChat = LoadBool("M_AlwaysChat", alwaysChat); + lobbyRainbowAll = LoadBool("M_LobbyRainbowAll", lobbyRainbowAll); + lobbyAllColor = LoadBool("M_LobbyAllColor", lobbyAllColor); + lobbyAllColorId = Mathf.Clamp(LoadInt("M_LobbyAllColorId", lobbyAllColorId), 0, MaxOutfitColorId()); + readGhostChat = LoadBool("M_ReadGhostChat", readGhostChat); + enableExtendedChat = LoadBool("M_EnableExtendedChat", enableExtendedChat); + enableFastChat = LoadBool("M_EnableFastChat", enableFastChat); + allowLinksAndSymbols = LoadBool("M_AllowLinksAndSymbols", allowLinksAndSymbols); + enableChatHistory = LoadBool("M_EnableChatHistory", enableChatHistory); + chatHistoryLimit = Mathf.Clamp(LoadInt("M_ChatHistoryLimit", chatHistoryLimit), 5, 80); + enableClipboard = LoadBool("M_EnableClipboard", enableClipboard); + enableChatMessageDoubleClickCopy = LoadBool("M_EnableChatMessageDoubleClickCopy", enableChatMessageDoubleClickCopy); + enableChatNameColorCopy = LoadBool("M_EnableChatNameColorCopy", enableChatNameColorCopy); + enableChatLog = LoadBool("M_EnableChatLog", enableChatLog); + enableColorCommand = LoadBool("M_EnableColorCommand", enableColorCommand); + blockRainbowChat = LoadBool("M_BlockRainbowChat", blockRainbowChat); + blockFortegreenChat = LoadBool("M_BlockFortegreenChat", blockFortegreenChat); + SpoofMenuEnabled = LoadBool("M_SpoofMenuEnabled", SpoofMenuEnabled); + noClip = LoadBool("M_NoClip", noClip); + tpToCursor = LoadBool("M_TpToCursor", tpToCursor); + dragToCursor = LoadBool("M_DragToCursor", dragToCursor); + autoFollowCursor = LoadBool("M_AutoFollowCursor", autoFollowCursor); + freecam = LoadBool("M_Freecam", freecam); + cameraZoom = LoadBool("M_CameraZoom", cameraZoom); + RevealVotesEnabled = LoadBool("M_RevealVotes", RevealVotesEnabled); + noTaskMode = LoadBool("M_NoTaskMode", noTaskMode); + noMapCooldowns = LoadBool("M_NoMapCooldowns", noMapCooldowns); + unlockVents = LoadBool("M_UnlockVents", unlockVents); + walkInVents = LoadBool("M_WalkInVents", walkInVents); + allowTasksAsImpostor = LoadBool("M_AllowTasksAsImpostor", allowTasksAsImpostor); + killWhileVanishedHostOnly = LoadBool("M_KillWhileVanishedHostOnly", killWhileVanishedHostOnly); + roleBuffImmortality = LoadBool("M_RoleBuffImmortality", roleBuffImmortality); + neverEndGame = LoadBool("M_NeverEndGame", neverEndGame); + removePenalty = LoadBool("M_RemovePenalty", removePenalty); + alwaysShowLobbyTimer = LoadBool("M_AlwaysShowLobbyTimer", alwaysShowLobbyTimer); + autoBanEnabled = LoadBool("M_AutoBanEnabled", autoBanEnabled); + allowDuplicateColors = LoadBool("M_AllowDuplicateColors", allowDuplicateColors); + blockSpoofRPC = LoadBool("M_BlockSpoofRPC", blockSpoofRPC); + autoBanPlatformSpoof = LoadBool("M_AutoBanPlatformSpoof", autoBanPlatformSpoof); + banCustomPlatformsFromTxt = LoadBool("M_BanCustomPlatformsFromTxt", banCustomPlatformsFromTxt); + autoKickLowLevelEnabled = LoadBool("M_AutoKickLowLevel", autoKickLowLevelEnabled); + autoKickMinLevel = Mathf.Clamp(LoadInt("M_AutoKickMinLevel", autoKickMinLevel), 1, 300); + blockSabotageRPC = LoadBool("M_BlockSabotageRPC", blockSabotageRPC); + punishmentMode = Mathf.Clamp(LoadInt("M_PunishmentMode", punishmentMode), 0, punishmentNames.Length - 1); + blockGameRpcInLobby = LoadBool("M_BlockGameRpcInLobby", blockGameRpcInLobby); + blockChatFloodRpc = LoadBool("M_BlockChatFloodRpc", blockChatFloodRpc); + blockMeetingFloodRpc = LoadBool("M_BlockMeetingFloodRpc", blockMeetingFloodRpc); + unfixableLights = LoadBool("M_UnfixableLights", unfixableLights); + enablePasosLimit = LoadBool("M_PasosLimit", enablePasosLimit); + enableLocalPasosBan = LoadBool("M_AntiPasosLocalBan", enableLocalPasosBan); + enableHostPasosBan = LoadBool("M_AntiPasosHostBan", enableHostPasosBan); + enableMalformedPacketGuard = LoadBool("M_MalformedPacketGuard", enableMalformedPacketGuard); + banMalformedPacketSender = LoadBool("M_BanMalformedPacketSender", banMalformedPacketSender); + enableQuickChatEmptyGuard = LoadBool("M_QuickChatEmptyGuard", enableQuickChatEmptyGuard); + banQuickChatEmptySpammer = LoadBool("M_BanQuickChatEmptySpammer", banQuickChatEmptySpammer); + enableUnownedSpawnGuard = LoadBool("M_UnownedSpawnGuard", enableUnownedSpawnGuard); + AutoHostEnabled = LoadBool("M_AutoHostEnabled", AutoHostEnabled); + AutoReturnLobbyAfterMatch = LoadBool("M_AutoReturnLobbyAfterMatch", AutoReturnLobbyAfterMatch); + AutoHostNotifications = LoadBool("M_AutoHostNotifications", AutoHostNotifications); + AutoHostForceLastMinute = LoadBool("M_AutoHostForceLastMinute", AutoHostForceLastMinute); + AutoHostWaitLoadedPlayers = LoadBool("M_AutoHostWaitLoadedPlayers", AutoHostWaitLoadedPlayers); + AutoHostCancelBelowMin = LoadBool("M_AutoHostCancelBelowMin", AutoHostCancelBelowMin); + AutoHostInstantStart = LoadBool("M_AutoHostInstantStart", AutoHostInstantStart); + autoGhostAfterStart = LoadBool("M_AutoGhostAfterStart", autoGhostAfterStart); + if (PlayerPrefs.HasKey("M_AutoHostMinPlayers")) AutoHostMinPlayers = PlayerPrefs.GetInt("M_AutoHostMinPlayers"); + if (PlayerPrefs.HasKey("M_AutoHostStartDelaySeconds")) AutoHostStartDelaySeconds = PlayerPrefs.GetFloat("M_AutoHostStartDelaySeconds"); + if (PlayerPrefs.HasKey("M_AutoHostFastStartPlayers")) AutoHostFastStartPlayers = PlayerPrefs.GetInt("M_AutoHostFastStartPlayers"); + if (PlayerPrefs.HasKey("M_AutoHostFastStartDelaySeconds")) AutoHostFastStartDelaySeconds = PlayerPrefs.GetFloat("M_AutoHostFastStartDelaySeconds"); + if (PlayerPrefs.HasKey("M_WalkSpeed")) walkSpeed = PlayerPrefs.GetFloat("M_WalkSpeed"); + if (PlayerPrefs.HasKey("M_EngineSpeed")) engineSpeed = PlayerPrefs.GetFloat("M_EngineSpeed"); + for (int i = 0; i < favoriteOutfitSlots.Length; i++) + favoriteOutfitSlots[i] = PlayerPrefs.GetString($"M_FavoriteOutfit_{i}", string.Empty); + enableBackground = LoadBool("M_EnableBackground", enableBackground); + hardMenu = LoadBool("M_HardMenu", hardMenu); + EnableCustomNotifs = LoadBool("M_EnableCustomNotifs", EnableCustomNotifs); + LogAllRPCs = LoadBool("M_LogAllRPCs", LogAllRPCs); + selectedSpoofMenuIndex = Mathf.Clamp(LoadInt("M_SelectedSpoofMenuIndex", selectedSpoofMenuIndex), 0, spoofMenuNames.Length - 1); + windowRect = new Rect( + LoadFloat("M_MenuWindowX", windowRect.x), + LoadFloat("M_MenuWindowY", windowRect.y), + Mathf.Clamp(LoadFloat("M_MenuWindowW", windowRect.width), 640f, 1400f), + Mathf.Clamp(LoadFloat("M_MenuWindowH", windowRect.height), 420f, 900f)); + currentTab = Mathf.Clamp(LoadInt("M_CurrentTab", currentTab), 0, tabNames.Length - 1); + targetTabIndex = Mathf.Clamp(LoadInt("M_TargetTab", currentTab), 0, tabNames.Length - 1); + currentGeneralSubTab = Mathf.Clamp(LoadInt("M_CurrentGeneralSubTab", currentGeneralSubTab), 0, generalSubTabs.Length - 1); + currentGeneralInfoSubTab = Mathf.Clamp(LoadInt("M_CurrentGeneralInfoSubTab", currentGeneralInfoSubTab), 0, generalInfoSubTabs.Length - 1); + currentSelfSubTab = Mathf.Clamp(LoadInt("M_CurrentSelfSubTab", currentSelfSubTab), 0, selfSubTabs.Length); + currentVisualsSubTab = Mathf.Clamp(LoadInt("M_CurrentVisualsSubTab", currentVisualsSubTab), 0, visualsSubTabs.Length - 1); + currentPlayersSubTab = Mathf.Clamp(LoadInt("M_CurrentPlayersSubTab", currentPlayersSubTab), 0, playersSubTabs.Length - 1); + currentHostOnlySubTab = Mathf.Clamp(LoadInt("M_CurrentHostOnlySubTab", currentHostOnlySubTab), 0, hostOnlySubTabs.Length - 1); + currentAutoHostSubTab = Mathf.Clamp(LoadInt("M_CurrentAutoHostSubTab", currentAutoHostSubTab), 0, autoHostSubTabs.Length - 1); + tabTransitionProgress = 1f; + SyncKeybindDictionary(); + if (PlayerPrefs.HasKey("M_SpoofName")) customNameInput = PlayerPrefs.GetString("M_SpoofName"); + } + catch { } + } + +private static void ApplyFpsLimit() + { + try + { + fpsLimit = Mathf.Clamp(fpsLimit, 60, 240); + if (lastAppliedFpsLimit == fpsLimit) return; + Application.targetFrameRate = fpsLimit; + QualitySettings.vSyncCount = 0; + lastAppliedFpsLimit = fpsLimit; + } + catch { } + } + +private static void TrimChatHistoryToLimit() + { + try + { + chatHistoryLimit = Mathf.Clamp(chatHistoryLimit, 5, 80); + while (ChatHistory.sentMessages.Count > chatHistoryLimit) + ChatHistory.sentMessages.RemoveAt(0); + + ChatHistory.HistoryIndex = Mathf.Clamp(ChatHistory.HistoryIndex, 0, ChatHistory.sentMessages.Count); + } + catch { } + } + +private static void SyncKeybindDictionary() + { + try + { + keyBinds["Toggle Menu"] = menuToggleKey; + keyBinds["Magnet Cursor"] = bindMagnetCursor; + keyBinds["Mass Morph"] = bindMassMorph; + keyBinds["Spawn Lobby"] = bindSpawnLobby; + keyBinds["Despawn Lobby"] = bindDespawnLobby; + keyBinds["Close Meeting"] = bindCloseMeeting; + keyBinds["Insta Start"] = bindInstaStart; + keyBinds["End Crew"] = bindEndCrew; + keyBinds["End Imp"] = bindEndImp; + keyBinds["End Imp DC"] = bindEndImpDC; + keyBinds["End H&S DC"] = bindEndHnsDC; + keyBinds["Toggle Tracers"] = bindToggleTracers; + keyBinds["Toggle NoClip"] = bindToggleNoClip; + keyBinds["Toggle Freecam"] = bindToggleFreecam; + keyBinds["Toggle Camera Zoom"] = bindToggleCameraZoom; + keyBinds["Toggle Player Info"] = bindTogglePlayerInfo; + keyBinds["Toggle See Roles"] = bindToggleSeeRoles; + keyBinds["Toggle See Ghosts"] = bindToggleSeeGhosts; + keyBinds["Toggle Full Bright"] = bindToggleFullBright; + keyBinds["Kill All"] = bindKillAll; + keyBinds["Call Meeting"] = bindCallMeeting; + keyBinds["Kick All"] = bindKickAll; + keyBinds["Fix Sabotages"] = bindFixSabotages; + keyBinds["All Ghost"] = bindSetAllGhost; + keyBinds["All Ghost Imp"] = bindSetAllGhostImp; + keyBinds["Revive All"] = bindReviveAll; + } + catch { } + } + +private Texture2D MakeRoundedTex(int size, Color col, float radius) + { + Texture2D result = new Texture2D(size, size, TextureFormat.RGBA32, false); + result.hideFlags = HideFlags.HideAndDontSave; + Color[] pix = new Color[size * size]; + float center = size / 2f; + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + float dx = Mathf.Max(0, Mathf.Abs(x - center + 0.5f) - (center - radius)); + float dy = Mathf.Max(0, Mathf.Abs(y - center + 0.5f) - (center - radius)); + float dist = Mathf.Sqrt(dx * dx + dy * dy); + float alpha = Mathf.Clamp01(radius - dist + 0.5f); + Color c = col; + c.a = col.a * alpha; + pix[y * size + x] = c; + } + } + result.SetPixels(pix); result.Apply(); + return result; + } + +private RectOffset CreateRectOffset(int left, int right, int top, int bottom) + { + return new RectOffset { left = left, right = right, top = top, bottom = bottom }; + } + +private void UpdateSwitchTex(Texture2D tex, bool isOn, Color accentColor) + { + int width = tex.width; int height = tex.height; + Color transparent = new Color(0, 0, 0, 0); + Color offBg = new Color(0.23f, 0.23f, 0.23f, 1f); + Color offKnob = new Color(0.6f, 0.6f, 0.6f, 1f); + Color bgColor = isOn ? accentColor : offBg; + Color knobColor = isOn ? Color.white : offKnob; + float r = height / 2f; + float cx1 = r; float cx2 = width - r; float cy = r; + float knobRadius = r - 2f; + float knobCx = isOn ? cx2 : cx1; + Color[] pixels = new Color[width * height]; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + float dLeft = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx1, cy)); + float dRight = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx2, cy)); + float dRect = (x + 0.5f >= cx1 && x + 0.5f <= cx2) ? Mathf.Abs((y + 0.5f) - cy) : 9999f; + float distBg = Mathf.Min(dLeft, Mathf.Min(dRight, dRect)); + float alphaBg = Mathf.Clamp01(r - distBg + 0.5f); + float distKnob = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(knobCx, cy)); + float alphaKnob = Mathf.Clamp01(knobRadius - distKnob + 0.5f); + if (alphaBg > 0) + { + Color finalCol = Color.Lerp(bgColor, knobColor, alphaKnob); + finalCol.a = alphaBg; + pixels[y * width + x] = finalCol; + } + else pixels[y * width + x] = transparent; + } + } + tex.SetPixels(pixels); tex.Apply(); + } + +private void UpdateTrackTex(Texture2D tex, bool isOn, Color accentColor) + { + int width = tex.width; int height = tex.height; + Color transparent = new Color(0, 0, 0, 0); + Color offBg = new Color(0.23f, 0.23f, 0.23f, 1f); + Color bgColor = isOn ? accentColor : offBg; + float r = height / 2f; + float cx1 = r; float cx2 = width - r; float cy = r; + Color[] pixels = new Color[width * height]; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + float dLeft = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx1, cy)); + float dRight = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx2, cy)); + float dRect = (x + 0.5f >= cx1 && x + 0.5f <= cx2) ? Mathf.Abs((y + 0.5f) - cy) : 9999f; + float distBg = Mathf.Min(dLeft, Mathf.Min(dRight, dRect)); + float alphaBg = Mathf.Clamp01(r - distBg + 0.5f); + if (alphaBg > 0) + { + Color finalCol = bgColor; + finalCol.a = alphaBg; + pixels[y * width + x] = finalCol; + } + else pixels[y * width + x] = transparent; + } + } + tex.SetPixels(pixels); tex.Apply(); + } + +private static Color GetThemeAccentColor(Color source) + { + if (!whiteMenuTheme) return source; + + Color.RGBToHSV(source, out float h, out float s, out float v); + + if (s < 0.08f) + return new Color(0.34f, 0.34f, 0.34f, 1f); + + if (rgbMenuMode) + { + float rgbS = Mathf.Clamp(Mathf.Max(s, 0.62f), 0.62f, 0.9f); + float rgbV = Mathf.Clamp(v * 0.58f, 0.34f, 0.68f); + Color rgbMapped = Color.HSVToRGB(h, rgbS, rgbV); + rgbMapped.a = 1f; + return rgbMapped; + } + + if (h <= 0.04f || h >= 0.96f) + return new Color(0.50f, 0.14f, 0.18f, 1f); + + if (h >= 0.11f && h <= 0.19f) + return new Color32(232, 194, 37, 255); + + float targetS = Mathf.Clamp(Mathf.Max(s, 0.55f), 0.55f, 0.95f); + float targetV = Mathf.Clamp(v * 0.62f, 0.26f, 0.72f); + Color mapped = Color.HSVToRGB(h, targetS, targetV); + mapped.a = 1f; + return mapped; + } + +private static bool RgbMenuTextActive() + { + return rgbMenuMode && rgbMenuText; + } + +private static Color GetStableMenuAccentSource() + { + try + { + if (activeGui != null && activeGui.menuColors != null && activeGui.menuColors.Length > 0) + return activeGui.menuColors[Mathf.Clamp(activeGui.currentMenuColorIndex, 0, activeGui.menuColors.Length - 1)]; + } + catch { } + + return currentAccentColor; + } +} +} diff --git a/features/PlayerActionPanel.cs b/features/PlayerActionPanel.cs new file mode 100644 index 0000000..9c4d169 --- /dev/null +++ b/features/PlayerActionPanel.cs @@ -0,0 +1,425 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +public static void MakePlayerGhost(PlayerControl target, bool impostorGhost = false, bool notify = true) + { + if (target == null || target.Data == null) + { + if (notify) ShowNotification("[ОШИБКА] Цель не найдена!"); + return; + } + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + if (notify) ShowNotification("[ОШИБКА] Требуются права хоста!"); + return; + } + if (target.Data.IsDead) + { + if (!TrySetGhostRole(target, impostorGhost, out _)) + SetPlayerRole(target, impostorGhost ? RoleTypes.Impostor : (IsImpostorTeamRole(target.Data.RoleType) ? RoleTypes.Impostor : RoleTypes.Crewmate)); + if (notify) ShowNotification($"{target.Data.PlayerName} уже призрак!"); + return; + } + + try + { + string methodUsed; + if (!TrySetGhostRole(target, impostorGhost, out methodUsed)) + { + RoleTypes fallbackRole = impostorGhost ? RoleTypes.Impostor : (IsImpostorTeamRole(target.Data.RoleType) ? RoleTypes.Impostor : RoleTypes.Crewmate); + SetPlayerRole(target, fallbackRole); + TryActivateGhostState(target, out methodUsed); + } + + var netObj = GameData.Instance?.GetComponent(); + if (netObj != null) netObj.SetDirtyBit(uint.MaxValue); + + if (notify) ShowNotification($"[GHOST] {target.Data.PlayerName} стал призраком ({methodUsed})!"); + } + catch (Exception) + { + if (notify) ShowNotification("Ошибка перевода в призрака!"); + } + } + +private static bool TrySetGhostRole(PlayerControl target, bool impostorGhost, out string methodUsed) + { + methodUsed = string.Empty; + if (target == null || target.Data == null) return false; + + string[] roleNames = impostorGhost + ? new[] { "ImpostorGhost", "GhostImpostor", "ImpGhost", "Ghost" } + : new[] { "CrewmateGhost", "GhostCrewmate", "CrewGhost", "Ghost" }; + + foreach (string roleName in roleNames) + { + if (!Enum.TryParse(roleName, true, out RoleTypes ghostRole)) continue; + + try { target.RpcSetRole(ghostRole, true); } catch { } + try { RoleManager.Instance?.SetRole(target, ghostRole); } catch { } + + methodUsed = $"SetRole:{roleName}"; + return true; + } + + return false; + } + +private static bool TryActivateGhostState(PlayerControl target, out string methodUsed) + { + methodUsed = string.Empty; + if (target == null) return false; + if (target.Data != null && target.Data.IsDead) + { + methodUsed = "already_dead"; + return true; + } + + if (TryDie(target, DeathReason.Exile, true) || + TryDie(target, DeathReason.Exile, false) || + TryDie(target, DeathReason.Kill, true) || + TryDie(target, DeathReason.Kill, false)) + { + methodUsed = "Die"; + return true; + } + + if (TryInvokeNoArg(target, "Exiled") || + TryInvokeNoArg(target, "RpcExiled") || + TryInvokeNoArg(target, "RpcExiledV2") || + TryInvokeNoArg(target, "SetDead")) + { + methodUsed = "Exiled/SetDead"; + return true; + } + + if (TrySetDeadFlag(target)) + { + methodUsed = "Data.IsDead"; + return true; + } + + methodUsed = "fallback"; + return false; + } + +private static bool TryDie(PlayerControl target, DeathReason reason, bool allowAnimation) + { + try { target.Die(reason, allowAnimation); } + catch { } + return target != null && target.Data != null && target.Data.IsDead; + } + +private static bool TryInvokeNoArg(object target, string methodName) + { + if (target == null || string.IsNullOrWhiteSpace(methodName)) return false; + try + { + for (Type type = target.GetType(); type != null; type = type.BaseType) + { + MethodInfo method = type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); + if (method == null) continue; + method.Invoke(target, null); + return target is PlayerControl player && player.Data != null && player.Data.IsDead; + } + } + catch { } + return false; + } + +private static bool TrySetDeadFlag(PlayerControl target) + { + if (target == null || target.Data == null) return false; + try + { + target.Data.IsDead = true; + if (target.Collider != null) target.Collider.enabled = false; + if (target.MyPhysics != null) target.MyPhysics.gameObject.layer = LayerMask.NameToLayer("Ghost"); + } + catch { } + return target.Data.IsDead; + } + +public static void SetAllPlayersGhost() + { + SetAllPlayersGhost(false); + } + +public static void SetAllPlayersGhost(bool impostorGhost) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ОШИБКА] Требуются права хоста!"); + return; + } + if (PlayerControl.AllPlayerControls == null) return; + + int count = 0; + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + MakePlayerGhost(pc, impostorGhost, false); + count++; + } + } + + ShowNotification($"[GHOST] {count} игрок(а/ов) стали {(impostorGhost ? "ghost impostor" : "призраками")}!"); + } + +public static void ReviveAllPlayers() + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ERROR] Host required!"); + return; + } + if (PlayerControl.AllPlayerControls == null) return; + + int count = 0; + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.Data == null || pc.Data.Disconnected || !pc.Data.IsDead) + continue; + + try + { + pc.Data.IsDead = false; + + if (pc.Collider != null) pc.Collider.enabled = true; + if (pc.MyPhysics != null) + pc.MyPhysics.gameObject.layer = LayerMask.NameToLayer("Players"); + + try + { + var allBehaviours = UnityEngine.Object.FindObjectsOfType(); + foreach (var mb in allBehaviours) + { + if (mb == null || mb.gameObject == null) continue; + Type t = mb.GetType(); + if (t == null || t.Name != "DeadBody") continue; + + byte parentId = byte.MaxValue; + + var parentProp = t.GetProperty("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentProp != null) + { + object val = parentProp.GetValue(mb, null); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + else + { + var parentField = t.GetField("ParentId", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (parentField != null) + { + object val = parentField.GetValue(mb); + if (val is byte b) parentId = b; + else if (val is int i) parentId = (byte)i; + } + } + + if (parentId == pc.PlayerId) + mb.gameObject.SetActive(false); + } + } + catch { } + + bool wasImpTeam = false; + try + { + if (pc.Data.Role != null) + { + int roleId = (int)pc.Data.Role.Role; + wasImpTeam = roleId == 1 || roleId == 5 || roleId == 7 || roleId == 9 || roleId == 18; + } + else + { + var rt = pc.Data.RoleType; + wasImpTeam = rt == RoleTypes.Impostor || rt == RoleTypes.Shapeshifter || (int)rt == 9 || (int)rt == 18; + } + } + catch { } + + pc.RpcSetRole(wasImpTeam ? RoleTypes.Impostor : RoleTypes.Crewmate, true); + count++; + } + catch { } + } + + var netObj = GameData.Instance?.GetComponent(); + if (netObj != null) netObj.SetDirtyBit(uint.MaxValue); + + ShowNotification($"[REVIVE] {count} player(s) revived!"); + } + +public static void SetAllPlayersRole(RoleTypes role) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) + { + ShowNotification("[ОШИБКА] Требуются права хоста!"); + return; + } + if (PlayerControl.AllPlayerControls == null) return; + + int count = 0; + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + pc.RpcSetRole(role, true); + count++; + } + } + + ShowNotification($"[РОЛИ] {count} игрок(а/ов) получили роль {GetRoleDisplayName(role)}!"); + } + +public static void SetPlayerRole(PlayerControl target, RoleTypes newRole) + { + if (target == null || target.Data == null) return; + target.RpcSetRole(newRole, true); + } + +private void DrawRolesTab() + { + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(GUILayout.Width(280)); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Roles", headerStyle); + GUILayout.BeginHorizontal(); + GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetMenuAccentColor() } }; + if (GUILayout.Button("<", btnStyle, GUILayout.Width(25), GUILayout.Height(22))) { fakeRoleIdx--; if (fakeRoleIdx < 0) fakeRoleIdx = forceRoleOptions.Length - 1; } + GUILayout.Label(forceRoleOptions[fakeRoleIdx].ToString(), middleLabelStyle, GUILayout.Width(100), GUILayout.Height(22)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(25), GUILayout.Height(22))) { fakeRoleIdx++; if (fakeRoleIdx >= forceRoleOptions.Length) fakeRoleIdx = 0; } + GUILayout.Space(15); + if (GUILayout.Button("Set", activeTabStyle, GUILayout.Width(45), GUILayout.Height(22))) RoleManager.Instance?.SetRole(PlayerControl.LocalPlayer, forceRoleOptions[fakeRoleIdx]); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Impostor", headerStyle); + killReach = DrawToggle(killReach, "Kill Reach", 160); + GUILayout.Space(5); + killAnyone = DrawToggle(killAnyone, "Kill Anyone", 160); + GUILayout.Space(5); + killAuraHostOnly = DrawToggle(killAuraHostOnly, "Kill Aura", 160); + GUILayout.Space(5); + noKillCooldownHostOnly = DrawToggle(noKillCooldownHostOnly, "Kill Cooldown 0", 160); + GUILayout.Space(5); + killWhileVanishedHostOnly = DrawToggle(killWhileVanishedHostOnly, "Kill While Vanished", 160); + GUILayout.Space(5); + allowTasksAsImpostor = DrawToggle(allowTasksAsImpostor, "Allow Tasks (Imp)", 160); + GUILayout.Space(5); + spamReportBodies = DrawToggle(spamReportBodies, "Spam Report Bodies", 160); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Shapeshifter", headerStyle); + NoShapeshiftAnim = DrawToggle(NoShapeshiftAnim, "No Ss Animation", 160); + GUILayout.Space(5); + endlessSsDuration = DrawToggle(endlessSsDuration, "Endless Ss Duration", 160); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Tracker", headerStyle); + EndlessTracking = DrawToggle(EndlessTracking, "Endless Tracking", 160); + GUILayout.Space(5); + NoTrackingCooldown = DrawToggle(NoTrackingCooldown, "No Track Cooldown", 160); + GUILayout.EndVertical(); + + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.BeginVertical(GUILayout.Width(280)); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Engineer", headerStyle); + endlessVentTime = DrawToggle(endlessVentTime, "Endless Vent Time", 160); + GUILayout.Space(5); + noVentCooldown = DrawToggle(noVentCooldown, "No Vent Cooldown", 160); + GUILayout.Space(5); + unlockVents = DrawToggle(unlockVents, "Unlock Vents", 160); + GUILayout.Space(5); + walkInVents = DrawToggle(walkInVents, "Walk In Vents", 160); + GUILayout.Space(5); + noMapCooldowns = DrawToggle(noMapCooldowns, "No Map Cooldowns", 160); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Scientist", headerStyle); + endlessBattery = DrawToggle(endlessBattery, "Endless Battery", 160); + GUILayout.Space(5); + noVitalsCooldown = DrawToggle(noVitalsCooldown, "No Vitals Cooldown", 160); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Detective", headerStyle); + UnlimitedInterrogateRange = DrawToggle(UnlimitedInterrogateRange, "Interrogate Reach", 160); + GUILayout.EndVertical(); + + GUILayout.Space(5); + GUILayout.BeginVertical(boxStyle); + GUILayout.Label("Global Buffs", headerStyle); + roleBuffImmortality = DrawToggle(roleBuffImmortality, "Immortality", 160); + GUILayout.EndVertical(); + + GUILayout.EndVertical(); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + } + +private Vector2 doorsScrollPos = Vector2.zero; +} +} diff --git a/features/PlayerEsp.cs b/features/PlayerEsp.cs new file mode 100644 index 0000000..4739c1b --- /dev/null +++ b/features/PlayerEsp.cs @@ -0,0 +1,698 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +[HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SelectRoles))] + public static class RoleManager_SelectRoles_Patch + { + public static bool Prefix(RoleManager __instance) + { + if (!ElysiumModMenuGUI.enablePreGameRoleForce || !AmongUsClient.Instance.AmHost) return true; + try + { + var allPlayers = PlayerControl.AllPlayerControls.ToArray().Where(p => p != null && p.Data != null && !p.Data.Disconnected && !p.Data.IsDead).ToList(); + int numImps = 1; + try { numImps = GameOptionsManager.Instance.CurrentGameOptions.GetInt((Int32OptionNames)1); } catch { } + var impRoleTypes = new HashSet { 1, 5, 9, 18 }; + List impostors = new List(); + foreach (var p in allPlayers) + if (ElysiumModMenuGUI.forcedImpostors.Contains(p.PlayerId) || (ElysiumModMenuGUI.forcedPreGameRoles.ContainsKey(p.PlayerId) && impRoleTypes.Contains((int)ElysiumModMenuGUI.forcedPreGameRoles[p.PlayerId]))) + impostors.Add(p); + if (impostors.Count > 0) numImps = impostors.Count; + else { if (numImps >= allPlayers.Count) numImps = allPlayers.Count - 1; if (numImps < 1) numImps = 1; } + System.Random rand = new System.Random(); + while (impostors.Count < numImps && allPlayers.Count > impostors.Count) + { + var available = allPlayers.Where(p => !impostors.Contains(p)).ToList(); + impostors.Add(available[rand.Next(available.Count)]); + } + List crewmates = allPlayers.Where(p => !impostors.Contains(p)).ToList(); + var impData = new Il2CppSystem.Collections.Generic.List(); + foreach (var i in impostors) impData.Add(i.Data); + var crewData = new Il2CppSystem.Collections.Generic.List(); + foreach (var c in crewmates) crewData.Add(c.Data); + IGameOptions opts = GameOptionsManager.Instance.CurrentGameOptions; + GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(impData, opts, (RoleTeamTypes)1, int.MaxValue, new Il2CppSystem.Nullable()); + GameManager.Instance.LogicRoleSelection.AssignRolesForTeam(crewData, opts, (RoleTeamTypes)0, int.MaxValue, new Il2CppSystem.Nullable((RoleTypes)0)); + foreach (var kvp in ElysiumModMenuGUI.forcedPreGameRoles) + { + if (kvp.Value != RoleTypes.Crewmate && kvp.Value != RoleTypes.Impostor) + { + var pc = allPlayers.FirstOrDefault(p => p.PlayerId == kvp.Key); + if (pc != null) RoleManager.Instance.SetRole(pc, kvp.Value); + } + } + foreach (var pc in allPlayers) if (pc.Data.Role != null) pc.Data.Role.Initialize(pc); + return false; + } + catch { return true; } + } + } + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.TurnOnProtection))] + public static class PlayerControl_TurnOnProtection_Patch + { + public static void Prefix(ref bool visible) + { + if (ElysiumModMenuGUI.seeGhosts || ElysiumModMenuGUI.seeProtections) visible = true; + } + } + +[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.LateUpdate))] + public static class PlayerVisuals_LateUpdate_Patch + { + public static void Postfix(PlayerPhysics __instance) + { + if (__instance == null || __instance.myPlayer == null || __instance.myPlayer.Data == null) return; + try + { + if (ElysiumModMenuGUI.seeGhosts && __instance.myPlayer.Data.IsDead && PlayerControl.LocalPlayer != null && !PlayerControl.LocalPlayer.Data.IsDead) + { + __instance.myPlayer.Visible = true; + var rend = __instance.myPlayer.GetComponent(); + if (rend != null) { Color c = rend.color; rend.color = new Color(c.r, c.g, c.b, 0.4f); } + } + var cosmetics = __instance.myPlayer.cosmetics; + var outfit = __instance.myPlayer.CurrentOutfit; + if (cosmetics != null && cosmetics.nameText != null && outfit != null) + { + cosmetics.SetName(ElysiumModMenuGUI.GetESPNameTag(__instance.myPlayer.Data, outfit.PlayerName)); + if (ElysiumModMenuGUI.seeRoles && ElysiumModMenuGUI.showPlayerInfo) cosmetics.nameText.transform.localPosition = new Vector3(0f, 0.186f, 0f); + else if (ElysiumModMenuGUI.seeRoles || ElysiumModMenuGUI.showPlayerInfo) cosmetics.nameText.transform.localPosition = new Vector3(0f, 0.093f, 0f); + else cosmetics.nameText.transform.localPosition = new Vector3(0f, 0f, 0f); + } + } + catch { } + } + } + +[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Update))] + public static class ESP_MeetingHud + { + public static void Postfix(MeetingHud __instance) + { + try + { + if (__instance.playerStates == null) return; + foreach (var state in __instance.playerStates) + { + if (state == null) continue; + var data = GameData.Instance.GetPlayerById(state.TargetPlayerId); + if (data != null && !data.Disconnected && data.DefaultOutfit != null && state.NameText != null) + { + string espName = ElysiumModMenuGUI.GetESPNameTag(data, data.DefaultOutfit.PlayerName ?? "???"); + if (!ElysiumModMenuGUI.seeRoles && ElysiumModMenuGUI.revealMeetingRoles && data.Role != null) + { + string roleName = data.Role.Role.ToString(); + int roleId = (int)data.Role.Role; + if (roleId == 8) roleName = "Noisemaker"; + else if (roleId == 9) roleName = "Phantom"; + else if (roleId == 10) roleName = "Tracker"; + else if (roleId == 12) roleName = "Detective"; + else if (roleId == 18) roleName = "Viper"; + else if (roleName == "GuardianAngel") roleName = "Guardian Angel"; + Color customColor = ElysiumModMenuGUI.GetRoleColor(roleId, data.Role.TeamColor); + string roleColor = ColorUtility.ToHtmlStringRGB(customColor); + espName = $"{roleName}\n{espName}"; + } + state.NameText.text = espName; + bool showingExtra = ElysiumModMenuGUI.seeRoles || ElysiumModMenuGUI.revealMeetingRoles; + if (showingExtra) { state.NameText.transform.localPosition = new Vector3(0.3384f, 0.1125f, -0.1f); state.NameText.transform.localScale = new Vector3(0.9f, 1f, 1f); } + else { state.NameText.transform.localPosition = new Vector3(0.3384f, 0.0311f, -0.1f); state.NameText.transform.localScale = new Vector3(0.9f, 1f, 1f); } + } + } + } + catch { } + } + } + +[HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetName))] + public static class ChatBubble_SetName_Patch + { + public static void Postfix(ChatBubble __instance) + { + if (!ElysiumModMenuGUI.showPlayerInfo || __instance.playerInfo == null) return; + if (ElysiumModMenuGUI.IsMeetingVoteUiActive()) return; + try + { + string accentHex = ElysiumModMenuGUI.GetMenuAccentHex(false); + string espLine = ElysiumModMenuGUI.BuildESPInfoLine(__instance.playerInfo); + if (string.IsNullOrWhiteSpace(espLine)) return; + string extra = $" {espLine}"; + + if (!__instance.NameText.text.Contains("Lv:")) __instance.NameText.text += extra; + } + catch { } + } + } + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcMurderPlayer))] + public static class KillCooldownTrackerPatch + { + public static void Prefix(PlayerControl __instance, PlayerControl target, bool didSucceed) + { + try + { + if (!didSucceed || __instance == null || __instance.Data == null) return; + ElysiumModMenuGUI.lastKillTimestamps[__instance.PlayerId] = Time.time; + } + catch { } + } + } + +[HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] + public static class FullBright_Patch + { + public static void Postfix(HudManager __instance) + { + try + { + if (__instance == null || __instance.ShadowQuad == null || __instance.ShadowQuad.gameObject == null) return; + __instance.ShadowQuad.gameObject.SetActive(!ElysiumModMenuGUI.fullBright); + } + catch { } + } + } + +[HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] + public static class HudManager_Update_Patch + { + public static void Postfix(HudManager __instance) + { + try + { + if (ElysiumModMenuGUI.alwaysChat && __instance.Chat != null) + __instance.Chat.gameObject.SetActive(true); + + object aspectPosition = ElysiumModMenuGUI.GetHudAspectPosition(__instance); + if (aspectPosition != null) + { + float camSize = Camera.main != null ? Camera.main.orthographicSize : 3f; + if ((!ElysiumModMenuGUI.hudZoomBaseCaptured || (!ElysiumModMenuGUI.cameraZoom && camSize <= 3.05f)) && + ElysiumModMenuGUI.TryGetAspectDistance(aspectPosition, out Vector3 currentDistance)) + { + ElysiumModMenuGUI.hudZoomBaseDistance = currentDistance; + ElysiumModMenuGUI.hudZoomBaseCaptured = true; + } + + if (ElysiumModMenuGUI.cameraZoom && ElysiumModMenuGUI.hudZoomBaseCaptured) + { + Vector3 distance = ElysiumModMenuGUI.hudZoomBaseDistance; + distance.y = ElysiumModMenuGUI.hudZoomBaseDistance.y + 3f * (camSize - 3f); + ElysiumModMenuGUI.TrySetAspectDistance(aspectPosition, distance); + } + } + + if (ElysiumModMenuGUI.cameraZoom && __instance.TaskPanel != null && ShipStatus.Instance != null && MeetingHud.Instance == null) + __instance.TaskPanel.gameObject.SetActive(true); + } + catch { } + } + } + +[HarmonyPatch(typeof(PlatformSpecificData), nameof(PlatformSpecificData.Serialize))] + public static class PlatformSpooferPatch { public static void Prefix(PlatformSpecificData __instance) { try { if (ElysiumModMenuGUI.enablePlatformSpoof && __instance != null) __instance.Platform = ElysiumModMenuGUI.platformValues[ElysiumModMenuGUI.currentPlatformIndex]; } catch { } } } + +[HarmonyPatch(typeof(FullAccount), nameof(FullAccount.CanSetCustomName))] + public static class FullAccount_CanSetCustomName_Patch { public static void Prefix(ref bool canSetName) { try { if (ElysiumModMenuGUI.unlockFeatures) canSetName = true; } catch { } } } + +[HarmonyPatch(typeof(AccountManager), nameof(AccountManager.CanPlayOnline))] + public static class AccountManager_CanPlayOnline_Patch { public static void Postfix(ref bool __result) { try { if (ElysiumModMenuGUI.unlockFeatures) __result = true; } catch { } } } + +[HarmonyPatch(typeof(EngineerRole), "FixedUpdate")] + public static class EngineerCheatsPatch + { + public static void Postfix(EngineerRole __instance) + { + if (__instance.Player != PlayerControl.LocalPlayer) return; + if (ElysiumModMenuGUI.endlessVentTime) __instance.inVentTimeRemaining = float.MaxValue; + if (ElysiumModMenuGUI.noVentCooldown && __instance.cooldownSecondsRemaining > 0f) + { + __instance.cooldownSecondsRemaining = 0f; + var btn = DestroyableSingleton.Instance?.AbilityButton; + if (btn != null) { btn.ResetCoolDown(); btn.SetCooldownFill(0f); } + } + } + } + +[HarmonyPatch(typeof(Vent), nameof(Vent.CanUse))] + public static class Vent_CanUse_UnlockVents_Patch + { + public static void Postfix(Vent __instance, NetworkedPlayerInfo pc, ref bool canUse, ref bool couldUse, ref float __result) + { + try + { + if (!ElysiumModMenuGUI.unlockVents || __instance == null || pc == null || pc.Object == null) + return; + + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null || local.Data == null || local.Data.IsDead || (local.Data.Role != null && local.Data.Role.CanVent)) + return; + + PlayerControl target = pc.Object; + if (target != local || target.Collider == null) + return; + + Vector2 center = target.Collider.bounds.center; + Vector2 position = __instance.transform.position; + float distance = Vector2.Distance(center, position); + canUse = distance <= __instance.UsableDistance && !PhysicsHelpers.AnythingBetween(target.Collider, center, position, Constants.ShipOnlyMask, false); + couldUse = true; + __result = distance; + } + catch { } + } + } + +private static bool TrySetCooldownMember(object target, float value) + { + if (target == null) return false; + + string[] names = { "CoolDown", "_CoolDown_k__BackingField", "k__BackingField", "coolDown", "cooldown" }; + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + try + { + Type type = target.GetType(); + foreach (string name in names) + { + PropertyInfo property = type.GetProperty(name, flags); + if (property != null && property.CanWrite) + { + property.SetValue(target, value, null); + return true; + } + + FieldInfo field = type.GetField(name, flags); + if (field != null) + { + field.SetValue(target, value); + return true; + } + } + } + catch { } + + return false; + } + +[HarmonyPatch(typeof(Ladder), "SetDestinationCooldown")] + public static class Ladder_SetDestinationCooldown_Patch + { + public static bool Prefix(Ladder __instance) + { + try + { + if (!ElysiumModMenuGUI.noMapCooldowns) return true; + TrySetCooldownMember(__instance, 0f); + return false; + } + catch { return true; } + } + } + +[HarmonyPatch(typeof(ZiplineConsole), "Update")] + public static class ZiplineConsole_Update_Patch + { + public static void Postfix(ZiplineConsole __instance) + { + try + { + if (!ElysiumModMenuGUI.noMapCooldowns) return; + TrySetCooldownMember(__instance, 0f); + } + catch { } + } + } + +[HarmonyPatch(typeof(PlayerControl), "MurderPlayer")] + public static class KillCooldownTrackerPatch2 + { + public static void Prefix(PlayerControl __instance, PlayerControl target) + { + try + { + if (__instance == null || __instance.Data == null) return; + ElysiumModMenuGUI.lastKillTimestamps[__instance.PlayerId] = Time.time; + + if (!ElysiumModMenuGUI.spamReportBodies) return; + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null || PlayerControl.LocalPlayer.Data.IsDead) return; + if (target == null || target.Data == null || !target.Data.IsDead) return; + + PlayerControl.LocalPlayer.CmdReportDeadBody(target.Data); + } + catch { } + } + } + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetKillTimer))] + public static class KillAuraNoKillCooldownPatch + { + public static void Prefix(PlayerControl __instance, ref float time) + { + try + { + if (!ElysiumModMenuGUI.noKillCooldownHostOnly) return; + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + if (__instance != PlayerControl.LocalPlayer) return; + time = 0f; + } + catch { } + } + } + +[HarmonyPatch(typeof(ScientistRole), "Update")] + public static class ScientistCheatsPatch + { + public static void Postfix(ScientistRole __instance) + { + if (__instance.Player != PlayerControl.LocalPlayer) return; + if (ElysiumModMenuGUI.noVitalsCooldown) __instance.currentCooldown = 0f; + if (ElysiumModMenuGUI.endlessBattery) __instance.currentCharge = float.MaxValue; + } + } + +[HarmonyPatch(typeof(ShapeshifterRole), "FixedUpdate")] + public static class ShapeshifterDurationPatch + { + public static void Postfix(ShapeshifterRole __instance) { if (__instance.Player == PlayerControl.LocalPlayer && ElysiumModMenuGUI.endlessSsDuration) __instance.durationSecondsRemaining = float.MaxValue; } + } + +[HarmonyPatch(typeof(ImpostorRole), "FindClosestTarget")] + public static class ImpostorRangePatch + { + public static bool Prefix(ImpostorRole __instance, ref PlayerControl __result) + { + if (!ElysiumModMenuGUI.killReach) return true; + try + { + var target = ElysiumModMenuGUI.FindClosestKillTarget(__instance, float.MaxValue); + if (target != null) __result = target; + return false; + } + catch { return true; } + } + } + +[HarmonyPatch(typeof(ImpostorRole), "FindClosestTarget")] + public static class ImpostorKillWhileVanishedPatch + { + public static void Postfix(ImpostorRole __instance, ref PlayerControl __result) + { + try + { + if (__result != null) return; + if (!ElysiumModMenuGUI.killWhileVanishedHostOnly) return; + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + if (!ElysiumModMenuGUI.IsLocalPhantomVanished()) return; + + __result = ElysiumModMenuGUI.FindClosestKillTarget(__instance, ElysiumModMenuGUI.GetVanillaKillDistance()); + } + catch { } + } + } + +[HarmonyPatch(typeof(PhantomRole), nameof(PhantomRole.IsValidTarget))] + public static class PhantomRole_IsValidTarget_KillWhileVanished_Patch + { + public static void Postfix(NetworkedPlayerInfo target, ref bool __result) + { + try + { + if (!ElysiumModMenuGUI.killWhileVanishedHostOnly) return; + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + if (!ElysiumModMenuGUI.IsLocalPhantomVanished()) return; + + __result = ElysiumModMenuGUI.IsMalumValidKillTarget(target); + } + catch { } + } + } + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdCheckMurder))] + public static class PlayerControl_CmdCheckMurder_KillWhileVanished_Patch + { + public static bool Prefix(PlayerControl __instance, PlayerControl target) + { + try + { + if (!ElysiumModMenuGUI.killWhileVanishedHostOnly) return true; + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return true; + if (__instance != PlayerControl.LocalPlayer) return true; + if (!ElysiumModMenuGUI.IsLocalPhantomVanished()) return true; + if (target == null || target.Data == null || !ElysiumModMenuGUI.IsMalumValidKillTarget(target.Data)) return true; + + PlayerControl.LocalPlayer.RpcMurderPlayer(target, true); + return false; + } + catch { return true; } + } + } + +[HarmonyPatch(typeof(ImpostorRole), "IsValidTarget")] + public static class ImpostorKillAnyonePatch + { + public static void Postfix(NetworkedPlayerInfo target, ref bool __result) { try { if (ElysiumModMenuGUI.killAnyone) __result = ElysiumModMenuGUI.IsMalumValidKillTarget(target); } catch { } } + } + +private void teleportToPlayer(PlayerControl t) + { + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.NetTransform == null || t == null) return; + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(t.transform.position); + } + +[HarmonyPatch(typeof(DetectiveRole), "FindClosestTarget")] + public static class DetectiveRangePatch + { + public static bool Prefix(DetectiveRole __instance, ref PlayerControl __result) + { + if (!ElysiumModMenuGUI.UnlimitedInterrogateRange) return true; + try + { + var target = PlayerControl.AllPlayerControls.ToArray() + .Where(p => p != null && __instance.IsValidTarget(p.Data) && !p.Data.IsDead && !p.Data.Disconnected) + .OrderBy(p => Vector2.Distance(p.transform.position, PlayerControl.LocalPlayer.transform.position)) + .FirstOrDefault(); + if (target != null) __result = target; + return false; + } + catch { return true; } + } + } + +[HarmonyPatch(typeof(DoorBreakerGame), nameof(DoorBreakerGame.Start))] + public static class DoorBreakerGame_Start_Patch + { + public static bool Prefix(DoorBreakerGame __instance) + { + if (!ElysiumModMenuGUI.autoOpenDoors) return true; + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(__instance.MyDoor.Id | 64)); } catch { } + __instance.MyDoor.SetDoorway(true); __instance.Close(); + return false; + } + } + +[HarmonyPatch(typeof(DoorCardSwipeGame), nameof(DoorCardSwipeGame.Begin))] + public static class DoorCardSwipeGame_Begin_Patch + { + public static bool Prefix(DoorCardSwipeGame __instance) + { + if (!ElysiumModMenuGUI.autoOpenDoors) return true; + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(__instance.MyDoor.Id | 64)); } catch { } + __instance.MyDoor.SetDoorway(true); __instance.Close(); + return false; + } + } + +[HarmonyPatch(typeof(MushroomDoorSabotageMinigame), nameof(MushroomDoorSabotageMinigame.Begin))] + public static class MushroomDoorSabotageMinigame_Begin_Patch + { + public static bool Prefix(MushroomDoorSabotageMinigame __instance) { if (ElysiumModMenuGUI.autoOpenDoors) { __instance.FixDoorAndCloseMinigame(); return false; } return true; } + } + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetTasks))] + public static class NoTaskMode_Patch { public static bool Prefix(PlayerControl __instance) { if (ElysiumModMenuGUI.noTaskMode) return false; return true; } } + +[HarmonyPatch] + public static class Console_CanUse_AllowTasksAsImpostor_Patch + { + public static MethodBase TargetMethod() + { + return AccessTools.Method( + typeof(global::Console), + nameof(global::Console.CanUse), + new[] { typeof(NetworkedPlayerInfo), typeof(bool).MakeByRefType(), typeof(bool).MakeByRefType() }); + } + + public static void Prefix(global::Console __instance, NetworkedPlayerInfo pc) + { + try + { + if (__instance == null || pc == null) return; + + bool isImpostor = ElysiumModMenuGUI.IsLocalImpostorRole(pc); + if (ElysiumModMenuGUI.allowTasksAsImpostor || !isImpostor) + { + __instance.AllowImpostor = true; + return; + } + + int[] sabotageConsoleIds = { 0, 1, 2 }; + if (!sabotageConsoleIds.Contains(__instance.ConsoleId)) + __instance.AllowImpostor = false; + } + catch { } + } + + public static void Postfix(global::Console __instance, NetworkedPlayerInfo pc, ref bool canUse, ref bool couldUse, ref float __result) + { + try + { + if (!ElysiumModMenuGUI.allowTasksAsImpostor) return; + if (__instance == null || pc == null || pc.Object != PlayerControl.LocalPlayer) return; + if (!ElysiumModMenuGUI.IsLocalImpostorRole(pc)) return; + + __instance.AllowImpostor = true; + + if (!ElysiumModMenuGUI.LocalPlayerHasTaskForConsole(__instance)) + { + canUse = false; + couldUse = false; + return; + } + + if (couldUse) + __result = Mathf.Min(__result, ElysiumModMenuGUI.GetConsoleUsableDistance(__instance)); + } + catch { } + } + } + +[HarmonyPatch] + public static class Console_Use_AllowTasksAsImpostor_Guard_Patch + { + public static IEnumerable TargetMethods() + { + MethodInfo consoleUse = AccessTools.Method(typeof(global::Console), nameof(global::Console.Use), Type.EmptyTypes); + if (consoleUse != null) yield return consoleUse; + + Type ventCleaningConsole = AccessTools.TypeByName("VentCleaningConsole"); + MethodInfo ventCleaningUse = ventCleaningConsole != null ? AccessTools.Method(ventCleaningConsole, nameof(global::Console.Use), Type.EmptyTypes) : null; + if (ventCleaningUse != null) yield return ventCleaningUse; + } + + public static bool Prefix(object __instance) + { + try + { + if (__instance is global::Console console && ElysiumModMenuGUI.ShouldBlockUnsafeConsoleUse(console)) + return false; + } + catch { } + + return true; + } + } + +[HarmonyPatch(typeof(VentilationSystem), nameof(VentilationSystem.Update))] + public static class VentilationSystem_Update_Immortality_Patch + { + public static bool Prefix(VentilationSystem.Operation op, int ventId) + { + try + { + if (ventId != ElysiumModMenuGUI.ImmortalityCustomVentId && + ElysiumModMenuGUI.roleBuffImmortality && + ElysiumModMenuGUI.immortalityVentStateApplied && + (op == VentilationSystem.Operation.Enter || op == VentilationSystem.Operation.Exit || op == VentilationSystem.Operation.Move)) + return false; + } + catch { } + + return true; + } + } + +[HarmonyPatch(typeof(GameManager), nameof(GameManager.StartGame))] + public static class GameManager_StartGame_Immortality_Patch + { + public static void Postfix() + { + if (ElysiumModMenuGUI.roleBuffImmortality) + ElysiumModMenuGUI.SetImmortalityVentState(true); + } + } + +[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Close))] + public static class MeetingHud_Close_Immortality_Patch + { + public static void Postfix() + { + try + { + if (ElysiumModMenuGUI.roleBuffImmortality && + PlayerControl.LocalPlayer != null && + PlayerControl.LocalPlayer.Data != null && + !PlayerControl.LocalPlayer.Data.IsDead) + ElysiumModMenuGUI.SetImmortalityVentState(true); + } + catch { } + } + } + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.MurderPlayer))] + public static class PlayerControl_MurderPlayer_ImmortalityNotice_Patch + { + public static void Postfix(PlayerControl __instance, PlayerControl target) + { + try + { + if (!ElysiumModMenuGUI.roleBuffImmortality || target != PlayerControl.LocalPlayer || __instance == null || __instance.Data == null) return; + ShowNotification($"[IMMORTALITY] {__instance.Data.PlayerName} tried to kill you."); + } + catch { } + } + } +} +} diff --git a/features/Reports.cs b/features/Reports.cs new file mode 100644 index 0000000..7c83a84 --- /dev/null +++ b/features/Reports.cs @@ -0,0 +1,648 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +public static Sprite LoadEmbeddedSprite(string fileName, float pixelsPerUnit = 1f) + { + string path = $"ElysiumModMenu.{fileName}"; + + try + { + if (CachedSprites.TryGetValue(path + pixelsPerUnit, out var cachedSprite)) + return cachedSprite; + + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream(path); + + if (stream == null) + { + return null; + } + + var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); + using System.IO.MemoryStream ms = new System.IO.MemoryStream(); + stream.CopyTo(ms); + + ImageConversion.LoadImage(texture, ms.ToArray(), false); + + Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); + + sprite.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontSaveInEditor; + + return CachedSprites[path + pixelsPerUnit] = sprite; + } + catch (System.Exception) + { + return null; + } + } + public void Start() + { + activeGui = this; + if (enableBackground) LoadBackgroundImage(); + UnlockCosmetics(); + LoadConfig(); + LoadBanList(); + LoadBotBanList(); + ClearSpamErrorLogOnStartup(); + StartBackgroundAnomalyLogMonitor(); + + + try + { + int starts = UnityEngine.PlayerPrefs.GetInt("Elysium_GameStarts", 0); + starts++; + + string chatLogPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ChatLog.txt"); + + if (starts >= 3) + { + if (System.IO.File.Exists(chatLogPath)) + { + System.IO.File.WriteAllText(chatLogPath, string.Empty); + } + starts = 0; + } + + UnityEngine.PlayerPrefs.SetInt("Elysium_GameStarts", starts); + UnityEngine.PlayerPrefs.Save(); + } + catch { } + } + + public void OnApplicationQuit() + { + StopBackgroundAnomalyLogMonitor(); + SaveConfig(); + } + + public void OnDisable() + { + SaveConfig(); + } + + private static void ClearSpamErrorLogOnStartup() + { + try + { + watchedLogLineCounts.Clear(); + logBurstWindowStartedAt = -1f; + logBurstCooldownUntil = 0f; + logBurstLineCount = 0; + anomalyLogWatchNotified = false; + logMonitorNextScanAt = 0f; + + string root = string.IsNullOrWhiteSpace(Plugin.ElysiumFolder) + ? System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu") + : Plugin.ElysiumFolder; + + if (!System.IO.Directory.Exists(root)) return; + + foreach (string file in System.IO.Directory.GetFiles(root, "SpamErrorLog*.txt", System.IO.SearchOption.AllDirectories)) + { + try { System.IO.File.Delete(file); } + catch { } + } + + System.Console.WriteLine("[ElysiumModMenu] Cleared previous SpamErrorLog files and reset log monitor state."); + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Failed to clear SpamErrorLog files: {ex.GetType().Name}: {ex.Message}"); + } + } + + public static void SendAnomalyAlert(string title, string message, string dedupeKey = null, bool waitForCompletion = false, IEnumerable attachmentPaths = null) + { + if (!enableAnomalyLogReports) return; + if (!DiscordStatusReporter.IsEnabled) return; + string webhookUrl = DiscordStatusReporter.ConfiguredWebhookUrl; + if (!DiscordStatusReporter.IsValidWebhookUrl(webhookUrl)) return; + int attachmentCount = attachmentPaths == null ? 0 : attachmentPaths.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().Count(); + System.Console.WriteLine($"[ElysiumModMenu] Sending freeze/overload logs to configured webhook. Type={title}. Attachments={attachmentCount}."); + DiscordStatusReporter.SendDiagnosticAlert(webhookUrl, title, message, waitForCompletion, attachmentPaths); + } + + private static void StartBackgroundAnomalyLogMonitor() + { + try + { + if (anomalyLogMonitorTimer != null) return; + anomalyLogMonitorTimer = new System.Threading.Timer(_ => TryDetectLogBurstTick(false), null, 1000, 1000); + System.Console.WriteLine("[ElysiumModMenu] Background freeze/overload log monitor started."); + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Failed to start background log monitor: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static void StopBackgroundAnomalyLogMonitor() + { + try + { + anomalyLogMonitorTimer?.Dispose(); + anomalyLogMonitorTimer = null; + } + catch { } + } + + private static float GetLogMonitorSeconds() + { + try { return (float)(DateTime.UtcNow - logMonitorStartedUtc).TotalSeconds; } + catch { return 0f; } + } + + private static string BuildAnomalyReportDetails(bool allowUnityAccess = true) + { + if (!allowUnityAccess) + return anomalyReportDetailsCache + "\nmonitor=background"; + + string clientId = "Unknown"; + string networkMode = "Unknown"; + string inGame = "no"; + string host = "no"; + string platform = "Unknown"; + + try + { + if (AmongUsClient.Instance != null) + { + clientId = AmongUsClient.Instance.ClientId.ToString(); + networkMode = AmongUsClient.Instance.NetworkMode.ToString(); + host = AmongUsClient.Instance.AmHost ? "yes" : "no"; + + ClientData client = AmongUsClient.Instance.GetClientFromCharacter(PlayerControl.LocalPlayer); + if (client != null) + { + platform = GetPlatform(client); + } + } + } + catch { } + + try { inGame = ShipStatus.Instance != null && LobbyBehaviour.Instance == null ? "yes" : "no"; } catch { } + + string details = $"sessionId={relaySessionId}\nclientId={clientId}\nnetworkMode={networkMode}\nhost={host}\nplatform={platform}\ninGame={inGame}\nmonitor=unity"; + anomalyReportDetailsCache = details; + return details; + } + + private static void TryDetectLogBurstTick(bool allowUnityAccess = true) + { + if (!enableAnomalyLogReports) return; + lock (anomalyLogMonitorLock) + { + try + { + float now = GetLogMonitorSeconds(); + if (now < logMonitorNextScanAt) return; + logMonitorNextScanAt = now + LogBurstScanIntervalSeconds; + + List watchedFiles = GetWatchedLogFiles().ToList(); + if (!anomalyLogWatchNotified) + { + anomalyLogWatchNotified = true; + System.Console.WriteLine($"[ElysiumModMenu] Freeze/overload log reporting is enabled. Watching: {string.Join(", ", watchedFiles.Select(System.IO.Path.GetFileName).ToArray())}. Sends only summary counters when error/red logs appear or {LogBurstLineThreshold}+ new lines arrive within {LogBurstWindowSeconds:0}s."); + } + + int newLines = 0; + int errorLines = 0; + int storedMsgLines = 0; + List touchedLogs = new List(); + List touchedLogFiles = new List(); + foreach (string file in watchedFiles) + { + List addedLines = ReadNewLogLines(file, out int currentLines); + if (!watchedLogLineCounts.TryGetValue(file, out int previousLines)) + { + watchedLogLineCounts[file] = currentLines; + addedLines = ReadInitialRecentLogTail(file); + if (addedLines.Count <= 0) continue; + } + else + { + watchedLogLineCounts[file] = currentLines; + } + + if (addedLines.Count <= 0) continue; + + newLines += addedLines.Count; + touchedLogs.Add(System.IO.Path.GetFileName(file)); + touchedLogFiles.Add(file); + errorLines += addedLines.Count(IsErrorLogLine); + storedMsgLines += addedLines.Count(IsStoredMessageOverloadLine); + } + + if (newLines <= 0) return; + + if (logBurstWindowStartedAt < 0f || now - logBurstWindowStartedAt > LogBurstWindowSeconds) + { + logBurstWindowStartedAt = now; + logBurstLineCount = 0; + } + + logBurstLineCount += newLines; + bool isStoredRpcBurst = storedMsgLines > 0; + bool isErrorBurst = errorLines > 0; + bool isLineBurst = logBurstLineCount >= LogBurstLineThreshold; + if ((!isErrorBurst && !isLineBurst) || (!isStoredRpcBurst && now < logBurstCooldownUntil)) return; + + logBurstCooldownUntil = now + (isStoredRpcBurst ? 5f : LogBurstAlertCooldownSeconds); + string reason = isStoredRpcBurst ? "stored rpc overload detected" : (isErrorBurst ? "error/red log detected" : "log spam detected"); + string message = $"{BuildAnomalyReportDetails(allowUnityAccess)}\nnewLogLines={logBurstLineCount}\nerrorLines={errorLines}\nstoredMsgLines={storedMsgLines}\nwindowSeconds={LogBurstWindowSeconds}\nthreshold={LogBurstLineThreshold}\nreason={reason}, needs fix\nwatchedLogs={string.Join(", ", touchedLogs.Distinct().ToArray())}"; + SendAnomalyAlert(isErrorBurst ? "Abnormal error log" : "Abnormal log spam", message, "abnormal-log-spam", !allowUnityAccess, touchedLogFiles); + logBurstWindowStartedAt = -1f; + logBurstLineCount = 0; + } + catch (Exception ex) + { + System.Console.WriteLine($"[ElysiumModMenu] Log monitor failed: {ex.GetType().Name}: {ex.Message}"); + } + } + } + + private static IEnumerable GetWatchedLogFiles() + { + string root = GetAmongUsRoot(); + List files = new List(); + + try + { + string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string unityLogRoot = System.IO.Path.Combine(userProfile, "AppData", "LocalLow", "Innersloth", "Among Us"); + AddLogPath(files, System.IO.Path.Combine(unityLogRoot, "Player.log")); + AddLogPath(files, System.IO.Path.Combine(unityLogRoot, "Player-prev.log")); + } + catch { } + + AddLogPath(files, System.IO.Path.Combine(root, "BepInEx", "ErrorLog.log")); + AddLogPath(files, System.IO.Path.Combine(root, "ErrorLog.log")); + AddLogPath(files, System.IO.Path.Combine(root, "BepInEx", "LogOutput.log")); + AddLogPath(files, System.IO.Path.Combine(root, "LogOutput.log")); + + try + { + string[] banLogDirs = + { + System.IO.Path.Combine(root, "BepInEx", "BAN_DATA", "LOG"), + System.IO.Path.Combine(root, "BAN_DATA", "LOG") + }; + + foreach (string banLogDir in banLogDirs) + { + if (!System.IO.Directory.Exists(banLogDir)) continue; + foreach (string file in System.IO.Directory.GetFiles(banLogDir)) + AddLogPath(files, file); + } + } + catch { } + + try + { + string playerLogRoot = System.IO.Path.Combine(root, "ElysiumModMenu", "PlayerLogs"); + if (System.IO.Directory.Exists(playerLogRoot)) + { + foreach (string dir in System.IO.Directory.GetDirectories(playerLogRoot)) + { + AddLogPath(files, System.IO.Path.Combine(dir, "LogOutput.txt")); + AddLogPath(files, System.IO.Path.Combine(dir, "LogOutput.log")); + } + } + } + catch { } + + return files; + } + + private static void AddLogPath(List files, string file) + { + try + { + if (!string.IsNullOrWhiteSpace(file) && System.IO.File.Exists(file) && !files.Contains(file)) + files.Add(file); + } + catch { } + } + + private static List ReadNewLogLines(string file, out int currentLines) + { + currentLines = 0; + List lines = new List(); + try + { + if (string.IsNullOrWhiteSpace(file) || !System.IO.File.Exists(file)) return lines; + + watchedLogLineCounts.TryGetValue(file, out int previousLines); + using (System.IO.FileStream stream = new System.IO.FileStream(file, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete)) + using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8, true)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + currentLines++; + if (currentLines > previousLines) + lines.Add(line); + } + } + } + catch + { + } + + return lines; + } + + private static List ReadInitialRecentLogTail(string file) + { + List result = new List(); + try + { + if (string.IsNullOrWhiteSpace(file) || !System.IO.File.Exists(file)) return result; + + Queue errorTail = new Queue(); + using (System.IO.FileStream stream = new System.IO.FileStream(file, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete)) + using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8, true)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + if (!IsErrorLogLine(line)) continue; + errorTail.Enqueue(line); + while (errorTail.Count > InitialLogTailLineLimit) + errorTail.Dequeue(); + } + } + + result.AddRange(errorTail); + } + catch { } + + return result; + } + + private static bool IsErrorLogLine(string line) + { + if (string.IsNullOrWhiteSpace(line)) return false; + string lower = line.ToLowerInvariant(); + if (lower.Contains("[elysiummodmenu]")) return false; + if (lower.Contains("method ") && lower.Contains(" has unsupported ") && lower.Contains("elysiummodmenu")) return false; + if (lower.Contains("registered mono type") && lower.Contains("elysiummodmenu")) return false; + return lower.Contains("[error") || + lower.Contains("[fatal") || + lower.Contains("exception") || + lower.Contains(" stack trace") || + lower.Contains("traceback") || + lower.Contains("stored data") || + lower.Contains("storeddata") || + IsStoredMessageOverloadLine(lower) || + IsKnownSpamWarningLine(lower) || + lower.Contains("overload") || + lower.Contains("freeze") || + lower.Contains("color=red") || + lower.Contains("#ff0000"); + } + + public static bool IsRelevantAnomalyLine(string line) + { + if (string.IsNullOrWhiteSpace(line)) return false; + string lower = line.ToLowerInvariant(); + if (lower.Contains("[elysiummodmenu]")) return false; + if (lower.Contains("bepinex") && lower.Contains("chainloader")) return false; + if (lower.Contains("registered mono type") && lower.Contains("elysiummodmenu")) return false; + if (lower.Contains("method ") && lower.Contains(" has unsupported ") && lower.Contains("elysiummodmenu")) return false; + return IsStoredMessageOverloadLine(lower) || + IsKnownSpamWarningLine(lower) || + lower.Contains("[error") || + lower.Contains("[fatal") || + lower.Contains("nullreferenceexception") || + lower.Contains("invaliddataexception") || + lower.Contains("exception:") || + lower.Contains(" stack trace") || + lower.Contains("traceback") || + lower.Contains("stored data") || + lower.Contains("storeddata"); + } + + private static bool IsKnownSpamWarningLine(string line) + { + if (string.IsNullOrWhiteSpace(line)) return false; + string lower = line.ToLowerInvariant(); + return lower.Contains("sendmode set to everything") || + lower.Contains("likely should be reliable") || + lower.Contains("stored msg"); + } + + private static bool IsStoredMessageOverloadLine(string line) + { + if (string.IsNullOrWhiteSpace(line)) return false; + string lower = line.ToLowerInvariant(); + return lower.Contains("stored msg") && lower.Contains(" rpc "); + } + + private static string GetAmongUsRoot() + { + try { return System.IO.Directory.GetCurrentDirectory(); } + catch { return string.Empty; } + } + + private static string EscapeJson(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + + StringBuilder builder = new StringBuilder(value.Length + 16); + foreach (char c in value) + { + switch (c) + { + case '\\': builder.Append("\\\\"); break; + case '"': builder.Append("\\\""); break; + case '\n': builder.Append("\\n"); break; + case '\r': builder.Append("\\r"); break; + case '\t': builder.Append("\\t"); break; + default: + if (c < 32) builder.Append("\\u").Append(((int)c).ToString("x4")); + else builder.Append(c); + break; + } + } + + return builder.ToString(); + } + + private void TrySendDiscordLaunchStatusTick() + { + if (discordLaunchStatusSent || !DiscordStatusReporter.IsEnabled) return; + if (Time.unscaledTime < discordLaunchStatusNextTryAt) return; + + string webhookUrl = DiscordStatusReporter.ConfiguredWebhookUrl.Trim(); + if (!DiscordStatusReporter.IsValidWebhookUrl(webhookUrl)) + { + discordLaunchStatusNextTryAt = Time.unscaledTime + 10f; + if (!discordInvalidWebhookNotified) + { + discordInvalidWebhookNotified = true; + } + return; + } + + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) + { + discordLaunchStatusNextTryAt = Time.unscaledTime + 2f; + return; + } + + string nickname = "Unknown"; + string friendCode = "Hidden"; + string puid = "Unknown"; + string platform = "Unknown"; + string roomCode = "No room"; + int level = 0; + + try + { + nickname = string.IsNullOrWhiteSpace(PlayerControl.LocalPlayer.Data.PlayerName) + ? "Unknown" + : PlayerControl.LocalPlayer.Data.PlayerName; + if (DiscordStatusReporter.IncludeLocalPuid) + friendCode = GetDisplayedFriendCode(PlayerControl.LocalPlayer.Data, "Hidden"); + + uint rawLevel = PlayerControl.LocalPlayer.Data.PlayerLevel; + if (rawLevel != uint.MaxValue && rawLevel < 10000) level = (int)rawLevel + 1; + } + catch { } + + try + { + ClientData client = AmongUsClient.Instance?.GetClientFromCharacter(PlayerControl.LocalPlayer); + if (client != null) + { + puid = GetClientPuid(client); + platform = GetPlatform(client); + } + } + catch { } + + try + { + roomCode = GetCurrentRoomCodeForStatus(); + } + catch { } + + DiscordStatusReporter.SendLaunchStatus(webhookUrl, nickname, friendCode, puid, platform, level, roomCode, DiscordStatusReporter.IncludeLocalPuid); + discordLaunchStatusSent = true; + } + + private static string lastKnownRoomCode = string.Empty; + +private static float lastRoomCodeCopyAt = -10f; + +public static string GetCurrentRoomCodeForStatus() + { + try + { + if (AmongUsClient.Instance == null || AmongUsClient.Instance.GameId == 0) + return string.IsNullOrWhiteSpace(lastKnownRoomCode) ? "No room" : lastKnownRoomCode; + + int gameId = AmongUsClient.Instance.GameId; + string gameName = GameCode.IntToGameName(gameId); + lastKnownRoomCode = string.IsNullOrWhiteSpace(gameName) ? gameId.ToString() : gameName; + return lastKnownRoomCode; + } + catch + { + try + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.GameId != 0) + { + lastKnownRoomCode = AmongUsClient.Instance.GameId.ToString(); + return lastKnownRoomCode; + } + } + catch { } + + return string.IsNullOrWhiteSpace(lastKnownRoomCode) ? "No room" : lastKnownRoomCode; + } + } + +public static bool TryCopyRoomCodeToClipboard(bool notify) + { + if (!autoCopyCodeAndLeave) + return false; + + try + { + if (Time.unscaledTime - lastRoomCodeCopyAt < 0.75f) + return false; + + string roomCode = GetCurrentRoomCodeForStatus(); + if (string.IsNullOrWhiteSpace(roomCode) || roomCode == "No room") + return false; + + GUIUtility.systemCopyBuffer = roomCode; + lastRoomCodeCopyAt = Time.unscaledTime; + if (notify) + ShowNotification($"[ROOM] Copied code: {roomCode}"); + return true; + } + catch { return false; } + } + +public static KeyCode bindMagnetCursor = KeyCode.F9; + +public static bool isWaitBindMagnetCursor = false; + +private bool CanRunHostBind(string actionName) + { + try + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) return true; + } + catch { } + + ShowNotification($"[BIND] {actionName}: host only"); + return false; + } +} +} diff --git a/features/Votekick.cs b/features/Votekick.cs new file mode 100644 index 0000000..230cb21 --- /dev/null +++ b/features/Votekick.cs @@ -0,0 +1,636 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +public void OnGUI() + { + Event e = Event.current; + + HandleMessage.HandleTimer(); + + bool isTyping = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingGhostChatColor || isEditingBan; + bool isBinding = isWaitingForBind || isWaitBindMassMorph || isWaitBindSpawnLobby || isWaitBindDespawnLobby || + isWaitBindCloseMeeting || isWaitBindInstaStart || isWaitBindEndCrew || isWaitBindEndImp || + isWaitBindEndImpDC || isWaitBindEndHnsDC || isWaitBindMagnetCursor || isWaitBindToggleTracers || + isWaitBindToggleNoClip || isWaitBindToggleFreecam || isWaitBindToggleCameraZoom || + isWaitBindKillAll || isWaitBindCallMeeting || isWaitBindTogglePlayerInfo || + isWaitBindToggleSeeRoles || isWaitBindToggleSeeGhosts || isWaitBindToggleFullBright || + isWaitBindKickAll || isWaitBindFixSabotages || isWaitBindSetAllGhost || + isWaitBindSetAllGhostImp || isWaitBindReviveAll; + + if (e != null && e.isKey && e.type == EventType.KeyDown) + { + if (e.keyCode == KeyCode.Escape) + { + isEditingName = isEditingLevel = isEditingFriendCode = isEditingLocalFriendCode = isEditingGhostChatColor = isEditingBan = false; + ResetAllBindWaits(); + e.Use(); + } + else if (isBinding && e.keyCode != KeyCode.None) + { + if (isWaitingForBind) { menuToggleKey = e.keyCode; } + else if (isWaitBindMassMorph) { bindMassMorph = e.keyCode; } + else if (isWaitBindSpawnLobby) { bindSpawnLobby = e.keyCode; } + else if (isWaitBindDespawnLobby) { bindDespawnLobby = e.keyCode; } + else if (isWaitBindCloseMeeting) { bindCloseMeeting = e.keyCode; } + else if (isWaitBindInstaStart) { bindInstaStart = e.keyCode; } + else if (isWaitBindEndCrew) { bindEndCrew = e.keyCode; } + else if (isWaitBindEndImp) { bindEndImp = e.keyCode; } + else if (isWaitBindEndImpDC) { bindEndImpDC = e.keyCode; } + else if (isWaitBindEndHnsDC) { bindEndHnsDC = e.keyCode; } + else if (isWaitBindMagnetCursor) { bindMagnetCursor = e.keyCode; } + else if (isWaitBindToggleTracers) { bindToggleTracers = e.keyCode; } + else if (isWaitBindToggleNoClip) { bindToggleNoClip = e.keyCode; } + else if (isWaitBindToggleFreecam) { bindToggleFreecam = e.keyCode; } + else if (isWaitBindToggleCameraZoom) { bindToggleCameraZoom = e.keyCode; } + else if (isWaitBindKillAll) { bindKillAll = e.keyCode; } + else if (isWaitBindCallMeeting) { bindCallMeeting = e.keyCode; } + else if (isWaitBindTogglePlayerInfo) { bindTogglePlayerInfo = e.keyCode; } + else if (isWaitBindToggleSeeRoles) { bindToggleSeeRoles = e.keyCode; } + else if (isWaitBindToggleSeeGhosts) { bindToggleSeeGhosts = e.keyCode; } + else if (isWaitBindToggleFullBright) { bindToggleFullBright = e.keyCode; } + else if (isWaitBindKickAll) { bindKickAll = e.keyCode; } + else if (isWaitBindFixSabotages) { bindFixSabotages = e.keyCode; } + else if (isWaitBindSetAllGhost) { bindSetAllGhost = e.keyCode; } + else if (isWaitBindSetAllGhostImp) { bindSetAllGhostImp = e.keyCode; } + else if (isWaitBindReviveAll) { bindReviveAll = e.keyCode; } + + ResetAllBindWaits(); + SaveConfig(); + e.Use(); + } + else if (isTyping) + { + if (isEditingBan && HandleClipboardShortcut(e, ref banInput)) { } + else if (isEditingName && HandleClipboardShortcut(e, ref customNameInput)) { } + else if (isEditingLevel && HandleClipboardShortcut(e, ref spoofLevelString)) { } + else if (isEditingFriendCode && HandleClipboardShortcut(e, ref spoofFriendCodeInput)) { } + else if (isEditingLocalFriendCode && HandleClipboardShortcut(e, ref localFriendCodeInput)) { } + else if (isEditingGhostChatColor && HandleClipboardShortcut(e, ref ghostChatColorHex, 7)) { ghostChatColorHex = FilterHexInput(ghostChatColorHex, 7); } + else if (e.keyCode == KeyCode.Backspace) + { + if (isEditingBan && banInput.Length > 0) { banInput = banInput.Substring(0, banInput.Length - 1); } + if (isEditingName && customNameInput.Length > 0) { customNameInput = customNameInput.Substring(0, customNameInput.Length - 1); } + if (isEditingLevel && spoofLevelString.Length > 0) { spoofLevelString = spoofLevelString.Substring(0, spoofLevelString.Length - 1); } + if (isEditingFriendCode && spoofFriendCodeInput.Length > 0) { spoofFriendCodeInput = spoofFriendCodeInput.Substring(0, spoofFriendCodeInput.Length - 1); } + if (isEditingLocalFriendCode && localFriendCodeInput.Length > 0) { localFriendCodeInput = localFriendCodeInput.Substring(0, localFriendCodeInput.Length - 1); } + if (isEditingGhostChatColor && ghostChatColorHex.Length > 0) { ghostChatColorHex = ghostChatColorHex.Substring(0, ghostChatColorHex.Length - 1); } + e.Use(); + } + else if (e.character != 0 && e.character != '\n' && e.character != '\r') + { + if (isEditingBan) { banInput += e.character; } + if (isEditingName) { customNameInput += e.character; } + if (isEditingLevel) { spoofLevelString += e.character; } + if (isEditingFriendCode) { spoofFriendCodeInput += e.character; } + if (isEditingLocalFriendCode) { localFriendCodeInput += e.character; } + if (isEditingGhostChatColor) { ghostChatColorHex = FilterHexInput((ghostChatColorHex ?? "") + e.character, 7); } + e.Use(); + } + } + } + + if (Event.current.type == EventType.Layout) + { + lockedPlayersList.Clear(); + if (PlayerControl.AllPlayerControls != null) + { + foreach (var p in PlayerControl.AllPlayerControls) + { + if (p != null && p.Data != null && !p.Data.Disconnected && p.PlayerId < 100) + lockedPlayersList.Add(p); + } + } + + if (!stylesInited) InitStyles(); + + if (showMenu) + { + FontStyle oldLabelFont = GUI.skin.label.fontStyle; + FontStyle oldBoxFont = GUI.skin.box.fontStyle; + FontStyle oldButtonFont = GUI.skin.button.fontStyle; + FontStyle oldToggleFont = GUI.skin.toggle.fontStyle; + Color oldContentColor = GUI.contentColor; + + try + { + FontStyle menuFontStyle = boldMenuText ? FontStyle.Bold : FontStyle.Normal; + GUI.skin.label.fontStyle = menuFontStyle; + GUI.skin.box.fontStyle = menuFontStyle; + GUI.skin.button.fontStyle = menuFontStyle; + GUI.skin.toggle.fontStyle = menuFontStyle; + if (RgbMenuTextActive()) + GUI.contentColor = GetMenuAccentColor(); + + windowRect = GUI.Window(0, windowRect, (Action)DrawElysiumModMenu, "", windowStyle); + } + finally + { + GUI.skin.label.fontStyle = oldLabelFont; + GUI.skin.box.fontStyle = oldBoxFont; + GUI.skin.button.fontStyle = oldButtonFont; + GUI.skin.toggle.fontStyle = oldToggleFont; + GUI.contentColor = oldContentColor; + } + } + + for (int i = screenNotifications.Count - 1; i >= 0; i--) + { + screenNotifications[i].lifetime += Time.deltaTime; + if (screenNotifications[i].HasExpired) screenNotifications.RemoveAt(i); + } + } + + try + { + if (AmongUsClient.Instance != null && (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Joined || AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started)) + { + if (PlayerControl.AllPlayerControls != null) + { + List currentIds = new List(); + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + currentIds.Add(pc.PlayerId); + UpsertPlayerHistory(pc); + } + } + + foreach (var id in currentIds) + { + if (!lastPlayerIds.Contains(id) && !pendingJoinTimers.ContainsKey(id)) + { + pendingJoinTimers[id] = 1.5f; + } + } + + var keysToProcess = pendingJoinTimers.Keys.ToList(); + foreach (var id in keysToProcess) + { + pendingJoinTimers[id] -= Time.deltaTime; + if (pendingJoinTimers[id] <= 0f) + { + pendingJoinTimers.Remove(id); + + var pc = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p != null && p.PlayerId == id); + if (pc != null && pc.Data != null && !pc.Data.Disconnected) + { + if (DetailedJoinInfo) + { + int level = 1; + try + { + uint rawLevel = pc.Data.PlayerLevel; + if (rawLevel != uint.MaxValue && rawLevel < 10000) level = (int)rawLevel + 1; + } + catch { } + + string platform = GetPlatform(AmongUsClient.Instance.GetClientFromCharacter(pc)); + string fc = GetDisplayedFriendCode(pc.Data); + + ShowNotification($"[+] {pc.Data.PlayerName} joined\nLvl: {level} | {platform} | FC: {fc}"); + } + else + { + ShowNotification($"[+] {pc.Data.PlayerName} присоединился"); + } + } + } + } + + foreach (var id in lastPlayerIds) + { + if (!currentIds.Contains(id)) + { + pendingJoinTimers.Remove(id); + MarkPlayerHistoryLeft(id); + } + } + + lastPlayerIds = new List(currentIds); + } + } + else + { + foreach (var id in lastPlayerIds) + MarkPlayerHistoryLeft(id); + lastPlayerIds.Clear(); + pendingJoinTimers.Clear(); + } + } + catch { } + if (screenNotifications.Count > 0) + { + int maxNotifs = 6; + int startIdx = Mathf.Max(0, screenNotifications.Count - maxNotifs); + for (int i = startIdx; i < screenNotifications.Count; i++) + { + ElysiumNotification notif = screenNotifications[i]; + int reverseIndex = screenNotifications.Count - 1 - i; + + float slideOffset = 0f; + float animSpeed = 0.3f; + float currentAlpha = 0.95f; + + if (notif.lifetime < animSpeed) + { + float t = Mathf.Clamp01(1f - (notif.lifetime / animSpeed)); + slideOffset = t * t * 300f; + } + else if (notif.lifetime > notif.ttl - animSpeed) + { + float t = Mathf.Clamp01((notif.lifetime - (notif.ttl - animSpeed)) / animSpeed); + slideOffset = t * t * 300f; + currentAlpha = Mathf.Lerp(0.95f, 0f, t); + } + + float xPos = (float)Screen.width - notificationBoxSize.x - 15f + slideOffset; + float yPos = Screen.height - 150f - (reverseIndex * (notificationBoxSize.y + 5f)); + + GUI.color = whiteMenuTheme + ? new Color(1f, 1f, 1f, currentAlpha) + : new Color(0.12f, 0.12f, 0.12f, currentAlpha); + GUI.Box(new Rect(xPos, yPos, notificationBoxSize.x, notificationBoxSize.y), "", windowStyle); + + GUI.color = new Color(1f, 1f, 1f, currentAlpha > 0.5f ? 1f : currentAlpha * 2f); + string notificationTextHex = whiteMenuTheme ? "202020" : GetMenuAccentHex(false); + string notificationMessage = GetNotificationTextForTheme(notif.message); + GUIStyle notifTitleStyle = new GUIStyle(GUI.skin.label) + { + richText = true, + fontSize = 12, + clipping = TextClipping.Clip + }; + notifTitleStyle.normal.textColor = whiteMenuTheme ? new Color(0.02f, 0.02f, 0.02f, 1f) : Color.white; + GUIStyle notifTimerStyle = new GUIStyle(notifTitleStyle) + { + alignment = TextAnchor.UpperRight + }; + GUIStyle notifMessageStyle = new GUIStyle(GUI.skin.label) + { + richText = true, + wordWrap = true, + fontSize = 12, + clipping = TextClipping.Clip + }; + notifMessageStyle.normal.textColor = whiteMenuTheme ? new Color(0.02f, 0.02f, 0.02f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); + + GUI.Label(new Rect(xPos + 10f, yPos + 5f, notificationBoxSize.x - 20f, 20f), $"{notif.title}", notifTitleStyle); + + float timeLeft = Mathf.Max(0, notif.ttl - notif.lifetime); + GUI.Label(new Rect(xPos + 10f, yPos + 5f, notificationBoxSize.x - 20f, 20f), $"{timeLeft:F1}s", notifTimerStyle); + GUI.Label(new Rect(xPos + 10f, yPos + 25f, notificationBoxSize.x - 20f, notificationBoxSize.y - 30f), notificationMessage, notifMessageStyle); + + float progress = 1f - (notif.lifetime / notif.ttl); + Color progressColor = GetMenuAccentColor(false); + GUI.color = new Color(progressColor.r, progressColor.g, progressColor.b, currentAlpha); + GUI.Box(new Rect(xPos + 8f, yPos + notificationBoxSize.y - 6f, (notificationBoxSize.x - 16f) * progress, 2f), "", safeLineStyle); + GUI.color = Color.white; + } + } + } + +public static bool votekickEveryone = false; + +public static List votekickedPlayerIds = new List(); + +private static bool votekickExitQueued = false; + +private static float votekickExitAt = 0f; + +private const float VotekickExitDelay = 0.45f; + +private Vector2 votekickScrollPosition = Vector2.zero; + +private void StartVotekickEveryoneRun() + { + votekickEveryone = true; + votekickedPlayerIds.Clear(); + votekickExitQueued = false; + votekickExitAt = 0f; + ShowNotification("[AUTO-VOTEKICK] Armed. Join a room and votes will be sent automatically."); + } + +private void StopVotekickEveryoneRun(bool clearVotes = true) + { + votekickEveryone = false; + votekickExitQueued = false; + votekickExitAt = 0f; + if (clearVotes) votekickedPlayerIds.Clear(); + } + + private void TickVotekickEveryoneRun() + { + if (!votekickEveryone) return; + + if (votekickExitQueued) + { + if (Time.unscaledTime >= votekickExitAt) + LeaveRoomAfterVotekick(); + return; + } + + if (VoteBanSystem.Instance == null || PlayerControl.AllPlayerControls == null || AmongUsClient.Instance == null) + return; + + int sent = ExecuteVotekickEveryone(true); + if (sent <= 0) return; + + votekickExitQueued = true; + votekickExitAt = Time.unscaledTime + VotekickExitDelay; + ShowNotification($"[AUTO-VOTEKICK] Votes sent: {sent}. Leaving room..."); + } + + private int ExecuteVotekickEveryone(bool rememberTargets) + { + if (VoteBanSystem.Instance == null || PlayerControl.AllPlayerControls == null) return 0; + + int votesSent = 0; + try + { + foreach (PlayerControl pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.AmOwner || pc.Data == null || pc.Data.Disconnected) continue; + + int clientId = pc.Data.ClientId; + + if (!rememberTargets || !votekickedPlayerIds.Contains(clientId)) + { + for (int i = 0; i < 3; i++) + { + VoteBanSystem.Instance.CmdAddVote(clientId); + votesSent++; + } + + if (rememberTargets) + votekickedPlayerIds.Add(clientId); + + ShowNotification($"[VOTEKICK] 3 votes sent to {pc.Data.PlayerName}."); + } + } + } + catch (Exception) + { + } + + return votesSent; + } + + private void SendVotekickEveryoneStay() + { + int sent = ExecuteVotekickEveryone(false); + if (sent > 0) + ShowNotification($"[VOTEKICK] Sent {sent} votes. Staying in room."); + else + ShowNotification("[VOTEKICK] No valid targets or VoteBanSystem is not ready."); + } + + private void LeaveRoomAfterVotekick() + { + try + { + votekickExitQueued = false; + votekickExitAt = 0f; + votekickedPlayerIds.Clear(); + + if (AmongUsClient.Instance != null) + { + AmongUsClient.Instance.ExitGame(DisconnectReasons.ExitGame); + ShowNotification("[AUTO-VOTEKICK] Left room. Auto mode is still armed."); + } + } + catch (Exception) + { + votekickExitQueued = false; + votekickExitAt = 0f; + } + } + + public static void ExecuteVotekickTarget(PlayerControl target) + { + if (target == null || target.Data == null || VoteBanSystem.Instance == null) return; + + try + { + int targetClientId = target.Data.ClientId; + + VoteBanSystem.Instance.CmdAddVote(targetClientId); + + + if (DestroyableSingleton.Instance != null && DestroyableSingleton.Instance.Notifier != null) + { + DestroyableSingleton.Instance.Notifier.AddDisconnectMessage("Votekick sent! Leave and rejoin 2 more times."); + } + + ShowNotification($"[VOTEKICK] Vote sent to {target.Data.PlayerName}!"); + } + catch (Exception) + { + } + } + + private void DrawVotekickTab() + { + GUILayout.BeginVertical(menuCardStyle); + try + { + GUIStyle voteInfoStyle = new GUIStyle(toggleLabelStyle) { richText = true, wordWrap = true }; + DrawMenuSectionHeader("VOTEKICK MENU"); + GUILayout.Label("Auto mode: sends 3 votes to every valid player, leaves the room, and stays armed until you press it again.", voteInfoStyle); + GUILayout.Space(5); + + string autoButtonText = votekickEveryone ? "STOP AUTO VOTEKICK + LEAVE" : "AUTO VOTEKICK + LEAVE"; + if (GUILayout.Button(autoButtonText, votekickEveryone ? activeTabStyle : btnStyle, GUILayout.Height(35))) + { + if (votekickEveryone) StopVotekickEveryoneRun(); + else StartVotekickEveryoneRun(); + } + + GUILayout.Space(5); + GUILayout.Label("Manual mode: sends 3 votes now and stays in the current room.", voteInfoStyle); + if (GUILayout.Button("SEND 3 VOTES + STAY", btnStyle, GUILayout.Height(32))) + { + SendVotekickEveryoneStay(); + } + } + finally { GUILayout.EndVertical(); } + + GUILayout.Space(10); + DrawMenuSectionHeader("TARGET VOTE"); + + if (PlayerControl.AllPlayerControls != null) + { + var safePlayersList = new System.Collections.Generic.List(); + foreach (var p in PlayerControl.AllPlayerControls) safePlayersList.Add(p); + + votekickScrollPosition = GUILayout.BeginScrollView(votekickScrollPosition); + try + { + foreach (var pc in safePlayersList) + { + if (pc == null || pc.Data == null || pc.PlayerId >= 100 || pc == PlayerControl.LocalPlayer) continue; + + GUILayout.BeginHorizontal(boxStyle); + try + { + string pName = pc.Data.PlayerName ?? "Unknown"; + bool isHost = (AmongUsClient.Instance != null && AmongUsClient.Instance.GetHost()?.Character == pc); + string displayStr = isHost ? pName + " [H]" : pName; + + GUILayout.Label(displayStr, GUILayout.Width(110)); + + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("Vote", btnStyle, GUILayout.Width(60), GUILayout.Height(25))) + { + ExecuteVotekickTarget(pc); + } + } + finally + { + GUILayout.EndHorizontal(); + } + GUILayout.Space(2); + } + } + finally + { + GUILayout.EndScrollView(); + } + } + } + + private void DrawElysiumModMenu(int windowID) + { + if (Event.current.type == EventType.Repaint && tabTransitionProgress < 1f) + { + tabTransitionProgress += Time.unscaledDeltaTime * 8f; + if (tabTransitionProgress >= 1f) { tabTransitionProgress = 1f; currentTab = targetTabIndex; } + } + + if (enableBackground && customMenuBg != null) + { + GUI.color = new Color(0.6f, 0.6f, 0.6f, 0.8f); + GUIStyle bgStyle = new GUIStyle() { normal = { background = customMenuBg } }; + GUI.Box(new Rect(0, 0, windowRect.width, windowRect.height), GUIContent.none, bgStyle); + GUI.color = Color.white; + } + + GUILayout.BeginHorizontal(); + GUILayout.Label(ApplyMenuShimmer("ElysiumModMenu Meowchelo & Carrot"), titleStyle); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("-", new GUIStyle(btnStyle) { fixedWidth = 20, fixedHeight = 18, margin = CreateRectOffset(0, 8, 6, 0) })) showMenu = false; + GUILayout.EndHorizontal(); + + GUI.color = new Color(1f, 1f, 1f, 0.1f); + GUI.Box(new Rect(0, 30, windowRect.width, 1), "", safeLineStyle); + GUI.color = Color.white; + + GUILayout.BeginArea(new Rect(0f, 31f, 130f, windowRect.height - 31f)); + GUILayout.BeginVertical(sidebarStyle, GUILayout.ExpandHeight(true)); + GUILayout.Space(5); + for (int i = 0; i < tabNames.Length; i++) + if (GUILayout.Button(tabNames[i], i == targetTabIndex ? activeSidebarBtnStyle : sidebarBtnStyle, GUILayout.Height(24))) + if (targetTabIndex != i) { targetTabIndex = i; tabTransitionProgress = 0f; scrollPosition = Vector2.zero; } + GUILayout.EndVertical(); + GUILayout.EndArea(); + + GUI.color = new Color(1f, 1f, 1f, 0.1f); + GUI.Box(new Rect(130, 31, 1, windowRect.height), "", safeLineStyle); + GUI.color = new Color(1f, 1f, 1f, tabTransitionProgress); + + GUILayout.BeginArea(new Rect(140f, 36f + ((1f - tabTransitionProgress) * 10f), windowRect.width - 150f, windowRect.height - 46f)); + scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, false, GUIStyle.none, GUI.skin.verticalScrollbar); + int tabToDraw = (tabTransitionProgress < 1f) ? targetTabIndex : currentTab; + + if (tabToDraw == 0) DrawGeneralTab(); + else if (tabToDraw == 1) DrawSelfTab(); + else if (tabToDraw == 2) DrawVisualsTab(); + else if (tabToDraw == 3) DrawPlayersTab(); + else if (tabToDraw == 4) DrawSabotagesTab(); + else if (tabToDraw == 5) DrawHostOnlyTab(); + else if (tabToDraw == 6) DrawVotekickTab(); + else if (tabToDraw == 7) DrawMenuTab(); + else if (tabToDraw == 8) DrawAnimationsTab(); + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + + GUI.color = Color.white; + GUI.DragWindow(new Rect(0, 0, 10000, 30)); + } + public static int punishmentMode = 1; + +public static bool settingsDirty = false; + +public static string[] punishmentNames = { "Null", "Warn", "Kick", "Ban" }; + +public static bool blockSpoofRPC = true; + +public static bool blockSabotageRPC = true; + +public static bool blockGameRpcInLobby = true; + +public static bool blockChatFloodRpc = true; + +public static bool blockMeetingFloodRpc = true; + +public static bool enablePasosLimit = true; + +public static bool enableLocalPasosBan = true; + +public static bool enableHostPasosBan = true; + +public static bool autoBanBrokenFriendCode = false; + +public static int chatRpcLimit = 1; + +public static float chatRpcWindow = 1f; + +public static int meetingRpcLimit = 2; + +public static float meetingRpcWindow = 9999f; +} +} diff --git a/routines/AmongUsClient_KickPlayer_BanList_Patch.cs b/routines/AmongUsClient_KickPlayer_BanList_Patch.cs new file mode 100644 index 0000000..d71ad1e --- /dev/null +++ b/routines/AmongUsClient_KickPlayer_BanList_Patch.cs @@ -0,0 +1,71 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.KickPlayer))] +public static class AmongUsClient_KickPlayer_BanList_Patch +{ + public static void Prefix(InnerNetClient __instance, int clientId, bool ban) + { + if (ban && PlayerControl.AllPlayerControls != null && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + try + { + var pc = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p.OwnerId == clientId); + if (pc != null && pc.Data != null) + { + string fc = string.IsNullOrEmpty(pc.Data.FriendCode) ? "Unknown" : pc.Data.FriendCode; + string name = pc.Data.PlayerName ?? "Unknown"; + string puid = "Unknown"; + + try + { + var client = AmongUsClient.Instance.GetClientFromCharacter(pc); + if (client != null) puid = ElysiumModMenuGUI.GetClientPuid(client); + } + catch { } + + ElysiumModMenuGUI.AddToBanList(fc, puid, name, "Host ban"); + ElysiumModMenuGUI.ShowNotification($"[BAN] {name} занесен в черный список!"); + } + } + catch { } + } + } +} diff --git a/routines/AutoChatEveryone_Start_Patch.cs b/routines/AutoChatEveryone_Start_Patch.cs new file mode 100644 index 0000000..32c8f82 --- /dev/null +++ b/routines/AutoChatEveryone_Start_Patch.cs @@ -0,0 +1,54 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +[HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.Start))] +public static class AutoChatEveryone_Start_Patch +{ + public static void Postfix() + { + ElysiumModMenuGUI.InitializeKillCooldownOnRoundStart(); + + if (ElysiumModMenuGUI.autoChatEveryone && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + ElysiumModMenuGUI.pendingAutoMeeting = true; + ElysiumModMenuGUI.autoMeetingTimer = 0f; + } + } +} diff --git a/routines/ChatController_AddChat_Patch.cs b/routines/ChatController_AddChat_Patch.cs new file mode 100644 index 0000000..3f9ae03 --- /dev/null +++ b/routines/ChatController_AddChat_Patch.cs @@ -0,0 +1,216 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +[HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] +public static class ChatController_AddChat_Patch +{ + public static bool Prefix(PlayerControl sourcePlayer, ref string chatText, bool censor, ChatController __instance) + { + if (string.IsNullOrEmpty(chatText)) return true; + string lowerText = chatText.ToLower().Trim(); + + if (ElysiumModMenuGUI.enableColorCommand && sourcePlayer != null) + { + string[] colorCommands = { "/color ", "!color ", "/col ", "!col ", "/c ", "!c " }; + string usedCmd = colorCommands.FirstOrDefault(cmd => lowerText.StartsWith(cmd)); + + if (usedCmd != null) + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + string colorInput = lowerText.Substring(usedCmd.Length).Trim(); + int colorId = -1; + + if (int.TryParse(colorInput, out int parsedId)) { if (parsedId >= 0 && parsedId <= 18) colorId = parsedId; } + else colorId = ElysiumModMenuGUI.GetColorIdByName(colorInput); + + if (colorId != -1) + { + if (colorId == 18 && ElysiumModMenuGUI.blockFortegreenChat) + { + if (HudManager.Instance?.Chat != null) + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Цвет Fortegreen запрещен хостом!"); + } + else + { + sourcePlayer.RpcSetColor((byte)colorId); + } + } + else if (sourcePlayer == PlayerControl.LocalPlayer) + { + __instance.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Неверный цвет."); + } + } + return false; + } + + if (lowerText == "/rainbow" || lowerText == "!rainbow" || lowerText == "/lgbt" || lowerText == "!lgbt") + { + if (AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) + { + if (ElysiumModMenuGUI.blockRainbowChat) + { + if (HudManager.Instance?.Chat != null) + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, "[ОШИБКА] Радуга запрещена хостом!"); + } + else + { + if (ElysiumModMenuGUI.rainbowPlayers.Contains(sourcePlayer.PlayerId)) + { + ElysiumModMenuGUI.rainbowPlayers.Remove(sourcePlayer.PlayerId); + ElysiumModMenuGUI.ShowNotification("[SERVER] Радуга ВЫКЛ."); + } + else + { + ElysiumModMenuGUI.rainbowPlayers.Add(sourcePlayer.PlayerId); + ElysiumModMenuGUI.ShowNotification("[SERVER] Радуга ВКЛ."); + } + } + } + return false; + } + } + + if (ShouldShowGhostMessage(sourcePlayer)) + { + return ShowGhostMessage(sourcePlayer, chatText, censor, __instance); + } + + return true; + } + + private static bool ShouldShowGhostMessage(PlayerControl sourcePlayer) + { + try + { + if (!ElysiumModMenuGUI.readGhostChat && !ElysiumModMenuGUI.seeGhosts) return false; + if (sourcePlayer == null || sourcePlayer.Data == null) return false; + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) return false; + if (PlayerControl.LocalPlayer.Data.IsDead) return false; + + return sourcePlayer.Data.IsDead; + } + catch { return false; } + } + + private static bool ShowGhostMessage(PlayerControl sourcePlayer, string chatText, bool censor, ChatController chat) + { + if (chat == null) return true; + + ChatBubble pooledBubble = null; + try + { + NetworkedPlayerInfo sourceData = sourcePlayer.Data; + if (sourceData == null) return true; + + pooledBubble = chat.GetPooledBubble(); + pooledBubble.transform.SetParent(chat.scroller.Inner); + pooledBubble.transform.localScale = Vector3.one; + + bool isLocal = sourcePlayer == PlayerControl.LocalPlayer; + if (isLocal) pooledBubble.SetRight(); + else pooledBubble.SetLeft(); + + bool didVote = MeetingHud.Instance != null && MeetingHud.Instance.DidVote(sourcePlayer.PlayerId); + pooledBubble.SetCosmetics(sourceData); + chat.SetChatBubbleName(pooledBubble, sourceData, sourceData.IsDead, didVote, PlayerNameColor.Get(sourceData), null); + + if (censor && AmongUs.Data.DataManager.Settings.Multiplayer.CensorChat) + { + chatText = BlockedWords.CensorWords(chatText, false); + } + + pooledBubble.SetText($"{chatText}"); + pooledBubble.AlignChildren(); + chat.AlignAllBubbles(); + + if (!chat.IsOpenOrOpening && chat.notificationRoutine == null) + { + chat.notificationRoutine = chat.StartCoroutine(chat.BounceDot()); + } + + if (!isLocal && !chat.IsOpenOrOpening) + { + SoundManager.Instance.PlaySound(chat.messageSound, false).pitch = 0.5f + sourcePlayer.PlayerId / 15f; + chat.chatNotification.SetUp(sourcePlayer, chatText); + } + + return false; + } + catch + { + try + { + if (pooledBubble != null) chat.chatBubblePool.Reclaim(pooledBubble); + } + catch { } + return true; + } + } + + + + public static void Postfix(GameStartManager __instance) + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; + if (ElysiumModMenuGUI.customStartTimer > 0f) return; + + if (ElysiumModMenuGUI.fakeStartCounterTroll) + { + try + { + sbyte[] arr = { -123, -100, -69, -42, 0, 42, 69, 100, 123 }; + sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; + PlayerControl.LocalPlayer.RpcSetStartCounter((int)b); + __instance.SetStartCounter(b); + } + catch { } + } + else if (ElysiumModMenuGUI.fakeStartCounterCustom && int.TryParse(ElysiumModMenuGUI.fakeStartInput, out int custom)) + { + try + { + PlayerControl.LocalPlayer.RpcSetStartCounter(custom); + __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); + } + catch { } + } + } +} diff --git a/routines/ChatController_SetVisible_Patch.cs b/routines/ChatController_SetVisible_Patch.cs new file mode 100644 index 0000000..813713b --- /dev/null +++ b/routines/ChatController_SetVisible_Patch.cs @@ -0,0 +1,49 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(ChatController), nameof(ChatController.SetVisible))] +public static class ChatController_SetVisible_Patch +{ + public static void Prefix(ref bool visible) + { + if (ElysiumModMenuGUI.alwaysChat) visible = true; + } +} diff --git a/routines/ChatController_Update_Patch.cs b/routines/ChatController_Update_Patch.cs new file mode 100644 index 0000000..b4bf858 --- /dev/null +++ b/routines/ChatController_Update_Patch.cs @@ -0,0 +1,67 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + + +[HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] +public static class ChatController_Update_Patch +{ + public static void Postfix(ChatController __instance) + { + try + { + if (!ElysiumModMenuGUI.enableChatDarkMode) return; + + if (__instance.freeChatField != null && __instance.freeChatField.background != null) + { + __instance.freeChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); + if (__instance.freeChatField.textArea != null && __instance.freeChatField.textArea.outputText != null) + __instance.freeChatField.textArea.outputText.color = Color.white; + } + if (__instance.quickChatField != null && __instance.quickChatField.background != null) + { + __instance.quickChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); + if (__instance.quickChatField.text != null) + __instance.quickChatField.text.color = Color.white; + } + } + catch { } + } +} diff --git a/routines/DarkMode_ChatBubblePatch.cs b/routines/DarkMode_ChatBubblePatch.cs new file mode 100644 index 0000000..d8f1975 --- /dev/null +++ b/routines/DarkMode_ChatBubblePatch.cs @@ -0,0 +1,62 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetText))] +public static class DarkMode_ChatBubblePatch +{ + public static void Postfix(ChatBubble __instance) + { + try + { + if (!ElysiumModMenuGUI.enableChatDarkMode) return; + + Transform bg = __instance.transform.Find("Background"); + if (bg != null) + { + var sr = bg.GetComponent(); + if (sr != null) sr.color = new Color32(35, 35, 35, 255); + } + if (__instance.TextArea != null) + __instance.TextArea.color = Color.white; + } + catch { } + } +} diff --git a/routines/ExtendedLobbyListPatch.cs b/routines/ExtendedLobbyListPatch.cs new file mode 100644 index 0000000..e6370c4 --- /dev/null +++ b/routines/ExtendedLobbyListPatch.cs @@ -0,0 +1,91 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.Start))] +public static class ExtendedLobbyListPatch +{ + public static Scroller scroller; + + public static bool Prefix(FindAGameManager __instance) + { + if (!ElysiumModMenuGUI.extendedLobby) return true; + try + { + if (__instance.gameContainers == null || __instance.gameContainers.Count == 0) return true; + if (__instance.gameContainers.Count > 10) return true; + + GameContainer prefab = __instance.gameContainers[0]; + GameObject holder = new GameObject("ExtendedLobbyScroller"); + holder.transform.SetParent(prefab.transform.parent); + + scroller = holder.AddComponent(); + scroller.Inner = holder.transform; + scroller.MouseMustBeOverToScroll = true; + scroller.allowY = true; + scroller.ScrollWheelSpeed = 0.4f; + scroller.SetYBoundsMin(0f); + scroller.SetYBoundsMax(4f); + + BoxCollider2D collider = prefab.transform.parent.gameObject.AddComponent(); + collider.size = new Vector2(100f, 100f); + scroller.ClickMask = collider; + + var list = new System.Collections.Generic.List(); + foreach (var gc in __instance.gameContainers) + { + gc.transform.SetParent(holder.transform); + gc.transform.localPosition = new Vector3(gc.transform.localPosition.x, gc.transform.localPosition.y, 25f); + list.Add(gc); + } + + for (int i = 0; i < 15; i++) + { + GameContainer newGc = UnityEngine.Object.Instantiate(prefab, holder.transform); + newGc.transform.localPosition = new Vector3(newGc.transform.localPosition.x, newGc.transform.localPosition.y - 0.75f * list.Count, 25f); + list.Add(newGc); + } + + __instance.gameContainers = new Il2CppReferenceArray(list.ToArray()); + return true; + } + catch { return true; } + } +} diff --git a/routines/ExtendedLobbyRefreshPatch.cs b/routines/ExtendedLobbyRefreshPatch.cs new file mode 100644 index 0000000..a723f5e --- /dev/null +++ b/routines/ExtendedLobbyRefreshPatch.cs @@ -0,0 +1,49 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.RefreshList))] +public static class ExtendedLobbyRefreshPatch +{ + public static void Postfix() + { + try { if (ElysiumModMenuGUI.extendedLobby && ExtendedLobbyListPatch.scroller != null) ExtendedLobbyListPatch.scroller.ScrollRelative(new Vector2(0f, -100f)); } catch { } + } +} diff --git a/routines/FriendCodeSpooferPatch.cs b/routines/FriendCodeSpooferPatch.cs new file mode 100644 index 0000000..26c5305 --- /dev/null +++ b/routines/FriendCodeSpooferPatch.cs @@ -0,0 +1,77 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(NetworkedPlayerInfo), nameof(NetworkedPlayerInfo.Serialize))] +public static class FriendCodeSpooferPatch +{ + private static string serializeRestoreValue = null; + + public static void Prefix(NetworkedPlayerInfo __instance) + { + try + { + serializeRestoreValue = null; + if (ElysiumModMenuGUI.PrepareLocalFriendCodeForSerialize(__instance, out serializeRestoreValue)) return; + if (!ElysiumModMenuGUI.enableFriendCodeSpoof) return; + if (__instance == null || PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) return; + if (__instance.PlayerId != PlayerControl.LocalPlayer.PlayerId) return; + + string input = ElysiumModMenuGUI.spoofFriendCodeInput ?? ""; + string clean = ""; + foreach (char c in input.ToLowerInvariant()) + { + if (char.IsWhiteSpace(c)) break; + if (char.IsLetterOrDigit(c)) clean += c; + if (clean.Length >= 10) break; + } + + if (string.IsNullOrWhiteSpace(clean)) return; + __instance.FriendCode = clean; + } + catch { } + } + + public static void Postfix(NetworkedPlayerInfo __instance) + { + ElysiumModMenuGUI.RestoreLocalFriendCodeAfterSerialize(__instance, serializeRestoreValue); + serializeRestoreValue = null; + } +} diff --git a/routines/GameManager_CheckTaskCompletion_Patch.cs b/routines/GameManager_CheckTaskCompletion_Patch.cs new file mode 100644 index 0000000..4c80f41 --- /dev/null +++ b/routines/GameManager_CheckTaskCompletion_Patch.cs @@ -0,0 +1,54 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(GameManager), nameof(GameManager.CheckTaskCompletion))] +public static class GameManager_CheckTaskCompletion_Patch +{ + public static bool Prefix(ref bool __result) + { + try + { + if (!ElysiumModMenuGUI.neverEndGame) return true; + __result = false; return false; + } + catch { return true; } + } +} diff --git a/routines/IGameOptionsExtensions_GetAdjustedNumImpostors_Patch.cs b/routines/IGameOptionsExtensions_GetAdjustedNumImpostors_Patch.cs new file mode 100644 index 0000000..083c1bd --- /dev/null +++ b/routines/IGameOptionsExtensions_GetAdjustedNumImpostors_Patch.cs @@ -0,0 +1,55 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(IGameOptionsExtensions), nameof(IGameOptionsExtensions.GetAdjustedNumImpostors))] +public static class IGameOptionsExtensions_GetAdjustedNumImpostors_Patch +{ + public static bool Prefix(IGameOptions __instance, ref int __result) + { + try + { + if (!ElysiumModMenuGUI.noSettingLimit) return true; + __result = GameOptionsManager.Instance.CurrentGameOptions.NumImpostors; + return false; + } + catch { return true; } + } +} diff --git a/routines/InvertControls_Patch.cs b/routines/InvertControls_Patch.cs new file mode 100644 index 0000000..3c8e597 --- /dev/null +++ b/routines/InvertControls_Patch.cs @@ -0,0 +1,104 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + + +[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.FixedUpdate))] +public static class InvertControls_Patch +{ + private static void SeePlayerVent(PlayerPhysics player) + { + if (GameManager.Instance.IsHideAndSeek() && player.myPlayer.Data.RoleType == RoleTypes.Impostor || player == null || + AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started) + return; + if (!SeePlayersInVent) + { + if (player.myPlayer.invisibilityAlpha == 0.3f) + { + PhantomRole? role = player.myPlayer.Data.Role as PhantomRole; + if (role != null) + { + player.myPlayer.SetInvisibility(role.isInvisible); + return; + } + else + { + player.myPlayer.cosmetics.SetPhantomRoleAlpha(1f); + player.myPlayer.invisibilityAlpha = 1; + if (player.myPlayer.inVent) + { + player.myPlayer.Visible = false; + } + } + } + return; + } + + if (player.myPlayer.inVent && player.NetId != PlayerControl.LocalPlayer.MyPhysics.NetId) + { + player.myPlayer.Visible = true; + player.myPlayer.invisibilityAlpha = 0.3f; + player.myPlayer.cosmetics.SetPhantomRoleAlpha(0.3f); + } + else + { + PhantomRole? role = player.myPlayer.Data.Role as PhantomRole; + if (role != null) + { + player.myPlayer.SetInvisibility(role.isInvisible); + } + else + { + player.myPlayer.cosmetics.SetPhantomRoleAlpha(1f); + player.myPlayer.invisibilityAlpha = 1; + } + } + } + + public static void Postfix(PlayerPhysics __instance) + { + if (__instance.AmOwner && ElysiumModMenuGUI.invertControls && __instance.body != null) + { + __instance.body.velocity = -__instance.body.velocity; + } + + SeePlayerVent(__instance); + } +} diff --git a/routines/LobbyStart_ApplyLevelSpoof.cs b/routines/LobbyStart_ApplyLevelSpoof.cs new file mode 100644 index 0000000..4f2455c --- /dev/null +++ b/routines/LobbyStart_ApplyLevelSpoof.cs @@ -0,0 +1,51 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +[HarmonyPatch(typeof(LobbyBehaviour), nameof(LobbyBehaviour.Start))] +public static class LobbyStart_ApplyLevelSpoof +{ + public static void Postfix() + { + if (ElysiumModMenuGUI.enableLevelSpoof && !ElysiumModMenuGUI.isEditingLevel && uint.TryParse(ElysiumModMenuGUI.spoofLevelString, out uint parsedLvl)) + { + ElysiumModMenuGUI.ApplyLevelSpoofValue(parsedLvl); + } + } +} diff --git a/routines/MoreLobbyInfo_FindAGameManager_HandleList_Postfix.cs b/routines/MoreLobbyInfo_FindAGameManager_HandleList_Postfix.cs new file mode 100644 index 0000000..3095bc4 --- /dev/null +++ b/routines/MoreLobbyInfo_FindAGameManager_HandleList_Postfix.cs @@ -0,0 +1,51 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(FindAGameManager), nameof(FindAGameManager.HandleList))] +public static class MoreLobbyInfo_FindAGameManager_HandleList_Postfix +{ + public static void Postfix(HttpMatchmakerManager.FindGamesListFilteredResponse response, FindAGameManager __instance) + { + if (!ElysiumModMenuGUI.moreLobbyInfo) return; + + __instance.TotalText.text = response.Metadata.AllGamesCount.ToString(); + } +} diff --git a/routines/MoreLobbyInfo_GameContainer_SetupGameInfo_Postfix.cs b/routines/MoreLobbyInfo_GameContainer_SetupGameInfo_Postfix.cs new file mode 100644 index 0000000..c5f5202 --- /dev/null +++ b/routines/MoreLobbyInfo_GameContainer_SetupGameInfo_Postfix.cs @@ -0,0 +1,78 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(GameContainer), nameof(GameContainer.SetupGameInfo))] +public static class MoreLobbyInfo_GameContainer_SetupGameInfo_Postfix +{ + public static void Postfix(GameContainer __instance) + { + if (!ElysiumModMenuGUI.moreLobbyInfo) return; + + var trueHostName = __instance.gameListing.TrueHostName; + const string separator = "<#0000>000000000000000"; + var age = __instance.gameListing.Age; + var lobbyTime = $"Age: {age / 60}:{(age % 60 < 10 ? "0" : "")}{age % 60}"; + + + int platId = (int)__instance.gameListing.Platform; + string platformStr = platId switch + { + 1 => "Epic", + 2 => "Steam", + 3 => "Mac", + 4 => "Microsoft Store", + 5 => "Itch.io", + 6 => "iOS", + 7 => "Android", + 8 => "Nintendo Switch", + 9 => "Xbox", + 10 => "PlayStation", + 112 => "Starlight", + _ => "Unknown" + }; + + string hexColor = ElysiumModMenuGUI.GetMenuAccentHex(false); + + __instance.capacity.text = $"{separator}\n{trueHostName}\n{__instance.capacity.text}\n" + + $"{GameCode.IntToGameName(__instance.gameListing.GameId)}\n" + + $"{platformStr}\n{lobbyTime}\n{separator}"; + } +} diff --git a/routines/NumberOption_Decrease_Patch.cs b/routines/NumberOption_Decrease_Patch.cs new file mode 100644 index 0000000..ec28d9e --- /dev/null +++ b/routines/NumberOption_Decrease_Patch.cs @@ -0,0 +1,61 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Decrease))] +public static class NumberOption_Decrease_Patch +{ + public static bool Prefix(NumberOption __instance) + { + try + { + if (!ElysiumModMenuGUI.noSettingLimit) return true; + if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && + (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) + return true; + __instance.Value -= __instance.Increment; + __instance.UpdateValue(); + __instance.OnValueChanged.Invoke(__instance); + __instance.AdjustButtonsActiveState(); + return false; + } + catch { return true; } + } +} diff --git a/routines/NumberOption_Increase_Patch.cs b/routines/NumberOption_Increase_Patch.cs new file mode 100644 index 0000000..1b09edc --- /dev/null +++ b/routines/NumberOption_Increase_Patch.cs @@ -0,0 +1,61 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increase))] +public static class NumberOption_Increase_Patch +{ + public static bool Prefix(NumberOption __instance) + { + try + { + if (!ElysiumModMenuGUI.noSettingLimit) return true; + if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && + (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) + return true; + __instance.Value += __instance.Increment; + __instance.UpdateValue(); + __instance.OnValueChanged.Invoke(__instance); + __instance.AdjustButtonsActiveState(); + return false; + } + catch { return true; } + } +} diff --git a/routines/NumberOption_Initialize_Patch.cs b/routines/NumberOption_Initialize_Patch.cs new file mode 100644 index 0000000..6f4ac09 --- /dev/null +++ b/routines/NumberOption_Initialize_Patch.cs @@ -0,0 +1,57 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Initialize))] +public static class NumberOption_Initialize_Patch +{ + public static void Postfix(NumberOption __instance) + { + try + { + if (!ElysiumModMenuGUI.noSettingLimit) return; + if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek && + (__instance.Title == StringNames.GameNumImpostors || __instance.Title == StringNames.GamePlayerSpeed)) + return; + __instance.ValidRange = new FloatRange(-999f, 999f); + } + catch { } + } +} diff --git a/routines/PlatformSpooferPatch.cs b/routines/PlatformSpooferPatch.cs new file mode 100644 index 0000000..a2c64c0 --- /dev/null +++ b/routines/PlatformSpooferPatch.cs @@ -0,0 +1,58 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +[HarmonyPatch(typeof(PlatformSpecificData), nameof(PlatformSpecificData.Serialize))] +public static class PlatformSpooferPatch +{ + public static void Prefix(PlatformSpecificData __instance) + { + try + { + if (__instance != null) + { + if (!ElysiumModMenuGUI.enablePlatformSpoof) return; + + __instance.Platform = ElysiumModMenuGUI.platformValues[ElysiumModMenuGUI.currentPlatformIndex]; + __instance.PlatformName = "ElysiumModMenu by Meowchelo (and one silly guy :p) https://github.com/meowchelo/ElysiumModMenu"; + } + } + catch { } + } +} diff --git a/routines/RPCSniffer_Patch.cs b/routines/RPCSniffer_Patch.cs new file mode 100644 index 0000000..e4b8b48 --- /dev/null +++ b/routines/RPCSniffer_Patch.cs @@ -0,0 +1,114 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] +public static class RPCSniffer_Patch +{ + private static readonly HashSet VanillaRPCs = ElysiumModMenuGUI.VanillaRpcIds; + + private static readonly Dictionary KnownMods = new Dictionary + { + { 157, ("RockStar", "#800000") }, + { 121, ("RockStar / Chocoo", "#800000") }, + { 167, ("TuffMenu", "#008000") }, + { 164, ("Hydra / Sicko", "#FF0000") }, + { 176, ("HostGuard / TOH", "#008000") }, + { 195, ("Polar Client", "#FFFF00") }, + { 204, ("Polar Client", "#FFFF00") }, + { 154, ("GNC", "#FF0000") }, + { 85, ("KillNet (Base)", "#FF0000") }, + { 150, ("KillNet (V2)", "#FF0000") }, + { 162, ("KNM", "#FF0000") }, + { 250, ("KillNet (Alt)", "#FF0000") }, + { 212, ("BanMod", "#008000") }, + { 213, ("BanMod", "#008000") }, + { 214, ("BanMod", "#008000") }, + { 215, ("BanMod", "#008000") }, + { 216, ("BanMod", "#008000") }, + { 217, ("BanMod", "#008000") }, + { 218, ("BanMod", "#008000") }, + { 219, ("BanMod", "#008000") }, + { 144, ("Gaff Menu", "#FF0000") }, + { 145, ("Gaff Menu", "#FF0000") }, + { 188, ("GMM", "#FF0000") }, + { 189, ("GMM", "#FF0000") }, + { 169, ("Malum", "#FF0000") }, + { 210, ("Eclipse", "#FFFF00") }, + { 173, ("Private Client", "#FF0000") }, + { 151, ("Better Among Us", "#008000") }, + { 152, ("Better Among Us", "#008000") }, + { 255, ("CrewMod", "#FFFF00") }, + { 111, ("AUM (BitCrackers)", "#FF0000") }, + { 231, ("SentinelAU", "#FF0000") }, + { 133, ("Lunar / ElysiumModMenu", "#00FFFF") }, + { 89, ("ElysiumModMenu Old", "#008000") } + }; + + public static bool Prefix(PlayerControl __instance, byte callId, MessageReader reader) + { + if (__instance == null) return true; + + + if (PlayerControl.LocalPlayer != null && __instance == PlayerControl.LocalPlayer) return true; + + ElysiumModMenuGUI.RecordPlayerRpc(__instance, callId); + + if (ElysiumModMenuGUI.LogAllRPCs) + { + + if (!VanillaRPCs.Contains(callId)) + { + string pNameSniff = (__instance.Data != null && !string.IsNullOrEmpty(__instance.Data.PlayerName)) ? __instance.Data.PlayerName : $"Player_{__instance.PlayerId}"; + + + if (KnownMods.TryGetValue(callId, out var modInfo)) + { + ElysiumModMenuGUI.ShowNotification($"[СНИФФЕР] {pNameSniff}: {modInfo.Name} ({callId})"); + } + else + { + ElysiumModMenuGUI.ShowNotification($"[СНИФФЕР] {pNameSniff} кинул неизвестный RPC: {callId}"); + } + } + } + return true; + } +} diff --git a/routines/RevealVotesCleanupPatch.cs b/routines/RevealVotesCleanupPatch.cs new file mode 100644 index 0000000..6e8a865 --- /dev/null +++ b/routines/RevealVotesCleanupPatch.cs @@ -0,0 +1,63 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +[HarmonyPatch(typeof(MeetingHud), "PopulateResults")] +public static class RevealVotesCleanupPatch +{ + public static void Prefix(MeetingHud __instance) + { + if (!ElysiumModMenuGUI.RevealVotesEnabled) return; + try + { + foreach (var item in __instance.playerStates) + { + if (item == null) continue; + var component = item.transform.GetComponent(); + if (component != null && component.Votes.Count != 0) + { + foreach (var sprite in component.Votes) Object.DestroyImmediate(sprite.gameObject); + component.Votes.Clear(); + } + } + RevealVotesPatch._votedPlayers.Clear(); + } + catch { } + } +} diff --git a/routines/RevealVotesPatch.cs b/routines/RevealVotesPatch.cs new file mode 100644 index 0000000..1caf904 --- /dev/null +++ b/routines/RevealVotesPatch.cs @@ -0,0 +1,75 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(MeetingHud), "Update")] +public static class RevealVotesPatch +{ + internal static List _votedPlayers = new List(); + public static void Prefix(MeetingHud __instance) + { + if (!ElysiumModMenuGUI.RevealVotesEnabled) return; + try + { + if ((int)__instance.state >= 4) return; + foreach (var item in __instance.playerStates) + { + if (item == null) continue; + var playerById = GameData.Instance.GetPlayerById(item.TargetPlayerId); + if (playerById == null || playerById.Disconnected || item.VotedFor == PlayerVoteArea.HasNotVoted || + item.VotedFor == PlayerVoteArea.MissedVote || item.VotedFor == PlayerVoteArea.DeadVote || _votedPlayers.Contains(item.TargetPlayerId)) continue; + _votedPlayers.Add(item.TargetPlayerId); + if (item.VotedFor != PlayerVoteArea.SkippedVote) + { + foreach (var item2 in __instance.playerStates) if (item2.TargetPlayerId == item.VotedFor) { __instance.BloopAVoteIcon(playerById, 0, item2.transform); break; } + } + else if (__instance.SkippedVoting != null) __instance.BloopAVoteIcon(playerById, 0, __instance.SkippedVoting.transform); + } + foreach (var item3 in __instance.playerStates) + { + if (item3 == null) continue; + var component = item3.transform.GetComponent(); + if (component != null) foreach (var sprite in component.Votes) sprite.gameObject.SetActive(true); + } + if (__instance.SkippedVoting != null) __instance.SkippedVoting.SetActive(true); + } + catch { } + } +} diff --git a/routines/UnlockCosmetics_HatManager_Initialize_Postfix.cs b/routines/UnlockCosmetics_HatManager_Initialize_Postfix.cs new file mode 100644 index 0000000..66215c4 --- /dev/null +++ b/routines/UnlockCosmetics_HatManager_Initialize_Postfix.cs @@ -0,0 +1,57 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(HatManager), nameof(HatManager.Initialize))] +public static class UnlockCosmetics_HatManager_Initialize_Postfix +{ + public static void Postfix(HatManager __instance) + { + if (!ElysiumModMenuGUI.unlockCosmetics) return; + + foreach (var bundle in __instance.allBundles) bundle.Free = true; + foreach (var hat in __instance.allHats) hat.Free = true; + foreach (var nameplate in __instance.allNamePlates) nameplate.Free = true; + foreach (var pet in __instance.allPets) pet.Free = true; + foreach (var skin in __instance.allSkins) skin.Free = true; + foreach (var visor in __instance.allVisors) visor.Free = true; + foreach (var starBundle in __instance.allStarBundles) starBundle.price = 0; + } +} diff --git a/routines/UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix.cs b/routines/UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix.cs new file mode 100644 index 0000000..6d92135 --- /dev/null +++ b/routines/UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix.cs @@ -0,0 +1,51 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + + +[HarmonyPatch(typeof(PlayerPurchasesData), nameof(PlayerPurchasesData.GetPurchase))] +public static class UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix +{ + public static bool Prefix(ref bool __result) + { + if (!ElysiumModMenuGUI.unlockCosmetics) return true; + __result = true; + return false; + } +} diff --git a/ui/GeneralMapsChat.cs b/ui/GeneralMapsChat.cs new file mode 100644 index 0000000..89ec3b4 --- /dev/null +++ b/ui/GeneralMapsChat.cs @@ -0,0 +1,831 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { +private void DrawAutoHostMainTab() + { + GUILayout.BeginHorizontal(); + for (int i = 0; i < autoHostSubTabs.Length; i++) + { + string subTabLabel = i < hostOnlySubTabs.Length ? hostOnlySubTabs[i] : autoHostSubTabs[i]; + if (GUILayout.Button(subTabLabel, currentAutoHostSubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) + { + currentAutoHostSubTab = i; + scrollPosition = Vector2.zero; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(8); + + if (currentAutoHostSubTab == 0) DrawLobbyControls(); + else if (currentAutoHostSubTab == 1) DrawPlayersRoles(); + else if (currentAutoHostSubTab == 2) DrawAntiCheatTab(); + else if (currentAutoHostSubTab == 3) DrawAutoHostTab(); + } + +private void DrawMapsTab() + { + GUILayout.BeginVertical(menuCardStyle); + + DrawMenuSectionHeader(L("LOBBY CONTROL", "КОНТРОЛЬ ЛОББИ")); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(L("Spawn Lobby", "Создать лобби"), btnStyle, GUILayout.Height(30))) SpawnLobby(); + if (GUILayout.Button(L("Despawn Lobby", "Удалить лобби"), btnStyle, GUILayout.Height(30))) DespawnLobby(); + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + + DrawMenuSectionHeader(L("MAP CONTROL", "КОНТРОЛЬ КАРТЫ")); + isManualMapSpawn = DrawToggle(isManualMapSpawn, L("Manual Map Spawn Mode", "Ручной спавн карты"), 250); + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + GUILayout.Label(L("Select Map:", "Выбор карты:"), GUILayout.Width(100)); + selectedMapSpawnIdx = (int)GUILayout.HorizontalSlider((int)selectedMapSpawnIdx, 0, mapSpawnNames.Length - 1, sliderStyle, sliderThumbStyle, GUILayout.Width(200)); + GUILayout.Label($"{mapSpawnNames[(int)selectedMapSpawnIdx]}", new GUIStyle(GUI.skin.label) { richText = true }); + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button(L("Spawn Map", "Создать карту"), activeTabStyle, GUILayout.Height(30))) SpawnMap((int)selectedMapSpawnIdx); + if (GUILayout.Button(L("Despawn Map", "Удалить карту"), btnStyle, GUILayout.Height(30))) DespawnCurrentMap(); + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + + DrawMenuSectionHeader(L("ROOM TELEPORTS (IN-GAME)", "ТЕЛЕПОРТЫ ПО КОМНАТАМ (В ИГРЕ)")); + if (ShipStatus.Instance != null && PlayerControl.LocalPlayer != null) + { + mapsScrollPos = GUILayout.BeginScrollView(mapsScrollPos, GUILayout.Height(160)); + var locations = GetTeleportLocations(); + int columns = 3; + int count = 0; + + GUILayout.BeginHorizontal(); + foreach (var loc in locations) + { + if (GUILayout.Button(loc.Key, btnStyle, GUILayout.Width(135), GUILayout.Height(30))) + { + TeleportTo(loc.Value); + ShowNotification($"[TELEPORT] {L("Moved to:", "Перемещен в:")} {loc.Key}"); + } + + count++; + if (count % columns == 0) + { + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + } + } + GUILayout.EndHorizontal(); + GUILayout.EndScrollView(); + } + else + { + GUILayout.Label($"{L("Teleports are only available when you are on a map.", "Телепорты доступны только когда вы находитесь на карте.")}", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); + } + + GUILayout.EndVertical(); + } + +private void DrawChatSettingsTab() + { + GUILayout.BeginVertical(boxStyle); + GUILayout.Label(L("CHAT SETTINGS & LOGS", "НАСТРОЙКИ ЧАТА И ЛОГИ"), headerStyle); + GUILayout.Space(10); + + string hexColor = GetMenuAccentHex(); + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(GUILayout.Width(300)); + GUILayout.Label($"{L("LOCAL FEATURES", "ЛОКАЛЬНЫЕ ФУНКЦИИ")}", toggleLabelStyle); + GUILayout.Space(6); + alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 280); + GUILayout.Space(2); + readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 280); + GUILayout.Space(4); + DrawGhostChatColorControl(280f); + GUILayout.Space(2); + enableExtendedChat = DrawToggle(enableExtendedChat, L("Extended Chat (120 chars)", "Длинный чат (120 симв.)"), 280); + GUILayout.Space(2); + enableFastChat = DrawToggle(enableFastChat, L("Fast Chat (3.1 to 2.1", "Быстрый чат (c 3.1 до 2.1)"), 280); + GUILayout.Space(2); + allowLinksAndSymbols = DrawToggle(allowLinksAndSymbols, L("Unlock Extra Characters", "Разрешить все символы"), 280); + GUILayout.Space(2); + enableSpellCheck = DrawToggle(enableSpellCheck, L("Spell Check (Basic)", "Проверка орфографии (Базовая)"), 280); + GUILayout.EndVertical(); + + GUILayout.BeginVertical(GUILayout.ExpandWidth(true)); + GUILayout.Label($"{L("UTILITY OPTIONS", "УТИЛИТЫ")}", toggleLabelStyle); + GUILayout.Space(6); + enableChatHistory = DrawToggle(enableChatHistory, L("Chat History (Up/Down)", "История чата (Стрелочки)"), 280); + GUILayout.Space(2); + GUILayout.BeginHorizontal(); + GUILayout.Label($"{L("History size:", "Размер истории:")} {chatHistoryLimit}", new GUIStyle(toggleLabelStyle) { richText = true }, GUILayout.Height(22), GUILayout.Width(130)); + chatHistoryLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(chatHistoryLimit, 5f, 80f, sliderStyle, sliderThumbStyle, GUILayout.Width(145)), 5, 80); + TrimChatHistoryToLimit(); + GUILayout.EndHorizontal(); + GUILayout.Space(2); + enableClipboard = DrawToggle(enableClipboard, L("Clipboard (Ctrl+C/V)", "Буфер обмена (Ctrl+C/V)"), 280); + GUILayout.Space(2); + enableChatMessageDoubleClickCopy = DrawToggle(enableChatMessageDoubleClickCopy, L("Double-click Copy Message", "Дабл-клик копирует сообщение"), 280); + GUILayout.Space(2); + enableChatNameColorCopy = DrawToggle(enableChatNameColorCopy, L("Click Copy Name", "Клик по нику копирует ник"), 280); + GUILayout.Space(2); + enableChatLog = DrawToggle(enableChatLog, L("Save Chat Log to File", "Сохранять лог чата в файл"), 280); + GUILayout.Space(2); + enableChatDarkMode = DrawToggle(enableChatDarkMode, L("Dark Chat Theme", "Темная тема чата"), 280); + if (enableChatDarkMode && GUILayout.Button(L("Turn Off Dark Chat", "Выключить темный чат"), btnStyle, GUILayout.Width(180), GUILayout.Height(24))) + { + enableChatDarkMode = false; + SaveConfig(); + } + + GUILayout.Space(8); + + GUILayout.Label($"{L("HOST LOBBY OPTIONS", "НАСТРОЙКИ ХОСТА")}", toggleLabelStyle); + GUILayout.Space(6); + enableColorCommand = DrawToggle(enableColorCommand, L("Enable /color command", "Разрешить команду /color"), 280); + GUILayout.Space(2); + blockFortegreenChat = DrawToggle(blockFortegreenChat, L("Block Fortegreen Chat", "Запрет чата Fortegreen"), 280); + GUILayout.Space(2); + blockRainbowChat = DrawToggle(blockRainbowChat, L("Block Rainbow Chat", "Запрет радужного чата"), 280); + GUILayout.EndVertical(); + + GUILayout.EndHorizontal(); + + GUILayout.Space(12); + + GUILayout.Label($"{L("CHAT SENDER", "ОТПРАВКА ЧАТА")}", toggleLabelStyle); + GUILayout.Space(6); + + GUILayout.BeginVertical(boxStyle); + GUILayout.Space(6); + + GUIStyle macFieldStyle = new GUIStyle(GUI.skin.textField) + { + fontSize = 12, + alignment = TextAnchor.MiddleLeft + }; + macFieldStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); + macFieldStyle.padding = new RectOffset(); + macFieldStyle.padding.left = 12; + macFieldStyle.padding.right = 12; + macFieldStyle.padding.top = 8; + macFieldStyle.padding.bottom = 8; + macFieldStyle.margin = new RectOffset(); + macFieldStyle.margin.left = 4; + macFieldStyle.margin.right = 4; + macFieldStyle.margin.top = 4; + macFieldStyle.margin.bottom = 4; + + Rect chatInputRect = GUILayoutUtility.GetRect(10f, 34f, GUILayout.ExpandWidth(true), GUILayout.Height(34)); + GUI.Box(chatInputRect, string.Empty, macFieldStyle); + + string drawText = string.IsNullOrEmpty(customChatMessage) + ? L("Type a message...", "Введите сообщение...") + : customChatMessage; + + if (customChatInputFocused && (Time.unscaledTime % 1f) < 0.5f) + drawText += "|"; + + GUIStyle chatInputTextStyle = new GUIStyle(GUI.skin.label) + { + alignment = TextAnchor.MiddleLeft, + clipping = TextClipping.Clip, + richText = false, + fontSize = 12 + }; + chatInputTextStyle.normal.textColor = whiteMenuTheme ? new Color(0.12f, 0.12f, 0.12f, 1f) : new Color(0.9f, 0.9f, 0.9f, 1f); + + Rect textRect = new Rect(chatInputRect.x + 12f, chatInputRect.y + 4f, chatInputRect.width - 24f, chatInputRect.height - 8f); + GUI.Label(textRect, drawText, chatInputTextStyle); + + Event e = Event.current; + if (e != null) + { + if (e.type == EventType.MouseDown) + { + customChatInputFocused = chatInputRect.Contains(e.mousePosition); + if (customChatInputFocused) e.Use(); + } + else if (customChatInputFocused && e.type == EventType.KeyDown) + { + if (HandleClipboardShortcut(e, ref customChatMessage, 120)) + { + } + else if (e.keyCode == KeyCode.Backspace) + { + if (!string.IsNullOrEmpty(customChatMessage)) + customChatMessage = customChatMessage.Substring(0, customChatMessage.Length - 1); + e.Use(); + } + else if (e.keyCode == KeyCode.Escape) + { + customChatInputFocused = false; + e.Use(); + } + else if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) + { + TrySendCustomChatMessage(customChatMessage); + e.Use(); + } + else if (!char.IsControl(e.character)) + { + if (customChatMessage == null) customChatMessage = string.Empty; + if (customChatMessage.Length < 120) + customChatMessage += e.character; + e.Use(); + } + } + } + + GUILayout.Space(10); + + GUILayout.BeginHorizontal(GUILayout.Height(30)); + if (GUILayout.Button(L("Send Chat", "Отправить"), btnStyle, GUILayout.Width(150), GUILayout.Height(30))) + TrySendCustomChatMessage(customChatMessage); + + GUILayout.Space(10); + string spamBtnText = customChatSpamEnabled ? L("Spam: ON", "Спам: ВКЛ") : L("Spam: OFF", "Спам: ВЫКЛ"); + if (GUILayout.Button(spamBtnText, customChatSpamEnabled ? activeTabStyle : btnStyle, GUILayout.Width(150), GUILayout.Height(30))) + customChatSpamEnabled = !customChatSpamEnabled; + + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.Space(12); + + GUILayout.BeginHorizontal(GUILayout.Height(24)); + GUILayout.Label($"{L("Delay:", "Задержка:")} {Mathf.Round(customChatSpamDelay * 10f) / 10f}s", new GUIStyle(toggleLabelStyle) { fontSize = 11 }, GUILayout.Height(22), GUILayout.Width(122)); + customChatSpamDelay = GUILayout.HorizontalSlider(customChatSpamDelay, 0.5f, 10f, sliderStyle, sliderThumbStyle, GUILayout.Width(300)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.Label($"{L("COMMANDS & INFO", "КОМАНДЫ И ИНФОРМАЦИЯ")}", toggleLabelStyle); + GUILayout.Space(4); + + GUILayout.Label($"{L("Whisper:", "Шепот:")} /w, /pm, /msg [Name/ID/Color] [Text]", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 12 }); + GUILayout.Label($"{L("Sends a private message to a player on your screen only.", "Отправляет личное сообщение выбранному игроку (видит только он и вы).")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); + + GUILayout.Space(6); + + GUILayout.Label($"Log Info: {L("ChatLog.txt clears every 3 game restarts.", "Файл ChatLog.txt очищается каждые 3 запуска игры.")}", new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); + + GUILayout.EndVertical(); + } + +private void TrySendCustomChatMessage(string rawText) + { + if (string.IsNullOrWhiteSpace(rawText)) return; + if (PlayerControl.LocalPlayer == null) return; + + try + { + PlayerControl.LocalPlayer.RpcSendChat(rawText.Trim()); + } + catch { } + } + +private static readonly HashSet BasicSpellDictionary = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "hello","hi","gg","wp","yes","no","ok","pls","please","thanks","thx","go","come","start","skip","vote","report","body","kill","who","where","why", + "привет","да","нет","ок","пж","пожалуйста","спасибо","го","старт","скип","голос","репорт","труп","килл","кто","где","почему","лол" + }; + +private static void TrySpellCheckNotify(string text) + { + if (!enableSpellCheck || string.IsNullOrWhiteSpace(text)) return; + if (text.StartsWith("/") || text.StartsWith("!")) return; + + try + { + var words = Regex.Matches(text.ToLower(), @"[a-zа-яё]{3,}"); + List suspicious = new List(); + foreach (Match m in words) + { + string w = m.Value; + if (w.Length < 3) continue; + if (BasicSpellDictionary.Contains(w)) continue; + if (suspicious.Contains(w)) continue; + suspicious.Add(w); + if (suspicious.Count >= 4) break; + } + + if (suspicious.Count > 0) + { + string joined = string.Join(", ", suspicious); + ShowNotification($"[SPELL] Проверь слова: {joined}"); + } + } + catch { } + } + +private static void UpsertPlayerHistory(PlayerControl pc) + { + try + { + if (pc == null || pc.Data == null || pc.Data.Disconnected) return; + string name = string.IsNullOrEmpty(pc.Data.PlayerName) ? "Unknown" : pc.Data.PlayerName; + string fc = GetDisplayedFriendCode(pc.Data); + string puid = "Unknown"; + string platform = "Unknown"; + string customPlatform = ""; + int level = 1; + + try + { + uint rawLevel = pc.Data.PlayerLevel; + if (rawLevel != uint.MaxValue && rawLevel < 10000) level = (int)rawLevel + 1; + } + catch { } + + try + { + var client = AmongUsClient.Instance?.GetClientFromCharacter(pc); + if (client != null) + { + platform = GetPlatform(client); + customPlatform = GetCustomPlatformName(client); + puid = GetClientPuid(client); + } + } + catch { } + + string key = $"{fc}|{puid}|{name}"; + var item = playerHistoryEntries.FirstOrDefault(x => $"{x.FriendCode}|{x.Puid}|{x.Name}" == key); + bool changed = false; + if (item == null) + { + item = new PlayerHistoryEntry + { + Name = name, + FriendCode = fc, + Puid = puid, + Platform = platform, + CustomPlatform = customPlatform, + Level = level, + FirstSeenUtc = DateTime.UtcNow, + LastSeenUtc = DateTime.UtcNow, + IsOnline = true + }; + playerHistoryEntries.Add(item); + changed = true; + } + else + { + changed = item.Name != name || + item.FriendCode != fc || + item.Puid != puid || + item.Platform != platform || + item.CustomPlatform != customPlatform || + item.Level != level || + !item.IsOnline || + item.LeftUtc.HasValue; + item.Name = name; + item.FriendCode = fc; + item.Puid = puid; + item.Platform = platform; + item.CustomPlatform = customPlatform; + item.Level = level; + item.LastSeenUtc = DateTime.UtcNow; + item.LeftUtc = null; + item.IsOnline = true; + } + playerHistoryKeysById[pc.PlayerId] = key; + if (changed) WritePlayerHistoryFile(); + } + catch { } + } + +private static string GetCustomPlatformName(ClientData client) + { + try + { + string value = client?.PlatformData?.PlatformName; + if (string.IsNullOrWhiteSpace(value)) return ""; + value = Regex.Replace(value, "<.*?>", string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(value)) return ""; + + string platform = GetPlatform(client); + if (value.Equals(platform, StringComparison.OrdinalIgnoreCase)) return ""; + if (value.Equals(client.PlatformData.Platform.ToString(), StringComparison.OrdinalIgnoreCase)) return ""; + return value; + } + catch { return ""; } + } + +public static string GetClientPuid(ClientData client) + { + if (client == null) return "Unknown"; + + try + { + string direct = client.ProductUserId; + if (!string.IsNullOrWhiteSpace(direct)) return direct.Trim(); + } + catch { } + + string[] memberNames = { "ProductUserId", "productUserId", "Puid", "PUID", "puid", "EosId", "EOSId", "ProductId", "PlayerId" }; + foreach (string memberName in memberNames) + { + try + { + PropertyInfo prop = client.GetType().GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + object value = prop?.GetValue(client, null); + if (value != null && !string.IsNullOrWhiteSpace(value.ToString())) return value.ToString().Trim(); + } + catch { } + + try + { + FieldInfo field = client.GetType().GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + object value = field?.GetValue(client); + if (value != null && !string.IsNullOrWhiteSpace(value.ToString())) return value.ToString().Trim(); + } + catch { } + } + + return "Unknown"; + } + +private static string FormatPlatformHistory(PlayerHistoryEntry entry) + { + if (entry == null) return "Unknown"; + return string.IsNullOrWhiteSpace(entry.CustomPlatform) + ? entry.Platform + : $"{entry.Platform} + custom: {entry.CustomPlatform}"; + } + +private static string PlayerHistoryFilePath() + { + string folder = string.IsNullOrWhiteSpace(Plugin.ElysiumFolder) + ? System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "ElysiumModMenu") + : Plugin.ElysiumFolder; + return System.IO.Path.Combine(folder, "ElysiumPlayerHistory.txt"); + } + +private static void MarkPlayerHistoryLeft(byte playerId) + { + try + { + if (!playerHistoryKeysById.TryGetValue(playerId, out string key)) return; + var item = playerHistoryEntries.FirstOrDefault(x => $"{x.FriendCode}|{x.Puid}|{x.Name}" == key); + if (item == null || !item.IsOnline) return; + + item.IsOnline = false; + item.LeftUtc = DateTime.UtcNow; + item.LastSeenUtc = item.LeftUtc.Value; + WritePlayerHistoryFile(); + } + catch { } + } + +public static void RecordPlayerRpc(PlayerControl pc, byte callId) + { + try + { + if (VanillaRpcIds.Contains(callId)) return; + if (pc == null || pc.Data == null) return; + UpsertPlayerHistory(pc); + + if (!playerHistoryKeysById.TryGetValue(pc.PlayerId, out string key)) return; + var item = playerHistoryEntries.FirstOrDefault(x => $"{x.FriendCode}|{x.Puid}|{x.Name}" == key); + if (item == null) return; + + if (!item.RpcCalls.Contains(callId)) + { + item.RpcCalls.Add(callId); + item.RpcCalls.Sort(); + WritePlayerHistoryFile(); + } + } + catch { } + } + +private static string FormatRpcHistory(PlayerHistoryEntry entry) + { + if (entry == null || entry.RpcCalls == null || entry.RpcCalls.Count == 0) return "нет"; + byte[] customRpcCalls = entry.RpcCalls.Where(x => !VanillaRpcIds.Contains(x)).Distinct().OrderBy(x => x).ToArray(); + if (customRpcCalls.Length == 0) return "нет"; + return string.Join(", ", customRpcCalls.Select(x => x.ToString()).ToArray()); + } + +private static void WritePlayerHistoryFile() + { + try + { + string path = PlayerHistoryFilePath(); + System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)); + + List lines = new List + { + "ElysiumModMenu Player History", + $"Updated UTC: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}", + "" + }; + + foreach (var e in playerHistoryEntries.OrderByDescending(x => x.LastSeenUtc)) + { + string left = e.LeftUtc.HasValue ? e.LeftUtc.Value.ToString("yyyy-MM-dd HH:mm:ss") : "online"; + lines.Add($"Nick: {e.Name}"); + lines.Add($"Level: {e.Level}"); + lines.Add($"FriendCode: {e.FriendCode}"); + lines.Add($"PUID: {e.Puid}"); + lines.Add($"Joined UTC: {e.FirstSeenUtc:yyyy-MM-dd HH:mm:ss}"); + lines.Add($"Left UTC: {left}"); + lines.Add($"Platform: {FormatPlatformHistory(e)}"); + lines.Add($"RPC calls: {FormatRpcHistory(e)}"); + lines.Add(new string('-', 48)); + } + + System.IO.File.WriteAllLines(path, lines.ToArray(), Encoding.UTF8); + } + catch { } + } + +private void TryHostOnlyKillAuraTick() + { + if (!killAuraHostOnly) + { + killAuraTimer = 0f; + return; + } + + if (AmongUsClient.Instance == null) return; + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.Data == null) return; + if (PlayerControl.LocalPlayer.Data.IsDead) return; + if (!RoleManager.IsImpostorRole(PlayerControl.LocalPlayer.Data.RoleType)) return; + if (MeetingHud.Instance != null) return; + if (PlayerControl.LocalPlayer.inVent || PlayerControl.LocalPlayer.onLadder) return; + if (!noKillCooldownHostOnly && GetRemainingKillCooldown(PlayerControl.LocalPlayer.PlayerId) > 0.05f) return; + + killAuraTimer += Time.deltaTime; + if (killAuraTimer < 0.05f) return; + + if (PlayerControl.AllPlayerControls == null) return; + + PlayerControl nearestTarget = null; + float nearestDistance = float.MaxValue; + Vector3 localPos = PlayerControl.LocalPlayer.transform.position; + Vector2 localPos2D = new Vector2(localPos.x, localPos.y); + + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc == PlayerControl.LocalPlayer || pc.Data == null) continue; + if (pc.Data.Disconnected || pc.Data.IsDead) continue; + if (pc.inVent || pc.onLadder) continue; + + Vector3 targetPos = pc.transform.position; + float dist = Vector2.Distance(localPos2D, new Vector2(targetPos.x, targetPos.y)); + if (dist <= 2.2f && dist < nearestDistance) + { + nearestDistance = dist; + nearestTarget = pc; + } + } + + if (nearestTarget == null) return; + + try + { + PlayerControl.LocalPlayer.CmdCheckMurder(nearestTarget); + PlayerControl.LocalPlayer.RpcMurderPlayer(nearestTarget, true); + + if (AmongUsClient.Instance.AmHost) + PlayerControl.LocalPlayer.SetKillTimer(noKillCooldownHostOnly ? 0f : GetConfiguredKillCooldown()); + + killAuraTimer = 0f; + } + catch { } + } + +private void DrawAntiCheatTab() + { + float antiCheatColumnWidth = (windowRect.width - 186f) / 2f; + if (antiCheatColumnWidth < 282f) antiCheatColumnWidth = 282f; + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(menuCardStyle, GUILayout.Width(antiCheatColumnWidth)); + + DrawMenuSectionHeader(L("PUNISHMENT SYSTEM", "СИСТЕМА НАКАЗАНИЙ")); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + GUILayout.Label(L("Mode:", "Режим:"), toggleLabelStyle, GUILayout.Width(60)); + + GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetMenuAccentColor() } }; + + if (GUILayout.Button("<", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) + { + punishmentMode--; + if (punishmentMode < 0) punishmentMode = punishmentNames.Length - 1; + settingsDirty = true; + } + + GUILayout.Label(punishmentNames[punishmentMode], middleLabelStyle, GUILayout.ExpandWidth(true), GUILayout.Height(25)); + + if (GUILayout.Button(">", btnStyle, GUILayout.Width(25), GUILayout.Height(25))) + { + punishmentMode++; + if (punishmentMode >= punishmentNames.Length) punishmentMode = 0; + settingsDirty = true; + } + GUILayout.EndHorizontal(); + + string modeDesc = punishmentMode switch + { + 0 => "Null: Пакеты блокируются без действий.", + 1 => "Warn: Блокировка + Уведомление на экран.", + 2 => "Kick: Игрок будет исключен из лобби.", + 3 => "Ban: Игрок будет забанен (Host Only).", + _ => "" + }; + GUILayout.Label(modeDesc, new GUIStyle(GUI.skin.label) { richText = true, fontSize = 11, wordWrap = true }); + + GUILayout.Space(12); + DrawMenuSectionHeader(L("RPC PROTECTIONS", "ЗАЩИТА RPC")); + + blockSpoofRPC = DrawToggle(blockSpoofRPC, L("Block Spoof RPC", "Блокировать spoof RPC"), 250); + GUILayout.Space(5); + blockSabotageRPC = DrawToggle(blockSabotageRPC, L("Block Sabotage & Meetings", "Блокировать саботажи и митинги"), 250); + GUILayout.Space(5); + blockGameRpcInLobby = DrawToggle(blockGameRpcInLobby, L("Block Game RPC in Lobby", "Блокировать игровые RPC в лобби"), 250); + GUILayout.Space(5); + + autoBanPlatformSpoof = DrawToggle(autoBanPlatformSpoof, L("Auto-Ban Platform Spoof (Host)", "Авто-бан Platform Spoof (Хост)"), 250); + GUILayout.Space(5); + banCustomPlatformsFromTxt = DrawToggle(banCustomPlatformsFromTxt, L("Ban Custom Platforms From TXT", "Бан кастом платформ из TXT"), 250); + GUILayout.Space(5); + + blockMeetingFloodRpc = DrawToggle(blockMeetingFloodRpc, L("Block Meeting RPC Flood", "Блокировать флуд RPC митинга"), 250); + GUILayout.Space(5); + blockChatFloodRpc = DrawToggle(blockChatFloodRpc, L("Block Chat RPC Flood", "Блокировать флуд RPC чата"), 250); + GUILayout.Space(5); + enablePasosLimit = DrawToggle(enablePasosLimit, L("RPC Anti-Cheat", "RPC Античит"), 250); + GUILayout.Space(5); + oldAntiCheatVersion = DrawToggle(oldAntiCheatVersion, L("anti-cheat old version", "anti-cheat old version"), 250); + GUILayout.Space(5); + banMalformedPacketSender = DrawToggle(banMalformedPacketSender, L("Ban Malformed Sender (Host)", "Бан за кривые пакеты (Хост)"), 250); + GUILayout.Space(5); + enableQuickChatEmptyGuard = DrawToggle(enableQuickChatEmptyGuard, L("QuickChat Anti-Crash", "Анти-краш QuickChat"), 250); + GUILayout.Space(5); + banQuickChatEmptySpammer = DrawToggle(banQuickChatEmptySpammer, L("Ban QuickChat Spammer (Host)", "Бан за QuickChat спам (Хост)"), 250); + GUILayout.Space(5); + GUILayout.Space(15); + DrawMenuSectionHeader(L("OTHER PROTECTIONS", "ПРОЧАЯ ЗАЩИТА")); + + disableVoteKicks = DrawToggle(disableVoteKicks, L("Disable Vote Kicks (Host)", "Запрет кика голосованием (Хост)"), 250); + GUILayout.Space(5); + + autoKickBugs = DrawToggle(autoKickBugs, L("Auto-Kick Fortegreen", "Авто-кик багнутых игроков"), 250); + if (autoKickBugs) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(L("Timer:", "Таймер:"), new GUIStyle(toggleLabelStyle), GUILayout.Height(22), GUILayout.Width(62)); + autoKickTimer = GUILayout.HorizontalSlider(autoKickTimer, 1f, 15f, sliderStyle, sliderThumbStyle, GUILayout.Width(112)); + GUILayout.Space(8); + GUILayout.Label(autoKickTimer.ToString("0.0") + "s", menuBadgeStyle, GUILayout.Width(46), GUILayout.Height(22)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + } + GUILayout.Space(5); + autoBanBrokenFriendCode = DrawToggle(autoBanBrokenFriendCode, L("Auto-Ban Broken FriendCode (Host)", "Авто-бан сломанного FriendCode (Хост)"), 250); + GUILayout.Space(5); + autoKickLowLevelEnabled = DrawToggle(autoKickLowLevelEnabled, L("Kick Low Level (Host)", "Кик по уровню (Хост)"), 250); + if (autoKickLowLevelEnabled) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(L("Min level:", "Мин. уровень:"), new GUIStyle(toggleLabelStyle), GUILayout.Height(22), GUILayout.Width(86)); + int oldMinLevel = autoKickMinLevel; + autoKickMinLevel = Mathf.Clamp((int)GUILayout.HorizontalSlider(autoKickMinLevel, 1f, 300f, sliderStyle, sliderThumbStyle, GUILayout.Width(112)), 1, 300); + if (oldMinLevel != autoKickMinLevel) settingsDirty = true; + GUILayout.Space(8); + GUILayout.Label(autoKickMinLevel.ToString(), menuBadgeStyle, GUILayout.Width(46), GUILayout.Height(22)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + } + GUILayout.Space(5); + banBotsEnabled = DrawToggle(banBotsEnabled, L("Ban Bots (Host)", "Бан ботов (Хост)"), 250); + + GUILayout.EndVertical(); + GUILayout.Space(10); + + GUILayout.BeginVertical(menuCardStyle, GUILayout.Width(antiCheatColumnWidth), GUILayout.ExpandHeight(true)); + DrawMenuSectionHeader(L("BAN LIST", "БАН ЛИСТ")); + autoBanEnabled = DrawToggle(autoBanEnabled, L("Auto-Ban Blacklisted Players", "Авто-бан игроков из списка"), 250); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + string defaultBanText = L("Enter Friend Code", "Введите Friend Code"); + string banValue = string.IsNullOrEmpty(banInput) && !isEditingBan ? defaultBanText : banInput; + + if (DrawPseudoInputButton(banValue, isEditingBan, 25f, 46)) + { + isEditingBan = !isEditingBan; + isEditingGhostChatColor = false; + ResetAllBindWaits(); + } + + if (GUILayout.Button(L("ADD", "ДОБАВИТЬ"), btnStyle, GUILayout.Width(75f), GUILayout.Height(25f))) + { + if (!string.IsNullOrWhiteSpace(banInput)) + { + AddToBanList(banInput.Trim(), "Manual", "Unknown", "Manual ban"); + banInput = ""; + isEditingBan = false; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + banListScroll = GUILayout.BeginScrollView(banListScroll); + + if (bannedEntries.Count == 0) + { + GUILayout.FlexibleSpace(); + GUILayout.Label($"{L("Ban list is empty.", "Бан лист пуст.")}", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); + GUILayout.FlexibleSpace(); + } + else + { + for (int i = 0; i < bannedEntries.Count; i++) + { + string entry = bannedEntries[i]; + if (string.IsNullOrWhiteSpace(entry)) continue; + + string[] parts = entry.Split('|'); + string disp = parts.Length >= 3 ? $"{parts[2]} ({parts[0]})" : entry; + + GUILayout.BeginHorizontal(boxStyle); + GUILayout.Label(disp, new GUIStyle(GUI.skin.label) { fontSize = 12 }, GUILayout.Width(185)); + GUILayout.FlexibleSpace(); + + GUIStyle redCrossStyle = new GUIStyle(btnStyle); + redCrossStyle.normal.textColor = new Color(1f, 0.3f, 0.3f); + + bool removedEntry = false; + if (GUILayout.Button("X", redCrossStyle, GUILayout.Width(25), GUILayout.Height(22))) + { + RemoveFromBanList(entry); + removedEntry = true; + } + GUILayout.EndHorizontal(); + if (removedEntry) break; + } + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + + GUILayout.EndHorizontal(); + } + } +} diff --git a/ui/MenuKeybinds.cs b/ui/MenuKeybinds.cs new file mode 100644 index 0000000..a2efdbe --- /dev/null +++ b/ui/MenuKeybinds.cs @@ -0,0 +1,621 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +private static readonly string[] menuTranslationFixKeys = + { + "ANTI CHEAT", "AUTO HOST", "LOBBY CONTROLS", "ROLE MANAGER", "PUNISHMENT SYSTEM", "Mode:", + "RPC PROTECTIONS", "Block Spoof RPC", "Block Sabotage & Meetings", "Block Game RPC in Lobby", + "Auto-Ban Platform Spoof (Host)", "Ban Custom Platforms From TXT", "Block Meeting RPC Flood", + "Block Chat RPC Flood", "OTHER PROTECTIONS", "Disable Vote Kicks (Host)", "Auto-Kick Fortegreen", + "Auto-Ban Broken FriendCode (Host)", "BAN LIST", "Auto-Ban Blacklisted Players", "Enter Friend Code", + "ADD", "Ban list is empty." + }; + +private static readonly Dictionary> menuExtraTranslations = new Dictionary> + { + ["de"] = new Dictionary { ["GAME RULES"]="SPIELREGELN", ["Rainbow All"]="Regenbogen alle", ["All Color"]="Alle Farbe", ["Color:"]="Farbe:", ["Applied lobby color."]="Lobby-Farbe angewendet." }, + ["fr"] = new Dictionary { ["GAME RULES"]="RÈGLES DU JEU", ["Rainbow All"]="Arc-en-ciel tous", ["All Color"]="Couleur tous", ["Color:"]="Couleur :", ["Applied lobby color."]="Couleur du lobby appliquée." }, + ["es"] = new Dictionary { ["GAME RULES"]="REGLAS DEL JUEGO", ["Rainbow All"]="Arcoíris todos", ["All Color"]="Color todos", ["Color:"]="Color:", ["Applied lobby color."]="Color del lobby aplicado." }, + ["it"] = new Dictionary { ["GAME RULES"]="REGOLE DI GIOCO", ["Rainbow All"]="Arcobaleno tutti", ["All Color"]="Colore tutti", ["Color:"]="Colore:", ["Applied lobby color."]="Colore lobby applicato." }, + ["pt"] = new Dictionary { ["GAME RULES"]="REGRAS DO JOGO", ["Rainbow All"]="Arco-íris todos", ["All Color"]="Cor todos", ["Color:"]="Cor:", ["Applied lobby color."]="Cor do lobby aplicada." }, + ["pl"] = new Dictionary { ["GAME RULES"]="ZASADY GRY", ["Rainbow All"]="Tęcza wszyscy", ["All Color"]="Kolor wszystkich", ["Color:"]="Kolor:", ["Applied lobby color."]="Kolor lobby zastosowany." }, + ["nl"] = new Dictionary { ["GAME RULES"]="SPELREGELS", ["Rainbow All"]="Regenboog allen", ["All Color"]="Kleur allen", ["Color:"]="Kleur:", ["Applied lobby color."]="Lobbykleur toegepast." }, + ["tr"] = new Dictionary { ["GAME RULES"]="OYUN KURALLARI", ["Rainbow All"]="Herkese gökkuşağı", ["All Color"]="Herkes renk", ["Color:"]="Renk:", ["Applied lobby color."]="Lobi rengi uygulandı." }, + ["cs"] = new Dictionary { ["GAME RULES"]="PRAVIDLA HRY", ["Rainbow All"]="Duha všem", ["All Color"]="Barva všem", ["Color:"]="Barva:", ["Applied lobby color."]="Barva lobby použita." }, + ["ro"] = new Dictionary { ["GAME RULES"]="REGULI JOC", ["Rainbow All"]="Curcubeu toți", ["All Color"]="Culoare toți", ["Color:"]="Culoare:", ["Applied lobby color."]="Culoarea lobby-ului aplicată." }, + ["hu"] = new Dictionary { ["GAME RULES"]="JÁTÉKSZABÁLYOK", ["Rainbow All"]="Szivárvány mind", ["All Color"]="Mindenki színe", ["Color:"]="Szín:", ["Applied lobby color."]="Lobby színe alkalmazva." }, + ["sv"] = new Dictionary { ["GAME RULES"]="SPELREGLER", ["Rainbow All"]="Regnbåge alla", ["All Color"]="Färg alla", ["Color:"]="Färg:", ["Applied lobby color."]="Lobbyfärg tillämpad." }, + ["da"] = new Dictionary { ["GAME RULES"]="SPILREGLER", ["Rainbow All"]="Regnbue alle", ["All Color"]="Farve alle", ["Color:"]="Farve:", ["Applied lobby color."]="Lobbyfarve anvendt." }, + ["fi"] = new Dictionary { ["GAME RULES"]="PELISÄÄNNÖT", ["Rainbow All"]="Sateenkaari kaikille", ["All Color"]="Väri kaikille", ["Color:"]="Väri:", ["Applied lobby color."]="Lobbyn väri käytetty." }, + ["no"] = new Dictionary { ["GAME RULES"]="SPILLREGLER", ["Rainbow All"]="Regnbue alle", ["All Color"]="Farge alle", ["Color:"]="Farge:", ["Applied lobby color."]="Lobbyfarge brukt." }, + ["uk"] = new Dictionary { ["GAME RULES"]="ПРАВИЛА ГРИ", ["Rainbow All"]="Райдуга всім", ["All Color"]="Колір усім", ["Color:"]="Колір:", ["Applied lobby color."]="Колір лобі застосовано." }, + ["el"] = new Dictionary { ["GAME RULES"]="ΚΑΝΟΝΕΣ ΠΑΙΧΝΙΔΙΟΥ", ["Rainbow All"]="Ουράνιο τόξο όλοι", ["All Color"]="Χρώμα όλοι", ["Color:"]="Χρώμα:", ["Applied lobby color."]="Το χρώμα lobby εφαρμόστηκε." }, + ["zh"] = new Dictionary { ["GAME RULES"]="游戏规则", ["Rainbow All"]="全员彩虹", ["All Color"]="全员颜色", ["Color:"]="颜色:", ["Applied lobby color."]="大厅颜色已应用。" }, + ["ja"] = new Dictionary { ["GAME RULES"]="ゲームルール", ["Rainbow All"]="全員レインボー", ["All Color"]="全員カラー", ["Color:"]="色:", ["Applied lobby color."]="ロビーの色を適用しました。" }, + ["ko"] = new Dictionary { ["GAME RULES"]="게임 규칙", ["Rainbow All"]="모두 무지개", ["All Color"]="모두 색상", ["Color:"]="색상:", ["Applied lobby color."]="로비 색상이 적용되었습니다." } + }; + +private static readonly Dictionary menuTranslationFixes = new Dictionary + { + ["de"] = new[] { "ANTI-CHEAT", "AUTO-HOST", "LOBBY-STEUERUNG", "ROLLENMANAGER", "STRAFSYSTEM", "Modus:", "RPC-SCHUTZ", "Spoof-RPC blockieren", "Sabotage & Meetings blockieren", "Spiel-RPC in Lobby blockieren", "Plattform-Spoof auto-bannen (Host)", "Custom-Plattformen aus TXT bannen", "Meeting-RPC-Flood blockieren", "Chat-RPC-Flood blockieren", "WEITERER SCHUTZ", "Vote-Kicks deaktivieren (Host)", "Fortegreen automatisch kicken", "Defekten FriendCode auto-bannen (Host)", "BAN-LISTE", "Spieler aus Ban-Liste auto-bannen", "Friend Code eingeben", "HINZUFÜGEN", "Ban-Liste ist leer." }, + ["fr"] = new[] { "ANTI-TRICHE", "HÔTE AUTO", "CONTRÔLES LOBBY", "GESTION DES RÔLES", "SYSTÈME DE SANCTIONS", "Mode :", "PROTECTIONS RPC", "Bloquer RPC spoof", "Bloquer sabotages et meetings", "Bloquer RPC de jeu en lobby", "Auto-ban spoof plateforme (Hôte)", "Ban plateformes custom TXT", "Bloquer flood RPC meeting", "Bloquer flood RPC chat", "AUTRES PROTECTIONS", "Désactiver vote-kicks (Hôte)", "Auto-kick Fortegreen", "Auto-ban FriendCode cassé (Hôte)", "LISTE DE BAN", "Auto-ban joueurs listés", "Entrer Friend Code", "AJOUTER", "La liste de ban est vide." }, + ["es"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROLES DE LOBBY", "GESTOR DE ROLES", "SISTEMA DE SANCIONES", "Modo:", "PROTECCIONES RPC", "Bloquear RPC spoof", "Bloquear sabotajes y reuniones", "Bloquear RPC de juego en lobby", "Auto-ban spoof de plataforma (Host)", "Ban de plataformas custom desde TXT", "Bloquear flood RPC de reunión", "Bloquear flood RPC de chat", "OTRAS PROTECCIONES", "Desactivar vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode roto (Host)", "LISTA DE BAN", "Auto-ban jugadores en lista", "Introducir Friend Code", "AÑADIR", "La lista de ban está vacía." }, + ["it"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROLLI LOBBY", "GESTORE RUOLI", "SISTEMA PUNIZIONI", "Modalità:", "PROTEZIONI RPC", "Blocca RPC spoof", "Blocca sabotaggi e meeting", "Blocca RPC di gioco in lobby", "Auto-ban spoof piattaforma (Host)", "Ban piattaforme custom da TXT", "Blocca flood RPC meeting", "Blocca flood RPC chat", "ALTRE PROTEZIONI", "Disattiva vote-kick (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode rotto (Host)", "LISTA BAN", "Auto-ban giocatori in lista", "Inserisci Friend Code", "AGGIUNGI", "La lista ban è vuota." }, + ["pt"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROLES DO LOBBY", "GERENCIADOR DE FUNÇÕES", "SISTEMA DE PUNIÇÕES", "Modo:", "PROTEÇÕES RPC", "Bloquear RPC spoof", "Bloquear sabotagens e reuniões", "Bloquear RPC de jogo no lobby", "Auto-ban spoof de plataforma (Host)", "Ban plataformas custom do TXT", "Bloquear flood RPC de reunião", "Bloquear flood RPC de chat", "OUTRAS PROTEÇÕES", "Desativar vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode quebrado (Host)", "LISTA DE BAN", "Auto-ban jogadores listados", "Inserir Friend Code", "ADICIONAR", "A lista de ban está vazia." }, + ["pl"] = new[] { "ANTI-CHEAT", "AUTO HOST", "KONTROLA LOBBY", "MENEDŻER RÓL", "SYSTEM KAR", "Tryb:", "OCHRONA RPC", "Blokuj spoof RPC", "Blokuj sabotaże i spotkania", "Blokuj RPC gry w lobby", "Auto-ban spoof platformy (Host)", "Ban platform custom z TXT", "Blokuj flood RPC spotkania", "Blokuj flood RPC czatu", "INNA OCHRONA", "Wyłącz vote-kicki (Host)", "Auto-kick Fortegreen", "Auto-ban uszkodzony FriendCode (Host)", "LISTA BANÓW", "Auto-ban graczy z listy", "Wpisz Friend Code", "DODAJ", "Lista banów jest pusta." }, + ["nl"] = new[] { "ANTI-CHEAT", "AUTO-HOST", "LOBBYBEDIENING", "ROLLENBEHEER", "STRAFSYSTEEM", "Modus:", "RPC-BESCHERMING", "Spoof-RPC blokkeren", "Sabotage & meetings blokkeren", "Game-RPC in lobby blokkeren", "Platform-spoof auto-bannen (Host)", "Custom platforms uit TXT bannen", "Meeting-RPC-flood blokkeren", "Chat-RPC-flood blokkeren", "ANDERE BESCHERMING", "Vote-kicks uitschakelen (Host)", "Fortegreen automatisch kicken", "Kapotte FriendCode auto-bannen (Host)", "BANLIJST", "Spelers op banlijst auto-bannen", "Friend Code invoeren", "TOEVOEGEN", "Banlijst is leeg." }, + ["tr"] = new[] { "ANTI-CHEAT", "OTO HOST", "LOBI KONTROLLERİ", "ROL YÖNETİCİSİ", "CEZA SİSTEMİ", "Mod:", "RPC KORUMALARI", "Spoof RPC engelle", "Sabotaj ve toplantıları engelle", "Lobide oyun RPC engelle", "Platform spoof oto-ban (Host)", "TXT özel platform ban", "Toplantı RPC flood engelle", "Sohbet RPC flood engelle", "DİĞER KORUMALAR", "Vote-kick kapat (Host)", "Fortegreen oto-kick", "Bozuk FriendCode oto-ban (Host)", "BAN LİSTESİ", "Listedeki oyuncuları oto-ban", "Friend Code gir", "EKLE", "Ban listesi boş." }, + ["cs"] = new[] { "ANTI-CHEAT", "AUTO HOST", "OVLÁDÁNÍ LOBBY", "SPRÁVCE ROLÍ", "SYSTÉM TRESTŮ", "Režim:", "OCHRANA RPC", "Blokovat spoof RPC", "Blokovat sabotáže a meetingy", "Blokovat herní RPC v lobby", "Auto-ban spoof platformy (Host)", "Ban custom platforem z TXT", "Blokovat meeting RPC flood", "Blokovat chat RPC flood", "DALŠÍ OCHRANA", "Vypnout vote-kicky (Host)", "Auto-kick Fortegreen", "Auto-ban rozbitý FriendCode (Host)", "BAN LIST", "Auto-ban hráčů z listu", "Zadej Friend Code", "PŘIDAT", "Ban list je prázdný." }, + ["ro"] = new[] { "ANTI-CHEAT", "HOST AUTO", "CONTROALE LOBBY", "MANAGER ROLURI", "SISTEM DE PEDEPSE", "Mod:", "PROTECȚII RPC", "Blochează RPC spoof", "Blochează sabotaje și meetinguri", "Blochează RPC de joc în lobby", "Auto-ban spoof platformă (Host)", "Ban platforme custom din TXT", "Blochează flood RPC meeting", "Blochează flood RPC chat", "ALTE PROTECȚII", "Dezactivează vote-kick (Host)", "Auto-kick Fortegreen", "Auto-ban FriendCode stricat (Host)", "LISTĂ BAN", "Auto-ban jucători listați", "Introdu Friend Code", "ADAUGĂ", "Lista de ban este goală." }, + ["hu"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBI VEZÉRLÉS", "SZEREPKEZELŐ", "BÜNTETÉSI RENDSZER", "Mód:", "RPC VÉDELEM", "Spoof RPC blokkolása", "Szabotázsok és meetingek blokkolása", "Játék RPC blokkolása lobbyban", "Platform spoof auto-ban (Host)", "Custom platformok bannolása TXT-ből", "Meeting RPC flood blokkolása", "Chat RPC flood blokkolása", "EGYÉB VÉDELEM", "Vote-kick tiltása (Host)", "Fortegreen auto-kick", "Hibás FriendCode auto-ban (Host)", "BAN LISTA", "Listás játékosok auto-banja", "Friend Code megadása", "HOZZÁAD", "A ban lista üres." }, + ["sv"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYKONTROLLER", "ROLLHANTERARE", "STRAFFSYSTEM", "Läge:", "RPC-SKYDD", "Blockera spoof-RPC", "Blockera sabotage och möten", "Blockera spel-RPC i lobby", "Auto-ban plattformsspoof (Host)", "Ban custom-plattformar från TXT", "Blockera meeting RPC-flood", "Blockera chat RPC-flood", "ANNAT SKYDD", "Inaktivera vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban trasig FriendCode (Host)", "BANLISTA", "Auto-ban spelare på lista", "Ange Friend Code", "LÄGG TILL", "Banlistan är tom." }, + ["da"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYKONTROL", "ROLLEMANAGER", "STRAFSYSTEM", "Tilstand:", "RPC-BESKYTTELSE", "Bloker spoof-RPC", "Bloker sabotager og møder", "Bloker spil-RPC i lobby", "Auto-ban platform spoof (Host)", "Ban custom-platforme fra TXT", "Bloker meeting RPC-flood", "Bloker chat RPC-flood", "ANDEN BESKYTTELSE", "Deaktiver vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban defekt FriendCode (Host)", "BANLISTE", "Auto-ban spillere på liste", "Indtast Friend Code", "TILFØJ", "Banlisten er tom." }, + ["fi"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYN HALLINTA", "ROOLIEN HALLINTA", "RANGAISTUSJÄRJESTELMÄ", "Tila:", "RPC-SUOJAUKSET", "Estä spoof RPC", "Estä sabotaasit ja kokoukset", "Estä peli-RPC lobbyssa", "Auto-ban platform spoof (Host)", "Ban custom-alustat TXT:stä", "Estä meeting RPC flood", "Estä chat RPC flood", "MUUT SUOJAUKSET", "Poista vote-kickit käytöstä (Host)", "Auto-kick Fortegreen", "Auto-ban rikkinäinen FriendCode (Host)", "BAN-LISTA", "Auto-ban listatut pelaajat", "Syötä Friend Code", "LISÄÄ", "Ban-lista on tyhjä." }, + ["no"] = new[] { "ANTI-CHEAT", "AUTO HOST", "LOBBYKONTROLLER", "ROLLEBEHANDLER", "STRAFFESYSTEM", "Modus:", "RPC-BESKYTTELSE", "Blokker spoof-RPC", "Blokker sabotasje og møter", "Blokker spill-RPC i lobby", "Auto-ban platform spoof (Host)", "Ban custom-plattformer fra TXT", "Blokker meeting RPC-flood", "Blokker chat RPC-flood", "ANNEN BESKYTTELSE", "Deaktiver vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban ødelagt FriendCode (Host)", "BANLISTE", "Auto-ban spillere på liste", "Skriv Friend Code", "LEGG TIL", "Banlisten er tom." }, + ["uk"] = new[] { "АНТИЧИТ", "АВТО ХОСТ", "КЕРУВАННЯ ЛОБІ", "МЕНЕДЖЕР РОЛЕЙ", "СИСТЕМА ПОКАРАНЬ", "Режим:", "ЗАХИСТ RPC", "Блокувати spoof RPC", "Блокувати саботажі та зустрічі", "Блокувати ігрові RPC у лобі", "Авто-бан spoof платформи (Хост)", "Бан кастомних платформ з TXT", "Блокувати flood RPC зустрічі", "Блокувати flood RPC чату", "ІНШИЙ ЗАХИСТ", "Вимкнути vote-kick (Хост)", "Авто-кік Fortegreen", "Авто-бан зламаного FriendCode (Хост)", "БАН-ЛИСТ", "Авто-бан гравців зі списку", "Введіть Friend Code", "ДОДАТИ", "Бан-лист порожній." }, + ["el"] = new[] { "ANTI-CHEAT", "AUTO HOST", "ΕΛΕΓΧΟΙ LOBBY", "ΔΙΑΧΕΙΡΙΣΗ ΡΟΛΩΝ", "ΣΥΣΤΗΜΑ ΠΟΙΝΩΝ", "Λειτουργία:", "ΠΡΟΣΤΑΣΙΕΣ RPC", "Μπλοκ spoof RPC", "Μπλοκ σαμποτάζ και meetings", "Μπλοκ game RPC στο lobby", "Auto-ban platform spoof (Host)", "Ban custom platforms από TXT", "Μπλοκ meeting RPC flood", "Μπλοκ chat RPC flood", "ΑΛΛΕΣ ΠΡΟΣΤΑΣΙΕΣ", "Απενεργοποίηση vote-kicks (Host)", "Auto-kick Fortegreen", "Auto-ban χαλασμένο FriendCode (Host)", "ΛΙΣΤΑ BAN", "Auto-ban παικτών στη λίστα", "Εισαγωγή Friend Code", "ΠΡΟΣΘΗΚΗ", "Η λίστα ban είναι άδεια." }, + ["zh"] = new[] { "反作弊", "自动房主", "大厅控制", "身份管理", "处罚系统", "模式:", "RPC 防护", "阻止 Spoof RPC", "阻止破坏和会议", "阻止大厅内游戏 RPC", "自动封禁平台伪装 (房主)", "从 TXT 封禁自定义平台", "阻止会议 RPC 洪泛", "阻止聊天 RPC 洪泛", "其他防护", "禁用投票踢人 (房主)", "自动踢出 Fortegreen", "自动封禁损坏 FriendCode (房主)", "封禁列表", "自动封禁列表玩家", "输入 Friend Code", "添加", "封禁列表为空。" }, + ["ja"] = new[] { "アンチチート", "自動ホスト", "ロビー制御", "ロール管理", "処罰システム", "モード:", "RPC保護", "Spoof RPCをブロック", "サボタージュと会議をブロック", "ロビー中のゲームRPCをブロック", "プラットフォーム偽装を自動BAN (ホスト)", "TXTのカスタムプラットフォームをBAN", "会議RPCフラッドをブロック", "チャットRPCフラッドをブロック", "その他の保護", "投票キックを無効化 (ホスト)", "Fortegreenを自動キック", "壊れたFriendCodeを自動BAN (ホスト)", "BANリスト", "BANリストのプレイヤーを自動BAN", "Friend Codeを入力", "追加", "BANリストは空です。" }, + ["ko"] = new[] { "안티치트", "자동 호스트", "로비 컨트롤", "역할 관리자", "처벌 시스템", "모드:", "RPC 보호", "Spoof RPC 차단", "사보타주와 회의 차단", "로비에서 게임 RPC 차단", "플랫폼 위장 자동 밴 (호스트)", "TXT 커스텀 플랫폼 밴", "회의 RPC 플러드 차단", "채팅 RPC 플러드 차단", "기타 보호", "투표 킥 비활성화 (호스트)", "Fortegreen 자동 킥", "손상된 FriendCode 자동 밴 (호스트)", "밴 목록", "목록의 플레이어 자동 밴", "Friend Code 입력", "추가", "밴 목록이 비어 있습니다." } + }; + +public static float resetingDataLimit; + +public static byte selectedMorphTargetId = 255; + +public static bool unlockCosmetics = true; + +public static bool moreLobbyInfo = true; + +public static Dictionary keyBinds = new Dictionary(); + +public static string bindingAction = ""; + +public static string L(string eng, string rus) + { + try + { + string configuredLanguage = CurrentMenuLanguageCode(); + if (configuredLanguage == "ru" || configuredLanguage == "uk") + return TryTranslateMenuText(configuredLanguage, eng, configuredLanguage == "ru" ? rus : null); + if (configuredLanguage != "auto") + return TryTranslateMenuText(configuredLanguage, eng, eng); + + string autoLanguage = ResolveAutoMenuLanguageCode(); + if (autoLanguage != "en") + return TryTranslateMenuText(autoLanguage, eng, autoLanguage == "ru" ? rus : null); + } + catch { } + return eng; + } + +private static string ResolveAutoMenuLanguageCode() + { + try + { + if (DestroyableSingleton.InstanceExists) + { + string currentLang = DestroyableSingleton.Instance.currentLanguage.ToString().ToLowerInvariant(); + if (currentLang.Contains("russian") || currentLang.Contains("рус") || currentLang == "ru") return "ru"; + if (currentLang.Contains("ukrainian") || currentLang.Contains("укр") || currentLang == "uk") return "uk"; + if (currentLang.Contains("german") || currentLang.Contains("deutsch") || currentLang == "de") return "de"; + if (currentLang.Contains("french") || currentLang.Contains("fran") || currentLang == "fr") return "fr"; + if (currentLang.Contains("spanish") || currentLang.Contains("espa") || currentLang == "es") return "es"; + if (currentLang.Contains("italian") || currentLang == "it") return "it"; + if (currentLang.Contains("portugu") || currentLang == "pt") return "pt"; + if (currentLang.Contains("polish") || currentLang == "pl") return "pl"; + if (currentLang.Contains("dutch") || currentLang.Contains("neder") || currentLang == "nl") return "nl"; + if (currentLang.Contains("turkish") || currentLang == "tr") return "tr"; + if (currentLang.Contains("czech") || currentLang == "cs") return "cs"; + if (currentLang.Contains("romanian") || currentLang == "ro") return "ro"; + if (currentLang.Contains("hungarian") || currentLang == "hu") return "hu"; + if (currentLang.Contains("swedish") || currentLang == "sv") return "sv"; + if (currentLang.Contains("danish") || currentLang == "da") return "da"; + if (currentLang.Contains("finnish") || currentLang == "fi") return "fi"; + if (currentLang.Contains("norwegian") || currentLang == "no") return "no"; + if (currentLang.Contains("greek") || currentLang == "el") return "el"; + if (currentLang.Contains("chinese") || currentLang == "zh") return "zh"; + if (currentLang.Contains("japanese") || currentLang == "ja") return "ja"; + if (currentLang.Contains("korean") || currentLang == "ko") return "ko"; + } + } + catch { } + + try + { + string gray = Palette.GetColorName(15); + if (!string.IsNullOrEmpty(gray) && gray.Any(c => c >= '\u0400' && c <= '\u04FF')) return "ru"; + } + catch { } + + return "en"; + } + +private static string TryTranslateMenuText(string languageCode, string englishText, string fallback) + { + try + { + if (string.IsNullOrWhiteSpace(languageCode) || string.IsNullOrEmpty(englishText)) + return fallback ?? englishText; + + if (languageCode == "en") + return englishText; + + if (menuExtraTranslations.TryGetValue(languageCode, out Dictionary extraTranslations) && + extraTranslations.TryGetValue(englishText, out string extraTranslated) && + !string.IsNullOrWhiteSpace(extraTranslated)) + return extraTranslated; + + if (menuTranslationFixes.TryGetValue(languageCode, out string[] fixedTranslations)) + { + int fixedIndex = Array.IndexOf(menuTranslationFixKeys, englishText); + if (fixedIndex >= 0 && fixedIndex < fixedTranslations.Length && !string.IsNullOrWhiteSpace(fixedTranslations[fixedIndex])) + return fixedTranslations[fixedIndex]; + } + + if (menuTranslations.TryGetValue(languageCode, out Dictionary translations) && + translations.TryGetValue(englishText, out string translated) && + !string.IsNullOrWhiteSpace(translated)) + return translated; + } + catch { } + + return fallback ?? englishText; + } + +public static string CurrentMenuLanguageCode() + { + try + { + int index = Mathf.Clamp(currentMenuLanguageIndex, 0, menuLanguageCodes.Length - 1); + return menuLanguageCodes[index]; + } + catch { } + + return "auto"; + } + +private int currentGeneralSubTab = 0; + +private int currentGeneralInfoSubTab = 0; + +private string[] generalSubTabs => new string[] { L("INFORMATION", "ИНФОРМАЦИЯ"), L("KEYBINDS", "БИНДЫ") } + +; + +private string[] generalInfoSubTabs => new string[] { L("WELCOME", "WELCOME"), L("CREDITS", "АВТОРЫ") } + +; + +public static KeyCode menuToggleKey = KeyCode.Insert; + +public static KeyCode bindMassMorph = KeyCode.None; + +public static KeyCode bindSpawnLobby = KeyCode.None; + +public static KeyCode bindDespawnLobby = KeyCode.None; + +public static KeyCode bindCloseMeeting = KeyCode.None; + +public static KeyCode bindInstaStart = KeyCode.None; + +public static KeyCode bindEndCrew = KeyCode.None; + +public static KeyCode bindEndImp = KeyCode.None; + +public static KeyCode bindEndImpDC = KeyCode.None; + +public static KeyCode bindEndHnsDC = KeyCode.None; + +public static KeyCode bindToggleTracers = KeyCode.None; + +public static KeyCode bindToggleNoClip = KeyCode.None; + +public static KeyCode bindToggleFreecam = KeyCode.None; + +public static KeyCode bindToggleCameraZoom = KeyCode.None; + +public static KeyCode bindKillAll = KeyCode.None; + +public static KeyCode bindCallMeeting = KeyCode.None; + +public static KeyCode bindTogglePlayerInfo = KeyCode.None; + +public static KeyCode bindToggleSeeRoles = KeyCode.None; + +public static KeyCode bindToggleSeeGhosts = KeyCode.None; + +public static KeyCode bindToggleFullBright = KeyCode.None; + +public static KeyCode bindKickAll = KeyCode.None; + +public static KeyCode bindFixSabotages = KeyCode.None; + +public static KeyCode bindSetAllGhost = KeyCode.None; + +public static KeyCode bindSetAllGhostImp = KeyCode.None; + +public static KeyCode bindReviveAll = KeyCode.None; + +public static readonly HashSet VanillaRpcIds = new HashSet + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 29, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 60, 61, 62, 63, 64, 65 + }; + +private bool isScannerActiveFlag = false; + +private bool isCamsActiveFlag = false; + +public static bool isWaitingForBind = false; + +public static bool isWaitBindMassMorph = false; + +public static bool isWaitBindSpawnLobby = false; + +public static bool isWaitBindDespawnLobby = false; + +public static bool isWaitBindCloseMeeting = false; + +public static bool isWaitBindInstaStart = false; + +public static bool isWaitBindEndCrew = false; + +public static bool isWaitBindEndImp = false; + +public static bool isWaitBindEndImpDC = false; + +public static bool isWaitBindEndHnsDC = false; + +public static bool isWaitBindToggleTracers = false; + +public static bool isWaitBindToggleNoClip = false; + +public static bool isWaitBindToggleFreecam = false; + +public static bool isWaitBindToggleCameraZoom = false; + +public static bool isWaitBindKillAll = false; + +public static bool isWaitBindCallMeeting = false; + +public static bool isWaitBindTogglePlayerInfo = false; + +public static bool isWaitBindToggleSeeRoles = false; + +public static bool isWaitBindToggleSeeGhosts = false; + +public static bool isWaitBindToggleFullBright = false; + +public static bool isWaitBindKickAll = false; + +public static bool isWaitBindFixSabotages = false; + +public static bool isWaitBindSetAllGhost = false; + +public static bool isWaitBindSetAllGhostImp = false; + +public static bool isWaitBindReviveAll = false; + +public static bool SpoofMenuEnabled = false; + +public static int selectedSpoofMenuIndex = 0; + +private float uiSpoofTimer = 0f; + +public static bool noClip = false; + +public static bool tpToCursor = false; + +public static bool dragToCursor = false; + +public static float walkSpeed = 1f; + +public static bool DetailedJoinInfo = true; + +private static List lastPlayerIds = new List(); + +private static Dictionary pendingJoinTimers = new Dictionary(); + +private static Dictionary playerHistoryKeysById = new Dictionary(); + +public class PlayerHistoryEntry + { + public string Name; + public string FriendCode; + public string Puid; + public string Platform; + public string CustomPlatform; + public int Level; + public DateTime FirstSeenUtc; + public DateTime LastSeenUtc; + public DateTime? LeftUtc; + public bool IsOnline; + public List RpcCalls = new List(); + } + +private static List playerHistoryEntries = new List(); + +private Vector2 playersHistoryScroll = Vector2.zero; + +private int currentPlayersSubTab = 0; + +private string[] playersSubTabs = { "ACTIONS", "HISTORY" }; + +private int currentRoleBuffSubTab = 0; + +public static float engineSpeed = 1f; + +public static bool invertControls = false; + +public static bool autoFollowCursor = false; + +public static int fakeRoleIdx = 0; + +public static RoleTypes[] forceRoleOptions = { RoleTypes.Crewmate, RoleTypes.Impostor, RoleTypes.Engineer, RoleTypes.Scientist, RoleTypes.Shapeshifter, RoleTypes.GuardianAngel }; + +public static RoleTypes[] roleAssignOptions = { + RoleTypes.Crewmate, RoleTypes.Impostor, RoleTypes.Engineer, RoleTypes.Scientist, RoleTypes.Shapeshifter, RoleTypes.GuardianAngel, + (RoleTypes)8, (RoleTypes)9, (RoleTypes)10, (RoleTypes)12, (RoleTypes)18, RoleTypes.Crewmate, RoleTypes.Impostor + }; + +public static string[] roleAssignNames = { + "Crewmate", "Impostor", "Engineer", "Scientist", "Shapeshifter", "Guardian Angel", + "Noisemaker", "Phantom", "Tracker", "Detective", "Viper", "Ghost", "Ghost Imp" + }; + +private int targetRoleAssignIdx = 0; + +private int allPlayersRoleAssignIdx = 0; + +public static bool NoShapeshiftAnim = false; + +public static bool EndlessTracking = false; + +public static bool NoTrackingCooldown = false; + +public static bool UnlimitedInterrogateRange = false; + +public static bool allowTasksAsImpostor = false; + +public static bool killWhileVanishedHostOnly = false; + +public static bool roleBuffImmortality = false; + +private const int ImmortalityCustomVentId = 50; + +private static bool immortalityVentStateApplied = false; + +public static bool noTaskMode = false; + +public static bool killAuraHostOnly = false; + +public static bool noKillCooldownHostOnly = false; + +public static bool spamReportBodies = false; + +private float killAuraTimer = 0f; + +public static bool enableColorCommand = false; + +public static bool hostChatColor = false; + +public static Color hostChatColorValue = new Color32(0, 128, 128, 255); + +public static bool showMenu = false; + +public static Rect windowRect = new Rect(100, 100, 750, 480); + +public static bool freecam = false; + +private static bool _freecamActive = false; + +public static bool cameraZoom = false; + +public static bool RevealVotesEnabled = false; + +private static bool hudZoomBaseCaptured = false; + +private static Vector3 hudZoomBaseDistance = Vector3.zero; + +private static bool zoomResolutionRefreshNeeded = false; + +public static Color currentAccentColor = new Color(1f, 0.549f, 0f, 1f); + +public static bool rgbMenuMode = false; + +public static bool rgbMenuText = false; + +public static bool boldMenuText = true; + +private static ElysiumModMenuGUI activeGui; + +private float rgbMenuHue = 0f; + +public static bool enableBackground = false; + +public static bool hardMenu = false; + +public static Texture2D customMenuBg = null; + +private bool wasShowMenu = false; + +private int currentMenuColorIndex = 10; + +private string[] menuColorNames = { + "Elysium Blue", "Dark Forest", "Green", "Sea Green", "Mint", "Chartreuse", + "Sun Yellow", "Marigold", "Old Gold", + "Bright Amber", "Vivid Orange", "Dark Orange", + "Blood Red", + "Hot Pink", "Pale Mauve", "Lilac", + "Lavender", "Deep Indigo", "Indigo", + "Med Slate Blue", "Slate Blue", "Navy", "Slate Grey", + "Arctic Cyan", "Neon Lime", "Royal Violet", "Crimson Glow", "Ocean Teal", + "Sunset Orange", "Rose Quartz", "Electric Blue", "Gold Ember", "Emerald Pulse", + "Midnight Steel", "Soft Lavender" + }; + +private Color[] menuColors = { + new Color32(51, 51, 255, 255), new Color(0.192f, 0.290f, 0.196f, 1f), new Color(0f, 0.502f, 0f, 1f), new Color(0.235f, 0.702f, 0.443f, 1f), new Color(0.243f, 0.706f, 0.537f, 1f), new Color(0.498f, 1f, 0f, 1f), + new Color(0.996f, 0.718f, 0.082f, 1f), new Color(0.812f, 0.651f, 0.004f, 1f), + new Color(0.996f, 0.612f, 0.063f, 1f), new Color(0.957f, 0.455f, 0.004f, 1f), new Color(1f, 0.549f, 0f, 1f), + new Color(0.871f, 0.071f, 0.149f, 1f), + new Color(0.992f, 0.529f, 0.859f, 1f), new Color(0.882f, 0.678f, 0.800f, 1f), new Color(0.784f, 0.635f, 0.784f, 1f), + new Color(0.925f, 0.686f, 0.996f, 1f), new Color(0.314f, 0.267f, 0.675f, 1f), new Color(0.294f, 0f, 0.51f, 1f), + new Color(0.482f, 0.408f, 0.933f, 1f), new Color(0.416f, 0.353f, 0.804f, 1f), new Color(0f, 0f, 0.502f, 1f), new Color(0.439f, 0.502f, 0.565f, 1f), + new Color32(72, 219, 251, 255), new Color32(163, 230, 53, 255), new Color32(124, 58, 237, 255), new Color32(239, 68, 68, 255), + new Color32(20, 184, 166, 255), new Color32(249, 115, 22, 255), new Color32(244, 114, 182, 255), new Color32(59, 130, 246, 255), + new Color32(245, 158, 11, 255), new Color32(16, 185, 129, 255), new Color32(51, 65, 85, 255), new Color32(196, 181, 253, 255) + }; + +public static float autoChatEveryoneDelay = 2.5f; + +public static string customChatMessage = "test"; + +public static bool customChatSpamEnabled = false; + +public static float customChatSpamDelay = 2.1f; + +public static bool customChatInputFocused = false; + +private float customChatSpamTimer = 0f; + +public static float autoMeetingTimer = 0f; + +private string[] tabNames => new string[] { L("GENERAL", "ОБЩИЕ"), L("SELF", "ИГРОК"), L("VISUALS", "ВИЗУАЛ"), L("PLAYERS", "ИГРОКИ"), L("SABOTAGES", "САБОТАЖИ"), L("HOST ONLY", "ХОСТ"), L("VOTEKICK", "КИК"), L("MENU", "МЕНЮ"), L("ANIMATIONS", "АНИМАЦИИ") } + +; + +public static float speedMultiplier = 1f; + +public static bool noSettingLimit = false; + +public static float globalRoomColorId = 0f; + +private int currentHostOnlySubTab = 0; + +private string[] hostOnlySubTabs => new string[] { L("LOBBY CONTROLS", "КОНТРОЛЬ ЛОББИ"), L("ROLE MANAGER", "МЕНЕДЖЕР РОЛЕЙ"), L("ANTI CHEAT", "АНТИ-ЧИТ"), L("AUTO HOST", "АВТО ХОСТ"), L("MAPS", "КАРТЫ") } + +; + +public static bool UseSnapToRPC = true; + +private static bool isSkeldFlipped = false; + +public static float selectedMapSpawnIdx = 0f; + +public static string[] mapSpawnNames = { "The Skeld", "Mira HQ", "Polus", "The Airship", "The Fungle" }; + +public static bool FlippedSkeld + { + get { return isSkeldFlipped; } + set + { + if (AmongUsClient.Instance == null || isSkeldFlipped == value) return; + var temp = AmongUsClient.Instance.ShipPrefabs[3]; + AmongUsClient.Instance.ShipPrefabs[3] = AmongUsClient.Instance.ShipPrefabs[0]; + AmongUsClient.Instance.ShipPrefabs[0] = temp; + isSkeldFlipped = value; + } + } + +[HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.Start))] + public static class AllowSymbols_TextBoxTMP_Start_Patch + { + public static void Postfix(TextBoxTMP __instance) + { + __instance.allowAllCharacters = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.AllowSymbols = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.AllowEmail = ElysiumModMenuGUI.allowLinksAndSymbols; + } + } + +[HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] + public static class ChatJailbreak_ChatController_Update_Postfix + { + public static void Postfix(ChatController __instance) + { + if (__instance == null || __instance.freeChatField == null || __instance.freeChatField.textArea == null) return; + + if (ElysiumModMenuGUI.enableFastChat && __instance.timeSinceLastMessage < 0.9f) + { + __instance.timeSinceLastMessage = 0.9f; + } + + __instance.freeChatField.textArea.allowAllCharacters = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.freeChatField.textArea.AllowSymbols = ElysiumModMenuGUI.allowLinksAndSymbols; + __instance.freeChatField.textArea.AllowEmail = ElysiumModMenuGUI.allowLinksAndSymbols; + + __instance.freeChatField.textArea.characterLimit = ElysiumModMenuGUI.enableExtendedChat ? 120 : 100; + } + } + +[HarmonyPatch(typeof(ChatController), nameof(ChatController.SendFreeChat))] + public static class AllowURLS_ChatController_SendFreeChat_Patch + { + public static bool Prefix(ChatController __instance) + { + if (!ElysiumModMenuGUI.allowLinksAndSymbols) return true; + + string text = __instance.freeChatField.Text; + + if (!string.IsNullOrWhiteSpace(text)) + { + PlayerControl.LocalPlayer.RpcSendChat(text); + __instance.freeChatField.textArea.SetText(string.Empty, string.Empty); + } + + return false; + } + } + +public static bool autoKickBugs = false; + +public static float autoKickTimer = 5f; + +public static Dictionary fortegreenTimer = new Dictionary(); +} +} diff --git a/ui/MenuLocalization.cs b/ui/MenuLocalization.cs new file mode 100644 index 0000000..bb28688 --- /dev/null +++ b/ui/MenuLocalization.cs @@ -0,0 +1,58 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +public static string[] spoofMenuNames = { "ElysiumModMenu", "HostGuard/TOH", "Polar", "BanMod", "Better Among Us", "Sicko Menu", "GNC", "KillNetwork (V1)", "KillNetwork (V2)", "KNM" }; + +public static byte[] spoofMenuRPCs = { 89, 176, 204, 212, 151, 164, 154, 85, 150, 162 }; + +public static float rpcSpoofDelay = 4f; + +public static readonly string[] menuLanguageNames = { "Auto", "English", "Русский", "Deutsch", "Français", "Español", "Italiano", "Português", "Polski", "Nederlands", "Türkçe", "Čeština", "Română", "Magyar", "Svenska", "Dansk", "Suomi", "Norsk", "Українська", "Ελληνικά", "中文", "日本語", "한국어" }; + +public static readonly string[] menuLanguageCodes = { "auto", "en", "ru", "de", "fr", "es", "it", "pt", "pl", "nl", "tr", "cs", "ro", "hu", "sv", "da", "fi", "no", "uk", "el", "zh", "ja", "ko" }; + +public static int currentMenuLanguageIndex = 0; +} +} diff --git a/ui/MenuMaps.cs b/ui/MenuMaps.cs new file mode 100644 index 0000000..9cad931 --- /dev/null +++ b/ui/MenuMaps.cs @@ -0,0 +1,345 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetColor))] + public static class AutoKickBugs_Patch + { + public static void Postfix(PlayerControl __instance, byte bodyColor) + { + if (!ElysiumModMenuGUI.autoKickBugs || AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost) return; + + try + { + if (__instance != null && __instance != PlayerControl.LocalPlayer && __instance.Data != null && !__instance.Data.Disconnected) + { + byte pid = __instance.PlayerId; + string colorName = Palette.GetColorName((int)bodyColor); + + if (bodyColor == 18 || colorName == "???" || bodyColor >= Palette.PlayerColors.Length) + { + if (!ElysiumModMenuGUI.fortegreenTimer.ContainsKey(pid)) + { + ElysiumModMenuGUI.fortegreenTimer[pid] = Time.time + ElysiumModMenuGUI.autoKickTimer; + } + } + else + { + if (ElysiumModMenuGUI.fortegreenTimer.ContainsKey(pid)) + { + ElysiumModMenuGUI.fortegreenTimer.Remove(pid); + } + } + } + } + catch { } + } + } + +[HarmonyPatch(typeof(VoteBanSystem), nameof(VoteBanSystem.HandleRpc))] + public static class VoteBanSystemPatch + { + public static bool Prefix(VoteBanSystem __instance, byte callId, Hazel.MessageReader reader) + { + if (callId != 26) + return true; + + bool shouldBlock = AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost && ElysiumModMenuGUI.disableVoteKicks; + try + { + Hazel.MessageReader copy = Hazel.MessageReader.Get(reader); + int targetClientId = copy.ReadInt32(); + int voterClientId = copy.ReadInt32(); + string targetName = ResolveVoteClientName(targetClientId); + string voterName = ResolveVoteClientName(voterClientId); + + ShowVoteKickChatInfo(voterName, targetName); + if (shouldBlock) + ElysiumModMenuGUI.ShowNotification($"[VOTEKICK BLOCK] {voterName} tried to vote-kick {targetName}"); + } + catch + { + if (shouldBlock) + ElysiumModMenuGUI.ShowNotification("[VOTEKICK BLOCK] Vote-kick blocked, sender could not be resolved."); + } + + return !shouldBlock; + } + + private static string ResolveVoteClientName(int clientId) + { + try + { + if (PlayerControl.AllPlayerControls != null) + { + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc == null || pc.Data == null) continue; + if (pc.Data.ClientId == clientId || (int)pc.OwnerId == clientId) + { + string name = string.IsNullOrWhiteSpace(pc.Data.PlayerName) ? "Unknown" : pc.Data.PlayerName; + return $"{name} ({clientId})"; + } + } + } + } + catch { } + + return $"client {clientId}"; + } + + private static void ShowVoteKickChatInfo(string voterName, string targetName) + { + string message = $"[VOTEKICK] {CleanVoteName(voterName)} vote-kicked {CleanVoteName(targetName)}"; + try + { + if (HudManager.Instance != null && HudManager.Instance.Chat != null && PlayerControl.LocalPlayer != null) + { + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, message); + return; + } + } + catch { } + + ElysiumModMenuGUI.ShowNotification(message); + } + + private static string CleanVoteName(string value) + { + if (string.IsNullOrWhiteSpace(value)) return "Unknown"; + value = Regex.Replace(value, "<[^>]*>", string.Empty); + return value.Replace("<", string.Empty).Replace(">", string.Empty).Trim(); + } + } + +public static bool disableVoteKicks = false; + +[HarmonyPatch(typeof(ShhhBehaviour), nameof(ShhhBehaviour.PlayAnimation))] + public static class SkipShhh_Perfect_Patch + { + public static bool Prefix(ShhhBehaviour __instance, ref Il2CppSystem.Collections.IEnumerator __result) + { + if (!ElysiumModMenuGUI.skipShhhAnim || __instance == null) return true; + + __instance.gameObject.SetActive(false); + + __result = FastSkip().WrapToIl2Cpp(); + return false; + } + + private static System.Collections.IEnumerator FastSkip() { yield break; } + } + +private void SpawnMap(int mapId) + { + try + { + if ((UnityEngine.Object)(object)AmongUsClient.Instance == (UnityEngine.Object)null || AmongUsClient.Instance.ShipPrefabs == null) + return; + + int realMapId = mapId; + if (mapId == 3) realMapId = 4; + if (mapId == 4) realMapId = 5; + + if (realMapId >= AmongUsClient.Instance.ShipPrefabs.Count) + return; + + BepInEx.Unity.IL2CPP.Utils.MonoBehaviourExtensions.StartCoroutine(this, CoSpawnMap(realMapId)); + } + catch { } + } + +[HideFromIl2Cpp] + private System.Collections.IEnumerator CoSpawnMap(int mapId) + { + AmongUsClient.Instance.ShipLoadingAsyncHandle = AmongUsClient.Instance.ShipPrefabs[mapId].InstantiateAsync((Transform)null, false); + yield return AmongUsClient.Instance.ShipLoadingAsyncHandle; + + ShipStatus.Instance = AmongUsClient.Instance.ShipLoadingAsyncHandle.Result.GetComponent(); + ((InnerNetClient)AmongUsClient.Instance).Spawn(((Component)ShipStatus.Instance).GetComponent(), -2, (SpawnFlags)0); + + } + +private void DespawnMap() + { + try + { + if (ShipStatus.Instance != null) + { + ShipStatus.Instance.Despawn(); + } + } + catch { } + } + +private void DespawnCurrentMap() + { + DespawnMap(); + } + +[HideFromIl2Cpp] + private System.Collections.IEnumerator CoSpawnOverlappedMap(int mapId) + { + yield return CoSpawnMap(mapId); + } + +public static Dictionary skeldTeleportLocations = new Dictionary() +{ + { "Cafeteria", new Vector2(-0.78f, 2.48f) }, + { "Weapons", new Vector2(8.04f, 1.24f) }, + { "Navigation", new Vector2(16.59f, -2.33f) }, + { "O2", new Vector2(5.15f, -3.12f) }, + { "Shields", new Vector2(10.15f, -7.64f) }, + { "Communications", new Vector2(3.87f, -11.08f) }, + { "Storage", new Vector2(-1.92f, -6.14f) }, + { "Admin", new Vector2(5.31f, -7.42f) }, + { "Electrical", new Vector2(-3.37f, -4.84f) }, + { "Security", new Vector2(-5.69f, -3.07f) }, + { "Medbay", new Vector2(-8.61f, -4.30f) }, + { "Reactor", new Vector2(-20.19f, -2.48f) }, + { "Upper Engine", new Vector2(-16.84f, 2.47f) }, + { "Lower Engine", new Vector2(-16.48f, -7.53f) } +}; + +public static Dictionary miraTeleportLocations = new Dictionary() +{ + { "Launchpad", new Vector2(0.12f, -1.5f) }, + { "Medbay", new Vector2(10.2f, 15.1f) }, + { "Locker Room", new Vector2(12.5f, 18.5f) }, + { "Decontamination", new Vector2(14.8f, 22.0f) }, + { "Reactor", new Vector2(20.5f, 25.0f) }, + { "Laboratory", new Vector2(26.2f, 22.1f) }, + { "Office", new Vector2(24.5f, 15.2f) }, + { "Greenhouse", new Vector2(22.1f, 8.5f) }, + { "Admin", new Vector2(18.2f, 3.1f) }, + { "Cafeteria", new Vector2(14.5f, -2.1f) }, + { "Storage", new Vector2(9.8f, -6.5f) } +}; + +public static Dictionary polusTeleportLocations = new Dictionary() +{ + { "Dropship", new Vector2(0f, 0f) }, + { "Electrical", new Vector2(5.2f, 12.1f) }, + { "O2", new Vector2(-12.4f, 8.5f) }, + { "Security", new Vector2(-18.5f, 2.2f) }, + { "Decontamination", new Vector2(-25.2f, 1.5f) }, + { "Specimen Room", new Vector2(-30.1f, -5.2f) }, + { "Laboratory", new Vector2(-20.5f, -12.1f) }, + { "Medbay", new Vector2(-8.2f, -15.4f) }, + { "Communications", new Vector2(8.5f, -12.1f) }, + { "Weapons", new Vector2(15.2f, -2.5f) } +}; + +public static Dictionary airshipTeleportLocations = new Dictionary() +{ + { "Cockpit", new Vector2(-30f, 15f) }, + { "Vault", new Vector2(-15f, 15f) }, + { "Brig", new Vector2(-5f, 10f) }, + { "Meeting Room", new Vector2(10f, 12f) }, + { "Records", new Vector2(25f, 12f) }, + { "Lounge", new Vector2(35f, 8f) }, + { "Kitchen", new Vector2(25f, -5f) } +}; + +public static Dictionary fungleTeleportLocations = new Dictionary() +{ + { "Beach", new Vector2(0f, -20f) }, + { "Jungle", new Vector2(15f, 10f) }, + { "Lookout", new Vector2(-10f, 25f) }, + { "Laboratory", new Vector2(-25f, 0f) }, + { "Storage", new Vector2(5f, -5f) } +}; + +public static int GetCurrentMapId() + { + if (AmongUsClient.Instance == null) return 0; + if (AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay) + { + return AmongUsClient.Instance.TutorialMapId; + } + else + { + if (GameOptionsManager.Instance == null || GameOptionsManager.Instance.CurrentGameOptions == null) return 0; + return GameOptionsManager.Instance.CurrentGameOptions.MapId; + } + } + +private Vector2 mapsScrollPos = Vector2.zero; + +public static Dictionary GetTeleportLocations() + { + switch (GetCurrentMapId()) + { + case 0: return skeldTeleportLocations; + case 1: return miraTeleportLocations; + case 2: return polusTeleportLocations; + case 3: return skeldTeleportLocations; + case 4: return airshipTeleportLocations; + case 5: return fungleTeleportLocations; + default: return skeldTeleportLocations; + } + } + +public static void TeleportTo(Vector2 position) + { + if (PlayerControl.LocalPlayer == null || PlayerControl.LocalPlayer.NetTransform == null) return; + if (UseSnapToRPC) + { + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(position); + } + else + { + PlayerControl.LocalPlayer.NetTransform.SnapTo(position); + } + } + +private int currentTab = 0; + +private int targetTabIndex = 0; + +private float tabTransitionProgress = 1f; + +private Vector2 scrollPosition = Vector2.zero; +} +} diff --git a/ui/MenuRendering.cs b/ui/MenuRendering.cs new file mode 100644 index 0000000..45f8273 --- /dev/null +++ b/ui/MenuRendering.cs @@ -0,0 +1,950 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { +private void DrawSabotagesTab() + { + GUIStyle miniLabelStyle = new GUIStyle(toggleLabelStyle) { fontSize = 11, richText = true, wordWrap = true }; + miniLabelStyle.normal.textColor = whiteMenuTheme ? new Color(0.25f, 0.25f, 0.25f, 1f) : new Color(0.72f, 0.72f, 0.72f, 1f); + + GUILayout.BeginHorizontal(); + + GUILayout.BeginVertical(menuCardStyle, GUILayout.Width(276), GUILayout.ExpandHeight(false)); + DrawMenuSectionHeader("CRITICAL SABOTAGES"); + GUILayout.Space(4); + + GUILayout.BeginHorizontal(); + if (DrawColoredActionButton("FIX ALL", new Color32(83, 231, 139, 255), 116f, 32f)) FixAllSabotages(); + GUILayout.Space(6); + if (DrawColoredActionButton("TRIGGER ALL", new Color32(255, 74, 74, 255), 116f, 32f)) TriggerAllSabotages(); + GUILayout.EndHorizontal(); + + GUILayout.Space(6); + if (GUILayout.Button("CALL MEETING", btnStyle, GUILayout.Height(30))) callMeetingPublic(); + + GUILayout.Space(8); + GUILayout.BeginHorizontal(); + DrawSabotageButton("Reactor", ref reactorSab, ToggleReactor, new Color32(255, 84, 84, 255)); + GUILayout.Space(6); + DrawSabotageButton("Oxygen", ref oxygenSab, ToggleO2, new Color32(255, 132, 54, 255)); + GUILayout.EndHorizontal(); + + GUILayout.Space(6); + GUILayout.BeginHorizontal(); + DrawSabotageButton("Comms", ref commsSab, ToggleComms, new Color32(66, 205, 128, 255)); + GUILayout.Space(6); + DrawSabotageButton("Lights", ref elecSab, ToggleLights, new Color32(255, 218, 77, 255)); + GUILayout.EndHorizontal(); + + GUILayout.Space(6); + DrawSabotageButton("Unfixable Lights", ref unfixableLights, ToggleUnfixableLights, new Color32(210, 128, 255, 255)); + + GUILayout.Space(8); + if (GUILayout.Button("MUSHROOM MIXUP", btnStyle, GUILayout.Height(28))) SabotageMushroom(); + + GUILayout.Space(10); + DrawMenuSectionHeader("VENTS"); + unlockVents = DrawToggle(unlockVents, "Unlock Vents", 230); + GUILayout.Space(4); + walkInVents = DrawToggle(walkInVents, "Walk In Vents", 230); + GUILayout.EndVertical(); + + GUILayout.Space(10); + + GUILayout.BeginVertical(menuCardStyle, GUILayout.ExpandWidth(true)); + DrawMenuSectionHeader("DOOR LOCKDOWN"); + GUILayout.Space(4); + GUILayout.Label("Global controls", miniLabelStyle); + + GUILayout.BeginHorizontal(); + if (DrawColoredActionButton("CLOSE", new Color32(255, 106, 66, 255), 82f, 30f)) SabotageDoors(); + GUILayout.Space(6); + if (DrawColoredActionButton("LOCK", new Color32(255, 184, 64, 255), 82f, 30f)) LockAllDoors(); + GUILayout.Space(6); + if (DrawColoredActionButton("OPEN", new Color32(89, 219, 146, 255), 82f, 30f)) OpenAllDoors(); + GUILayout.EndHorizontal(); + + GUILayout.Space(8); + GUILayout.Label("Target doors", miniLabelStyle); + + if (ShipStatus.Instance != null && ShipStatus.Instance.AllDoors != null) + { + var rooms = ShipStatus.Instance.AllDoors + .Where(d => d != null) + .Select(d => d.Room) + .Distinct() + .OrderBy(r => r.ToString()) + .ToList(); + + doorsScrollPos = GUILayout.BeginScrollView(doorsScrollPos, false, true, GUILayout.Height(214)); + foreach (var room in rooms) + { + DrawDoorTargetRow(room); + GUILayout.Space(3); + } + GUILayout.EndScrollView(); + } + else + { + GUILayout.FlexibleSpace(); + GUILayout.Label("Вы не в игре или на карте нет дверей.", new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, richText = true }); + GUILayout.FlexibleSpace(); + } + GUILayout.EndVertical(); + + GUILayout.EndHorizontal(); + } + +private void DrawSabotageButton(string label, ref bool state, Action toggleAction, Color accent) + { + GUIStyle style = state ? activeTabStyle : btnStyle; + Color oldBackground = GUI.backgroundColor; + GUI.backgroundColor = state ? accent : Color.white; + + if (GUILayout.Button(state ? label + " ON" : label, style, GUILayout.Height(30))) + { + state = !state; + toggleAction(state); + } + + GUI.backgroundColor = oldBackground; + } + +private void DrawDoorTargetRow(SystemTypes room) + { + GUILayout.BeginHorizontal(boxStyle); + GUILayout.Label($"{room}", toggleLabelStyle, GUILayout.Width(96)); + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("Close", btnStyle, GUILayout.Width(52), GUILayout.Height(24))) CloseDoorsOfType(room); + GUILayout.Space(4); + if (GUILayout.Button("Lock", activeSubTabStyle, GUILayout.Width(52), GUILayout.Height(24))) LockDoorsOfType(room); + GUILayout.Space(4); + if (GUILayout.Button("Open", btnStyle, GUILayout.Width(52), GUILayout.Height(24))) OpenDoorsOfType(room); + + GUILayout.EndHorizontal(); + } + +private void callMeetingPublic() + { + if (PlayerControl.LocalPlayer == null || PlayerControl.AllPlayerControls == null) return; + try + { + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc != null && pc.Data != null && pc.Data.IsDead && !pc.Data.Disconnected) + { + PlayerControl.LocalPlayer.CmdReportDeadBody(pc.Data); + ShowNotification($"[MEETING] Найден и зарепорчен труп: {pc.Data.PlayerName}!"); + return; + } + } + + PlayerControl.LocalPlayer.CmdReportDeadBody(null); + ShowNotification("[MEETING] Легально нажата кнопка собрания!"); + } + catch { } + } + +private void TriggerAllSabotages() + { + if (ShipStatus.Instance == null) return; + try + { + reactorSab = true; + oxygenSab = true; + commsSab = true; + elecSab = true; + + ToggleReactor(true); + ToggleO2(true); + ToggleComms(true); + ToggleLights(true); + + ShowNotification("[SABOTAGE] Все системы саботированы!"); + } + catch { } + } + +private void FixAllSabotages() + { + if (ShipStatus.Instance == null) return; + try + { + reactorSab = false; + oxygenSab = false; + commsSab = false; + elecSab = false; + + ToggleReactor(false); + ToggleO2(false); + ToggleComms(false); + ToggleLights(false); + + if (ShipStatus.Instance.AllDoors != null) + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null) + { + door.SetDoorway(true); + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(door.Id | 64)); } catch { } + } + } + } + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.MushroomMixupSabotage, 0); } catch { } + ShowNotification("[SABOTAGE] Все саботажи и двери починены!"); + } + catch { } + } + +private void SabotageDoors() + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + var rooms = new System.Collections.Generic.HashSet(); + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null) + { + rooms.Add(door.Room); + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); } catch { } + } + } + foreach (var room in rooms) + { + try { ShipStatus.Instance.RpcCloseDoorsOfType(room); } catch { } + } + ShowNotification("[DOORS] Сигнал на закрытие отправлен!"); + } + catch { } + } + +private void CloseDoorsOfType(SystemTypes room) + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + ShipStatus.Instance.RpcCloseDoorsOfType(room); + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null && door.Room == room) + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); + } + ShowNotification($"[DOORS] {room}: close sent"); + } + catch { } + } + +private void LockDoorsOfType(SystemTypes room) + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null && door.Room == room) + { + door.SetDoorway(false); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); + } + } + ShipStatus.Instance.RpcCloseDoorsOfType(room); + ShowNotification($"[DOORS] {room}: locked"); + } + catch { } + } + +private void OpenDoorsOfType(SystemTypes room) + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null && door.Room == room) + { + door.SetDoorway(true); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(door.Id | 64)); + } + } + ShowNotification($"[DOORS] {room}: opened"); + } + catch { } + } + +private void LockAllDoors() + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + var rooms = new System.Collections.Generic.HashSet(); + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null) + { + door.SetDoorway(false); + rooms.Add(door.Room); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)door.Id); + } + } + foreach (var room in rooms) + ShipStatus.Instance.RpcCloseDoorsOfType(room); + + ShowNotification("[DOORS] Все двери залочены!"); + } + catch { } + } + +private void OpenAllDoors() + { + if (ShipStatus.Instance == null || ShipStatus.Instance.AllDoors == null) return; + try + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + if (door != null) + { + door.SetDoorway(true); + try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Doors, (byte)(door.Id | 64)); } catch { } + } + } + ShowNotification("[DOORS] Все двери открыты!"); + } + catch { } + } + +private void ToggleReactor(bool state) { if (ShipStatus.Instance == null) return; byte flag = (byte)(state ? 128 : 16); try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Reactor, flag); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Laboratory, flag); if (state) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)128); else { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)16); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.HeliSabotage, (byte)17); } } catch { } } + +private void ToggleO2(bool state) { if (ShipStatus.Instance == null) return; try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.LifeSupp, (byte)(state ? 128 : 16)); } catch { } } + +private void ToggleComms(bool state) { if (ShipStatus.Instance == null) return; try { if (state) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)128); else { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)16); ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, (byte)17); } } catch { } } + +private void ToggleLights(bool state) + { + if (ShipStatus.Instance == null) return; + try + { + if (state && unfixableLights) + { + unfixableLights = false; + ToggleUnfixableLights(false); + } + if (state) + { + byte b = 4; + for (int i = 0; i < 5; i++) if (UnityEngine.Random.value > 0.5f) b |= (byte)(1 << i); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, (byte)(b | 128)); + } + else + { + var sys = ShipStatus.Instance.Systems[SystemTypes.Electrical].Cast(); + if (sys != null) + { + for (int i = 0; i < 5; i++) + { + bool expected = (sys.ExpectedSwitches & (1 << i)) != 0; + bool actual = (sys.ActualSwitches & (1 << i)) != 0; + if (expected != actual) ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, (byte)i); + } + } + } + } + catch { } + } + +private void ToggleUnfixableLights(bool state) + { + if (ShipStatus.Instance == null) return; + try + { + if (!ShipStatus.Instance.Systems.ContainsKey(SystemTypes.Electrical)) + { + unfixableLights = false; + unfixableLightsApplied = false; + ShowNotification("[LIGHTS] Electrical system not present."); + return; + } + + if (state) + elecSab = false; + + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, 69); + unfixableLightsApplied = state; + ShowNotification(state ? "[LIGHTS] Unfixable lights ON" : "[LIGHTS] Unfixable lights OFF"); + } + catch { } + } + +private void UpdateUnfixableLightsState() + { + if (unfixableLights == unfixableLightsApplied) return; + ToggleUnfixableLights(unfixableLights); + } + +private void ApplyVentCheatsTick() + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null || local.Data == null) + return; + + if (unlockVents && !local.Data.IsDead && local.Data.Role != null && !local.Data.Role.CanVent && HudManager.Instance != null && HudManager.Instance.ImpostorVentButton != null) + HudManager.Instance.ImpostorVentButton.gameObject.SetActive(true); + + if (walkInVents && local.inVent) + { + local.inVent = false; + local.moveable = true; + } + } + catch { } + } + +private static void SetImmortalityVentState(bool enter) + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null || local.Data == null || ShipStatus.Instance == null) return; + if (local.inVent) return; + + VentilationSystem.Update(enter ? VentilationSystem.Operation.Enter : VentilationSystem.Operation.Exit, ImmortalityCustomVentId); + immortalityVentStateApplied = enter; + } + catch { } + } + +private static void TickRoleBuffImmortality() + { + try + { + PlayerControl local = PlayerControl.LocalPlayer; + if (local == null || local.Data == null || ShipStatus.Instance == null) + { + immortalityVentStateApplied = false; + return; + } + + if (!roleBuffImmortality || local.Data.IsDead) + { + if (immortalityVentStateApplied) + SetImmortalityVentState(false); + return; + } + + if (MeetingHud.Instance != null) + return; + + if (!immortalityVentStateApplied) + SetImmortalityVentState(true); + } + catch { } + } + +private static void DisableRoleBuffImmortality() + { + try + { + if (immortalityVentStateApplied) + SetImmortalityVentState(false); + } + catch { } + } + +private void SabotageMushroom() { if (ShipStatus.Instance == null) return; try { ShipStatus.Instance.RpcUpdateSystem(SystemTypes.MushroomMixupSabotage, (byte)1); } catch { } } + +private void DrawPlayersRoles() + { + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader("PRE-GAME ROLE MANAGER"); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(enablePreGameRoleForce ? "Role Forcing: ON" : "Role Forcing: OFF", enablePreGameRoleForce ? activeTabStyle : btnStyle, GUILayout.Height(25))) enablePreGameRoleForce = !enablePreGameRoleForce; + if (GUILayout.Button("Random 2 Imps", btnStyle, GUILayout.Width(110), GUILayout.Height(25))) + { + forcedPreGameRoles.Clear(); forcedImpostors.Clear(); + var activePlayers = PlayerControl.AllPlayerControls.ToArray().Where(p => p != null && !p.Data.Disconnected).ToList(); + if (activePlayers.Count >= 2) + { + for (int i = activePlayers.Count - 1; i > 0; i--) { int swapIndex = UnityEngine.Random.Range(0, i + 1); var temp = activePlayers[i]; activePlayers[i] = activePlayers[swapIndex]; activePlayers[swapIndex] = temp; } + forcedImpostors.Add(activePlayers[0].PlayerId); forcedImpostors.Add(activePlayers[1].PlayerId); + enablePreGameRoleForce = true; + } + } + if (GUILayout.Button("Clear All Roles", btnStyle, GUILayout.Width(110), GUILayout.Height(25))) { forcedPreGameRoles.Clear(); forcedImpostors.Clear(); } + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(8); + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader("LIVE ROLE DISTRIBUTOR (HOST)"); + GUILayout.BeginHorizontal(); + + GUIStyle allRoleMidStyle = new GUIStyle(btnStyle) + { + fontStyle = FontStyle.Bold, + normal = { background = null, textColor = GetMenuAccentColor() }, + alignment = TextAnchor.MiddleCenter + }; + + if (GUILayout.Button("<", btnStyle, GUILayout.Width(28), GUILayout.Height(25))) + { + allPlayersRoleAssignIdx--; + if (allPlayersRoleAssignIdx < 0) allPlayersRoleAssignIdx = roleAssignOptions.Length - 1; + } + + GUILayout.Label(roleAssignNames[allPlayersRoleAssignIdx], allRoleMidStyle, GUILayout.Height(25), GUILayout.ExpandWidth(true)); + + if (GUILayout.Button(">", btnStyle, GUILayout.Width(28), GUILayout.Height(25))) + { + allPlayersRoleAssignIdx++; + if (allPlayersRoleAssignIdx >= roleAssignOptions.Length) allPlayersRoleAssignIdx = 0; + } + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + if (GUILayout.Button("SET ALL PLAYERS ROLE", activeTabStyle, GUILayout.Height(28))) + { + if (IsGhostRoleSelection(allPlayersRoleAssignIdx)) + SetAllPlayersGhost(); + else if (IsGhostImpostorRoleSelection(allPlayersRoleAssignIdx)) + SetAllPlayersGhost(true); + else + SetAllPlayersRole(roleAssignOptions[allPlayersRoleAssignIdx]); + } + GUILayout.Space(4); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("ALL -> GHOST", btnStyle, GUILayout.Height(26))) + SetAllPlayersGhost(); + if (GUILayout.Button("REVIVE ALL", activeTabStyle, GUILayout.Height(26))) + ReviveAllPlayers(); + GUILayout.EndHorizontal(); + GUILayout.Space(4); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("ALL -> GHOST IMP", btnStyle, GUILayout.Height(26))) + SetAllPlayersGhost(true); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.Space(10); + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(menuCardStyle, GUILayout.Width(150), GUILayout.Height(315)); + preRolesListScrollPos = GUILayout.BeginScrollView(preRolesListScrollPos, GUILayout.ExpandHeight(true)); + foreach (var pc in lockedPlayersList) + { + if (pc == null || pc.Data == null || pc.PlayerId >= 100) continue; + string pName = pc.Data.PlayerName ?? "Unknown"; + if (forcedPreGameRoles.ContainsKey(pc.PlayerId)) { string rShort = forcedPreGameRoles[pc.PlayerId].ToString().Replace("9", "Pha").Replace("10", "Tra").Replace("8", "Noi").Replace("12", "Det").Replace("18", "Vip"); if (rShort.Length > 3) rShort = rShort.Substring(0, 3); pName += $" [{rShort}]"; } + else if (forcedImpostors.Contains(pc.PlayerId)) pName += " [Imp]"; + bool isSelected = selectedPreRoleId == pc.PlayerId; + try { GUI.contentColor = Palette.PlayerColors[pc.Data.DefaultOutfit.ColorId]; } catch { } + if (GUILayout.Button(pName, isSelected ? activeTabStyle : btnStyle, GUILayout.Height(30))) selectedPreRoleId = pc.PlayerId; + GUI.contentColor = Color.white; + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + + GUILayout.Space(8); + GUILayout.BeginVertical(menuCardStyle, GUILayout.ExpandWidth(true), GUILayout.Height(315)); + preRolesActionScrollPos = GUILayout.BeginScrollView(preRolesActionScrollPos, GUILayout.ExpandHeight(true)); + PlayerControl target = lockedPlayersList.FirstOrDefault(p => p.PlayerId == selectedPreRoleId); + if (target != null && target.Data != null) + { + GUIStyle infoStyle = new GUIStyle(GUI.skin.label) { richText = true, fontSize = 14 }; + GUILayout.Label($"Selecting role for: {target.Data.PlayerName}", infoStyle); + RoleTypes currentForced = forcedPreGameRoles.ContainsKey(target.PlayerId) ? forcedPreGameRoles[target.PlayerId] : RoleTypes.Crewmate; + bool isForced = forcedPreGameRoles.ContainsKey(target.PlayerId) || forcedImpostors.Contains(target.PlayerId); + string roleNameStr = currentForced.ToString().Replace("9", "Phantom").Replace("10", "Tracker").Replace("8", "Noisemaker").Replace("12", "Detective").Replace("18", "Viper"); + if (forcedImpostors.Contains(target.PlayerId)) roleNameStr = "Impostor"; + GUILayout.Label($"Status: {(isForced ? $"Forced ({roleNameStr})" : "Not Forced (Random)")}", infoStyle); + GUILayout.Space(15); + DrawMenuSectionHeader("IMPOSTOR ROLES (Red Team)"); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Impostor", btnStyle, GUILayout.Height(24))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Add(target.PlayerId); } + if (GUILayout.Button("Shapeshifter", btnStyle, GUILayout.Height(24))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Shapeshifter; } + if (GUILayout.Button("Phantom", btnStyle, GUILayout.Height(24))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)9; } + if (GUILayout.Button("Viper", btnStyle, GUILayout.Height(24))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)18; } + GUILayout.EndHorizontal(); + GUILayout.Space(10); + DrawMenuSectionHeader("CREWMATE ROLES (Blue Team)"); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Crewmate", btnStyle, GUILayout.Height(24))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Crewmate; } + if (GUILayout.Button("Engineer", btnStyle, GUILayout.Height(24))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Engineer; } + if (GUILayout.Button("Scientist", btnStyle, GUILayout.Height(24))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.Scientist; } + if (GUILayout.Button("Tracker", btnStyle, GUILayout.Height(24))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)10; } + GUILayout.EndHorizontal(); + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Noisemaker", btnStyle, GUILayout.Height(24))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)8; } + if (GUILayout.Button("Guardian Angel", btnStyle, GUILayout.Height(24))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = RoleTypes.GuardianAngel; } + if (GUILayout.Button("Detective", btnStyle, GUILayout.Height(24))) { forcedImpostors.Remove(target.PlayerId); forcedPreGameRoles[target.PlayerId] = (RoleTypes)12; } + GUILayout.EndHorizontal(); + GUILayout.Space(15); + if (GUILayout.Button("REMOVE FORCED ROLE", activeTabStyle, GUILayout.Height(35))) { forcedPreGameRoles.Remove(target.PlayerId); forcedImpostors.Remove(target.PlayerId); } + GUILayout.Space(20); + GUILayout.Label("Hide & Seek Notice:\nВыбор Impostor/Shapeshifter/Phantom/Viper расширит лимит маньяков (Seekers) в Прятках!", new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true }); + } + else + { + GUILayout.FlexibleSpace(); + GUILayout.Label("Select a player to set their role", new GUIStyle(GUI.skin.label) { richText = true, alignment = TextAnchor.MiddleCenter }); + GUILayout.FlexibleSpace(); + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + +private void DrawMenuSectionHeader(string title) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(GUIContent.none, menuAccentBarStyle, GUILayout.Width(3), GUILayout.Height(16)); + GUILayout.Space(8); + GUILayout.Label(title, menuSectionTitleStyle, GUILayout.Height(16)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Space(8); + } + +private void DrawMenuTab() + { + bool menuPrefsChanged = false; + + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader(L("MENU CUSTOMIZATION", "ОФОРМЛЕНИЕ МЕНЮ")); + + bool prevRgb = rgbMenuMode; + rgbMenuMode = DrawToggle(rgbMenuMode, "RGB Menu Mode", 260); + if (prevRgb && !rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); + if (prevRgb != rgbMenuMode) menuPrefsChanged = true; + GUILayout.Label(L("Smoothly cycles the accent through the rainbow.", "Плавно переливает акцент по радуге."), menuDescStyle); + GUILayout.Space(8); + + bool prevRgbText = rgbMenuText; + rgbMenuText = DrawToggle(rgbMenuText, "RGB Text", 260); + if (prevRgbText != rgbMenuText) + { + InitStyles(); + UpdateAccentColor(currentAccentColor); + menuPrefsChanged = true; + } + GUILayout.Label("When off, RGB Menu Mode does not recolor menu text.", menuDescStyle); + GUILayout.Space(8); + + bool prevBoldMenuText = boldMenuText; + boldMenuText = DrawToggle(boldMenuText, "Bold Menu Text", 260); + if (prevBoldMenuText != boldMenuText) + { + InitStyles(); + UpdateAccentColor(currentAccentColor); + menuPrefsChanged = true; + } + GUILayout.Label("Switches menu text between bold and normal. Default: bold.", menuDescStyle); + GUILayout.Space(8); + + bool prevWhiteTheme = whiteMenuTheme; + whiteMenuTheme = DrawToggle(whiteMenuTheme, "White Theme", 260); + if (prevWhiteTheme != whiteMenuTheme) + { + InitStyles(); + UpdateAccentColor(currentAccentColor); + menuPrefsChanged = true; + } + GUILayout.Label(L("Switches between the dark and light interface.", "Переключает тёмный и светлый интерфейс."), menuDescStyle); + GUILayout.Space(8); + + bool prevBg = enableBackground; + enableBackground = DrawToggle(enableBackground, "Enable Image Background", 260); + if (enableBackground && !prevBg) LoadBackgroundImage(); + if (prevBg != enableBackground) menuPrefsChanged = true; + GUILayout.Label(L("Put 'MenuBG.png' or .jpg in BepInEx/config to add a background image.", "Положите 'MenuBG.png' или .jpg в BepInEx/config для фона."), menuDescStyle); + GUILayout.Space(8); + + bool prevHardMenu = hardMenu; + hardMenu = DrawToggle(hardMenu, L("Solid Menu (block game clicks)", "Твердое меню (блок кликов по игре)"), 260); + if (prevHardMenu != hardMenu) menuPrefsChanged = true; + GUILayout.Label(L("When on, clicks over the menu stay in the menu so you can't misclick the game behind it.", "Когда включено, клики по меню остаются в меню — вы не промахнёте��ь по игре за ним."), menuDescStyle); + GUILayout.Space(8); + + bool prevAutoCopyCode = autoCopyCodeAndLeave; + autoCopyCodeAndLeave = DrawToggle(autoCopyCodeAndLeave, "Copy Code On Disconnect", 260); + if (prevAutoCopyCode != autoCopyCodeAndLeave) menuPrefsChanged = true; + GUILayout.Label("Copies the room code when you leave, get kicked, banned, or disconnected.", menuDescStyle); + GUILayout.EndVertical(); + + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader(L("ACCENT & PERFORMANCE", "АКЦЕНТ И ПРОИЗВОДИТЕЛЬНОСТЬ")); + + GUILayout.BeginHorizontal(); + GUILayout.Label(L("FPS Limit", "Лимит FPS"), new GUIStyle(toggleLabelStyle), GUILayout.Height(25), GUILayout.Width(110)); + int newFpsLimit = Mathf.Clamp((int)GUILayout.HorizontalSlider(fpsLimit, 60f, 240f, sliderStyle, sliderThumbStyle, GUILayout.Width(180)), 60, 240); + GUILayout.Space(10); + GUILayout.Label(fpsLimit.ToString(), menuBadgeStyle, GUILayout.Width(52), GUILayout.Height(22)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + if (newFpsLimit != fpsLimit) + { + fpsLimit = newFpsLimit; + ApplyFpsLimit(); + menuPrefsChanged = true; + } + + GUILayout.Space(12); + + GUILayout.BeginHorizontal(); + GUILayout.Label(L("Accent Color", "Цвет акцента"), new GUIStyle(toggleLabelStyle), GUILayout.Height(25), GUILayout.Width(110)); + Color prevGuiColor = GUI.color; + GUI.color = GetMenuControlAccentColor(); + GUILayout.Label(GUIContent.none, menuSwatchStyle, GUILayout.Width(22), GUILayout.Height(22)); + GUI.color = prevGuiColor; + GUILayout.Space(8); + GUI.enabled = !rgbMenuMode; + GUIStyle middleColorStyle = new GUIStyle(btnStyle) { normal = { background = null, textColor = GetMenuAccentColor() }, fontStyle = FontStyle.Bold }; + if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex--; if (currentMenuColorIndex < 0) currentMenuColorIndex = menuColors.Length - 1; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); menuPrefsChanged = true; } + GUILayout.Label(rgbMenuMode ? "RGB" : menuColorNames[currentMenuColorIndex], middleColorStyle, GUILayout.Width(120), GUILayout.Height(25)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { currentMenuColorIndex++; if (currentMenuColorIndex >= menuColors.Length) currentMenuColorIndex = 0; if (!rgbMenuMode) UpdateAccentColor(menuColors[currentMenuColorIndex]); menuPrefsChanged = true; } + GUI.enabled = true; + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader(L("SPOOF MENU IDENTITY", "ПОДМЕНА МЕНЮ")); + bool prevSpoofMenuEnabled = SpoofMenuEnabled; + SpoofMenuEnabled = DrawToggle(SpoofMenuEnabled, "Enable Fake RPC", 260); + if (prevSpoofMenuEnabled != SpoofMenuEnabled) menuPrefsChanged = true; + GUILayout.Label(L("Reports a fake mod menu name to other players.", "Показывает игрокам поддельное имя меню."), menuDescStyle); + GUILayout.Space(8); + GUILayout.BeginHorizontal(); + GUILayout.Label(L("Fake Name", "Поддельное имя"), new GUIStyle(toggleLabelStyle), GUILayout.Height(25), GUILayout.Width(110)); + GUI.enabled = SpoofMenuEnabled; + GUIStyle middleLabelStyle = new GUIStyle(btnStyle) { fontStyle = FontStyle.Bold, normal = { background = null, textColor = GetMenuAccentColor() } }; + if (GUILayout.Button("<", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex--; if (selectedSpoofMenuIndex < 0) selectedSpoofMenuIndex = spoofMenuNames.Length - 1; menuPrefsChanged = true; } + GUILayout.Label(spoofMenuNames[selectedSpoofMenuIndex], middleLabelStyle, GUILayout.Width(150), GUILayout.Height(25)); + if (GUILayout.Button(">", btnStyle, GUILayout.Width(30), GUILayout.Height(25))) { selectedSpoofMenuIndex++; if (selectedSpoofMenuIndex >= spoofMenuNames.Length) selectedSpoofMenuIndex = 0; menuPrefsChanged = true; } + GUI.enabled = true; + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader(L("NOTIFICATIONS & LOGGING", "УВЕДОМЛЕНИЯ И ЛОГИ")); + bool prevCustomNotifs = EnableCustomNotifs; + EnableCustomNotifs = DrawToggle(EnableCustomNotifs, "Enable Custom UI Notifications", 280); + if (prevCustomNotifs != EnableCustomNotifs) menuPrefsChanged = true; + GUILayout.Space(6); + bool prevLogAllRpcs = LogAllRPCs; + LogAllRPCs = DrawToggle(LogAllRPCs, "Sniff All RPCs (On-Screen)", 280); + if (prevLogAllRpcs != LogAllRPCs) menuPrefsChanged = true; + GUILayout.EndVertical(); + + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader(L("RESET SETTINGS", "СБРОС НАСТРОЕК")); + GUILayout.Label(L("Resets all sliders back to their default values.", "Сбрасывает все ползунки до значений по умолчанию."), menuDescStyle); + GUILayout.Space(6); + if (GUILayout.Button(L("Reset Sliders to Default", "Сбросить ползунки до дефолта"), activeTabStyle, GUILayout.Height(30))) + { + ResetSlidersToDefault(); + menuPrefsChanged = true; + } + GUILayout.EndVertical(); + + if (menuPrefsChanged) SaveConfig(); + } + +private void ResetSlidersToDefault() + { + selectedMapSpawnIdx = 0f; + chatHistoryLimit = 20; + customChatSpamDelay = 2.1f; + autoChatEveryoneDelay = 2.5f; + engineSpeed = 1f; + walkSpeed = 1f; + currentPlatformIndex = 1; + autoKickTimer = 5f; + autoKickMinLevel = 200; + fpsLimit = 60; + ApplyFpsLimit(); + AutoHostMinPlayers = 4; + AutoHostStartDelaySeconds = 15f; + AutoHostFastStartPlayers = 13; + AutoHostFastStartDelaySeconds = 5f; + punishmentMode = 0; + + showPlayerInfo = false; + seeGhosts = false; + seeRoles = false; + revealMeetingRoles = false; + showTracers = false; + fullBright = false; + seeProtections = false; + seeKillCooldown = false; + extendedLobby = false; + moreLobbyInfo = true; + alwaysShowLobbyTimer = false; + noClip = false; + tpToCursor = false; + dragToCursor = false; + autoFollowCursor = false; + freecam = false; + cameraZoom = false; + alwaysChat = false; + lobbyRainbowAll = false; + lobbyAllColor = false; + lobbyAllColorId = 0; + readGhostChat = false; + enableExtendedChat = true; + enableFastChat = true; + allowLinksAndSymbols = false; + enableChatHistory = true; + enableClipboard = true; + enableChatMessageDoubleClickCopy = true; + enableChatNameColorCopy = true; + enableChatLog = true; + enableColorCommand = false; + blockRainbowChat = true; + blockFortegreenChat = true; + AnimAsteroidsEnabled = false; + IsScanning = false; + AnimShieldsEnabled = false; + AnimCamsInUseEnabled = false; + AnimEmptyGarbageEnabled = false; + skipShhhAnim = false; + localRainbow = false; + localRainbowFreeOnly = false; + RevealVotesEnabled = false; + noTaskMode = false; + noMapCooldowns = false; + allowTasksAsImpostor = false; + killWhileVanishedHostOnly = false; + DisableRoleBuffImmortality(); + roleBuffImmortality = false; + neverEndGame = false; + removePenalty = true; + autoGhostAfterStart = false; + AutoHostEnabled = false; + AutoReturnLobbyAfterMatch = true; + AutoHostNotifications = true; + AutoHostForceLastMinute = true; + AutoHostWaitLoadedPlayers = true; + AutoHostCancelBelowMin = true; + AutoHostInstantStart = false; + autoBanEnabled = true; + allowDuplicateColors = false; + blockSpoofRPC = true; + autoBanPlatformSpoof = false; + banCustomPlatformsFromTxt = false; + autoKickLowLevelEnabled = false; + autoKickBugs = false; + disableVoteKicks = false; + blockSabotageRPC = true; + blockGameRpcInLobby = true; + blockChatFloodRpc = true; + blockMeetingFloodRpc = true; + enablePasosLimit = true; + enableLocalPasosBan = true; + enableHostPasosBan = true; + enableMalformedPacketGuard = true; + banMalformedPacketSender = false; + enableQuickChatEmptyGuard = true; + banQuickChatEmptySpammer = true; + enableUnownedSpawnGuard = true; + enableLocalNameSpoof = false; + enableLocalFriendCodeSpoof = false; + SpoofMenuEnabled = false; + enableBackground = false; + hardMenu = false; + rgbMenuText = false; + boldMenuText = true; + EnableCustomNotifs = true; + LogAllRPCs = true; + + settingsDirty = true; + InitStyles(); + UpdateAccentColor(currentAccentColor); + + ShowNotification(L("All sliders & toggles reset to default.", "Все ползунки и тумблеры сброшены до дефолта.")); + } + +private Vector2 outfitsScrollPos = Vector2.zero; + +public static bool AutoHostEnabled = false; + +public static bool AutoReturnLobbyAfterMatch = true; + +public static bool AutoHostNotifications = true; + +public static bool AutoHostForceLastMinute = true; + +public static bool AutoHostWaitLoadedPlayers = true; + +public static bool AutoHostCancelBelowMin = true; + +public static bool AutoHostInstantStart = false; + +public static int AutoHostMinPlayers = 4; + +public static int AutoHostForceMinPlayers = 2; + +public static float AutoHostStartDelaySeconds = 15f; + +public static float AutoHostBackoffSeconds = 8f; + +public static float AutoHostWarmupSeconds = 5f; + +public static float AutoHostLoadGraceSeconds = 20f; + +public static int AutoHostForceAfterMinutes = 0; + +public static int AutoHostFastStartPlayers = 13; + +public static float AutoHostFastStartDelaySeconds = 5f; + +private int currentAutoHostSubTab = 0; + +private string[] autoHostSubTabs = { "LOBBY CONTROLS", "ROLE MANAGER", "ANTI CHEAT", "AUTO HOST" }; + } +} diff --git a/ui/MenuTranslations.cs b/ui/MenuTranslations.cs new file mode 100644 index 0000000..0c8febe --- /dev/null +++ b/ui/MenuTranslations.cs @@ -0,0 +1,70 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { + +private static readonly Dictionary> menuTranslations = new Dictionary> + { + ["de"] = new Dictionary { ["RESET SETTINGS"]="ZURÜCKSETZEN",["Resets all sliders back to their default values."]="Setzt alle Regler auf ihre Standardwerte zurück.",["Reset Sliders to Default"]="Regler zurücksetzen",["All sliders & toggles reset to default."]="Alle Regler & Schalter zurückgesetzt.",["RAINBOW — FREE COLORS (NO HOST)"]="REGENBOGEN — FREIE FARBEN (KEIN HOST)",["Rainbow (only free colors)"]="Regenbogen (nur freie Farben)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Wechselt deine Farbe nur durch im Raum freie Farben, damit der Anti-Cheat des Hosts dich nicht für eine vergebene Farbe bannt.",["Pick free color:"]="Freie Farbe wählen:",["none"]="keine",["Applied free color: "]="Freie Farbe angewendet: ",["Free colors right now: "]="Freie Farben jetzt: ",["GENERAL"]="ALLGEMEIN",["SELF"]="SPIELER",["VISUALS"]="VISUELL",["PLAYERS"]="SPIELER",["SABOTAGES"]="SABOTAGEN",["HOST ONLY"]="NUR HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="ABSTIMMUNG",["MENU"]="MENÜ",["MAPS"]="KARTEN",["ANIMATIONS"]="ANIMATIONEN",["INFORMATION"]="INFORMATION",["KEYBINDS"]="TASTEN",["WELCOME"]="WILLKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menüsprache:",["FPS Limit"]="FPS-Limit",["Chat History"]="Chat-Verlauf",["History:"]="Verlauf:",["History size:"]="Verlaufgröße:",["CHAT UTILITY"]="CHAT-TOOLS",["Always Show Chat"]="Chat immer anzeigen",["Read Ghost Chat"]="Geisterchat lesen",["Extended Chat"]="Erweiterter Chat",["Fast Chat"]="Schneller Chat",["Unlock Extra Characters"]="Alle Zeichen erlauben",["Spell Check"]="Rechtschreibprüfung",["Clipboard"]="Zwischenablage",["Save Chat Log"]="Chatlog speichern",["Dark Chat Theme"]="Dunkles Chat-Thema",["Enable /color"]="Aktiviere /color",["Block Fortegreen"]="Fortegreen blockieren",["Allow Duplicate Colors"]="Doppelte Farben erlauben",["Auto Ghost After Start"]="Auto-Geist nach Start",["FAVORITE OUTFITS"]="FAVORITEN-OUTFITS",["Slot"]="Slot",["Empty"]="Leer",["Apply"]="Anwenden",["Save Mine"]="Meins speichern",["Save Selected"]="Auswahl speichern",["Saved slot"]="Slot gespeichert",["Applied slot"]="Slot angewendet",["Cleared slot"]="Slot gelöscht",["Auto-Ban Platform Spoof (Host)"]="Auto-Ban Plattform-Spoof (Host)",["Ban Custom Platforms From TXT"]="Custom-Plattformen aus TXT bannen",["RPC Anti-Cheat"]="RPC-Anti-Cheat",["RPC limit:"]="RPC-Limit:",["RPC Local Drop"]="RPC lokal droppen",["RPC Host Ban"]="RPC Host-Ban" }, + ["fr"] = new Dictionary { ["RESET SETTINGS"]="RÉINITIALISER",["Resets all sliders back to their default values."]="Réinitialise tous les curseurs à leurs valeurs par défaut.",["Reset Sliders to Default"]="Réinitialiser les curseurs",["All sliders & toggles reset to default."]="Tous les curseurs et options réinitialisés.",["RAINBOW — FREE COLORS (NO HOST)"]="ARC-EN-CIEL — COULEURS LIBRES (SANS HÔTE)",["Rainbow (only free colors)"]="Arc-en-ciel (couleurs libres)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Change votre couleur uniquement parmi les couleurs libres du salon, pour que l'anti-triche de l'hôte ne vous bannisse pas.",["Pick free color:"]="Choisir couleur libre :",["none"]="aucune",["Applied free color: "]="Couleur libre appliquée : ",["Free colors right now: "]="Couleurs libres maintenant : ",["GENERAL"]="GÉNÉRAL",["SELF"]="JOUEUR",["VISUALS"]="VISUELS",["PLAYERS"]="JOUEURS",["SABOTAGES"]="SABOTAGES",["HOST ONLY"]="HÔTE",["OUTFITS"]="TENUES",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="CARTES",["ANIMATIONS"]="ANIMATIONS",["INFORMATION"]="INFORMATION",["KEYBINDS"]="TOUCHES",["WELCOME"]="ACCUEIL",["CREDITS"]="CRÉDITS",["Menu language:"]="Langue du menu :",["FPS Limit"]="Limite FPS",["Chat History"]="Historique du chat",["History:"]="Historique :",["History size:"]="Taille historique :",["CHAT UTILITY"]="OUTILS CHAT",["Always Show Chat"]="Toujours afficher le chat",["Read Ghost Chat"]="Lire le chat fantôme",["Extended Chat"]="Chat étendu",["Fast Chat"]="Chat rapide",["Unlock Extra Characters"]="Autoriser tous les caractères",["Spell Check"]="Correction",["Clipboard"]="Presse-papiers",["Save Chat Log"]="Sauver le log chat",["Dark Chat Theme"]="Thème chat sombre",["Enable /color"]="Activer /color",["Block Fortegreen"]="Bloquer Fortegreen",["Allow Duplicate Colors"]="Autoriser les couleurs doubles",["Auto Ghost After Start"]="Fantôme auto après départ",["FAVORITE OUTFITS"]="TENUES FAVORITES",["Slot"]="Empl.",["Empty"]="Vide",["Apply"]="Appliquer",["Save Mine"]="Sauver mien",["Save Selected"]="Sauver sélection",["Saved slot"]="Emplacement sauvé",["Applied slot"]="Emplacement appliqué",["Cleared slot"]="Emplacement vidé",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof plateforme (Hôte)",["Ban Custom Platforms From TXT"]="Ban plateformes custom TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limite RPC :",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC hôte" }, + ["es"] = new Dictionary { ["RESET SETTINGS"]="RESTABLECER",["Resets all sliders back to their default values."]="Restablece todos los controles a sus valores predeterminados.",["Reset Sliders to Default"]="Restablecer controles",["All sliders & toggles reset to default."]="Todos los controles y opciones restablecidos.",["RAINBOW — FREE COLORS (NO HOST)"]="ARCOÍRIS — COLORES LIBRES (SIN HOST)",["Rainbow (only free colors)"]="Arcoíris (solo colores libres)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Cambia tu color solo entre los colores libres de la sala, para que el anti-cheat del host no te banee por un color ocupado.",["Pick free color:"]="Elegir color libre:",["none"]="ninguno",["Applied free color: "]="Color libre aplicado: ",["Free colors right now: "]="Colores libres ahora: ",["GENERAL"]="GENERAL",["SELF"]="JUGADOR",["VISUALS"]="VISUALES",["PLAYERS"]="JUGADORES",["SABOTAGES"]="SABOTAJES",["HOST ONLY"]="HOST",["OUTFITS"]="ATUENDOS",["VOTEKICK"]="VOTOKICK",["MENU"]="MENÚ",["MAPS"]="MAPAS",["ANIMATIONS"]="ANIMACIONES",["INFORMATION"]="INFORMACIÓN",["KEYBINDS"]="TECLAS",["WELCOME"]="BIENVENIDA",["CREDITS"]="CRÉDITOS",["Menu language:"]="Idioma del menú:",["FPS Limit"]="Límite FPS",["Chat History"]="Historial de chat",["History:"]="Historial:",["History size:"]="Tamaño historial:",["CHAT UTILITY"]="UTILIDAD CHAT",["Always Show Chat"]="Mostrar chat siempre",["Read Ghost Chat"]="Leer chat fantasma",["Extended Chat"]="Chat extendido",["Fast Chat"]="Chat rápido",["Unlock Extra Characters"]="Permitir caracteres extra",["Spell Check"]="Ortografía",["Clipboard"]="Portapapeles",["Save Chat Log"]="Guardar log chat",["Dark Chat Theme"]="Tema chat oscuro",["Enable /color"]="Activar /color",["Block Fortegreen"]="Bloquear Fortegreen",["Allow Duplicate Colors"]="Permitir colores duplicados",["Auto Ghost After Start"]="Fantasma auto al iniciar",["FAVORITE OUTFITS"]="ATUENDOS FAVORITOS",["Slot"]="Ranura",["Empty"]="Vacío",["Apply"]="Aplicar",["Save Mine"]="Guardar mío",["Save Selected"]="Guardar selección",["Saved slot"]="Ranura guardada",["Applied slot"]="Ranura aplicada",["Cleared slot"]="Ranura borrada",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof plataforma (Host)",["Ban Custom Platforms From TXT"]="Ban plataformas TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Límite RPC:",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC host" }, + ["it"] = new Dictionary { ["RESET SETTINGS"]="RIPRISTINA",["Resets all sliders back to their default values."]="Ripristina tutti i cursori ai valori predefiniti.",["Reset Sliders to Default"]="Ripristina cursori",["All sliders & toggles reset to default."]="Tutti i cursori e le opzioni ripristinati.",["RAINBOW — FREE COLORS (NO HOST)"]="ARCOBALENO — COLORI LIBERI (NO HOST)",["Rainbow (only free colors)"]="Arcobaleno (solo colori liberi)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Cambia il tuo colore solo tra i colori liberi nella stanza, così l'anti-cheat dell'host non ti banna per un colore occupato.",["Pick free color:"]="Scegli colore libero:",["none"]="nessuno",["Applied free color: "]="Colore libero applicato: ",["Free colors right now: "]="Colori liberi ora: ",["GENERAL"]="GENERALE",["SELF"]="GIOCATORE",["VISUALS"]="VISIVI",["PLAYERS"]="GIOCATORI",["SABOTAGES"]="SABOTAGGI",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFIT",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPPE",["ANIMATIONS"]="ANIMAZIONI",["INFORMATION"]="INFO",["KEYBINDS"]="TASTI",["WELCOME"]="BENVENUTO",["CREDITS"]="CREDITI",["Menu language:"]="Lingua menu:",["FPS Limit"]="Limite FPS",["Chat History"]="Cronologia chat",["History:"]="Cronologia:",["History size:"]="Dim. cronologia:",["CHAT UTILITY"]="UTILITÀ CHAT",["Always Show Chat"]="Mostra sempre chat",["Read Ghost Chat"]="Leggi chat fantasmi",["Extended Chat"]="Chat estesa",["Fast Chat"]="Chat veloce",["Unlock Extra Characters"]="Sblocca caratteri extra",["Spell Check"]="Correttore",["Clipboard"]="Appunti",["Save Chat Log"]="Salva log chat",["Dark Chat Theme"]="Tema chat scuro",["Enable /color"]="Abilita /color",["Block Fortegreen"]="Blocca Fortegreen",["Allow Duplicate Colors"]="Consenti colori doppi",["Auto Ghost After Start"]="Fantasma auto dopo start",["FAVORITE OUTFITS"]="OUTFIT PREFERITI",["Slot"]="Slot",["Empty"]="Vuoto",["Apply"]="Applica",["Save Mine"]="Salva mio",["Save Selected"]="Salva selez.",["Saved slot"]="Slot salvato",["Applied slot"]="Slot applicato",["Cleared slot"]="Slot pulito",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof piattaforma",["Ban Custom Platforms From TXT"]="Ban piattaforme custom TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limite RPC:",["RPC Local Drop"]="Drop RPC locale",["RPC Host Ban"]="Ban RPC host" }, + ["pt"] = new Dictionary { ["RESET SETTINGS"]="REDEFINIR",["Resets all sliders back to their default values."]="Redefine todos os controles para os valores padrão.",["Reset Sliders to Default"]="Redefinir controles",["All sliders & toggles reset to default."]="Todos os controles e opções redefinidos.",["RAINBOW — FREE COLORS (NO HOST)"]="ARCO-ÍRIS — CORES LIVRES (SEM HOST)",["Rainbow (only free colors)"]="Arco-íris (apenas cores livres)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Alterna sua cor apenas entre as cores livres na sala, para que o anti-cheat do host não te bana por uma cor ocupada.",["Pick free color:"]="Escolher cor livre:",["none"]="nenhuma",["Applied free color: "]="Cor livre aplicada: ",["Free colors right now: "]="Cores livres agora: ",["GENERAL"]="GERAL",["SELF"]="JOGADOR",["VISUALS"]="VISUAIS",["PLAYERS"]="JOGADORES",["SABOTAGES"]="SABOTAGENS",["HOST ONLY"]="HOST",["OUTFITS"]="VISUAIS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPAS",["ANIMATIONS"]="ANIMAÇÕES",["INFORMATION"]="INFORMAÇÃO",["KEYBINDS"]="TECLAS",["WELCOME"]="BOAS-VINDAS",["CREDITS"]="CRÉDITOS",["Menu language:"]="Idioma do menu:",["FPS Limit"]="Limite FPS",["Chat History"]="Histórico do chat",["History:"]="Histórico:",["History size:"]="Tamanho histórico:",["CHAT UTILITY"]="UTILIDADE CHAT",["Always Show Chat"]="Sempre mostrar chat",["Read Ghost Chat"]="Ler chat fantasma",["Extended Chat"]="Chat estendido",["Fast Chat"]="Chat rápido",["Unlock Extra Characters"]="Liberar caracteres extra",["Spell Check"]="Ortografia",["Clipboard"]="Área de transferência",["Save Chat Log"]="Salvar log chat",["Dark Chat Theme"]="Tema chat escuro",["Enable /color"]="Ativar /color",["Block Fortegreen"]="Bloquear Fortegreen",["Allow Duplicate Colors"]="Permitir cores duplicadas",["Auto Ghost After Start"]="Fantasma auto após iniciar",["FAVORITE OUTFITS"]="VISUAIS FAVORITOS",["Slot"]="Slot",["Empty"]="Vazio",["Apply"]="Aplicar",["Save Mine"]="Salvar meu",["Save Selected"]="Salvar seleção",["Saved slot"]="Slot salvo",["Applied slot"]="Slot aplicado",["Cleared slot"]="Slot limpo",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof plataforma",["Ban Custom Platforms From TXT"]="Ban plataformas TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limite RPC:",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC host" }, + ["pl"] = new Dictionary { ["RESET SETTINGS"]="RESETUJ",["Resets all sliders back to their default values."]="Przywraca wszystkie suwaki do wartości domyślnych.",["Reset Sliders to Default"]="Resetuj suwaki",["All sliders & toggles reset to default."]="Wszystkie suwaki i przełączniki zresetowane.",["RAINBOW — FREE COLORS (NO HOST)"]="TĘCZA — WOLNE KOLORY (BEZ HOSTA)",["Rainbow (only free colors)"]="Tęcza (tylko wolne kolory)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Zmienia twój kolor tylko wśród wolnych kolorów w pokoju, aby anti-cheat hosta nie zbanował cię za zajęty kolor.",["Pick free color:"]="Wybierz wolny kolor:",["none"]="brak",["Applied free color: "]="Zastosowano wolny kolor: ",["Free colors right now: "]="Wolne kolory teraz: ",["GENERAL"]="OGÓLNE",["SELF"]="GRACZ",["VISUALS"]="WIZUALNE",["PLAYERS"]="GRACZE",["SABOTAGES"]="SABOTAŻE",["HOST ONLY"]="HOST",["OUTFITS"]="STROJE",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPY",["ANIMATIONS"]="ANIMACJE",["INFORMATION"]="INFORMACJE",["KEYBINDS"]="KLAWISZE",["WELCOME"]="WITAJ",["CREDITS"]="AUTORZY",["Menu language:"]="Język menu:",["FPS Limit"]="Limit FPS",["Chat History"]="Historia czatu",["History:"]="Historia:",["History size:"]="Rozmiar historii:",["CHAT UTILITY"]="NARZĘDZIA CZATU",["Always Show Chat"]="Zawsze pokazuj czat",["Read Ghost Chat"]="Czytaj czat duchów",["Extended Chat"]="Rozszerzony czat",["Fast Chat"]="Szybki czat",["Unlock Extra Characters"]="Odblokuj znaki",["Spell Check"]="Pisownia",["Clipboard"]="Schowek",["Save Chat Log"]="Zapisz log czatu",["Dark Chat Theme"]="Ciemny czat",["Enable /color"]="Włącz /color",["Block Fortegreen"]="Blokuj Fortegreen",["Allow Duplicate Colors"]="Zezwól na duplikaty kolorów",["Auto Ghost After Start"]="Auto duch po starcie",["FAVORITE OUTFITS"]="ULUBIONE STROJE",["Slot"]="Slot",["Empty"]="Pusty",["Apply"]="Zastosuj",["Save Mine"]="Zapisz mój",["Save Selected"]="Zapisz wybrany",["Saved slot"]="Slot zapisany",["Applied slot"]="Slot użyty",["Cleared slot"]="Slot wyczyszczony",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof platformy",["Ban Custom Platforms From TXT"]="Ban platform z TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limit RPC:",["RPC Local Drop"]="Lokalny drop RPC",["RPC Host Ban"]="Ban RPC hosta" }, + ["nl"] = new Dictionary { ["RESET SETTINGS"]="RESETTEN",["Resets all sliders back to their default values."]="Zet alle schuifregelaars terug naar standaardwaarden.",["Reset Sliders to Default"]="Schuifregelaars resetten",["All sliders & toggles reset to default."]="Alle schuifregelaars & schakelaars gereset.",["RAINBOW — FREE COLORS (NO HOST)"]="REGENBOOG — VRIJE KLEUREN (GEEN HOST)",["Rainbow (only free colors)"]="Regenboog (alleen vrije kleuren)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Wisselt je kleur alleen tussen vrije kleuren in de room, zodat de anti-cheat van de host je niet bant voor een bezette kleur.",["Pick free color:"]="Kies vrije kleur:",["none"]="geen",["Applied free color: "]="Vrije kleur toegepast: ",["Free colors right now: "]="Vrije kleuren nu: ",["GENERAL"]="ALGEMEEN",["SELF"]="SPELER",["VISUALS"]="VISUEEL",["PLAYERS"]="SPELERS",["SABOTAGES"]="SABOTAGES",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="KAARTEN",["ANIMATIONS"]="ANIMATIES",["INFORMATION"]="INFORMATIE",["KEYBINDS"]="TOETSEN",["WELCOME"]="WELKOM",["CREDITS"]="CREDITS",["Menu language:"]="Menutaal:",["FPS Limit"]="FPS-limiet",["Chat History"]="Chatgeschiedenis",["History:"]="Geschiedenis:",["History size:"]="Geschiedenisgrootte:",["CHAT UTILITY"]="CHAT-HULP",["Always Show Chat"]="Chat altijd tonen",["Read Ghost Chat"]="Geestenchat lezen",["Extended Chat"]="Uitgebreide chat",["Fast Chat"]="Snelle chat",["Unlock Extra Characters"]="Extra tekens toestaan",["Spell Check"]="Spelling",["Clipboard"]="Klembord",["Save Chat Log"]="Chatlog opslaan",["Dark Chat Theme"]="Donker chatthema",["Enable /color"]="/color inschakelen",["Block Fortegreen"]="Fortegreen blokkeren",["Allow Duplicate Colors"]="Dubbele kleuren toestaan",["Auto Ghost After Start"]="Auto-geest na start",["FAVORITE OUTFITS"]="FAVORIETE OUTFITS",["Slot"]="Slot",["Empty"]="Leeg",["Apply"]="Toepassen",["Save Mine"]="Mijn opslaan",["Save Selected"]="Selectie opslaan",["Saved slot"]="Slot opgeslagen",["Applied slot"]="Slot toegepast",["Cleared slot"]="Slot gewist",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform-spoof",["Ban Custom Platforms From TXT"]="Ban custom platforms uit TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-limiet:",["RPC Local Drop"]="RPC lokale drop",["RPC Host Ban"]="RPC host-ban" }, + ["tr"] = new Dictionary { ["RESET SETTINGS"]="SIFIRLA",["Resets all sliders back to their default values."]="Tüm kaydırıcıları varsayılan değerlerine sıfırlar.",["Reset Sliders to Default"]="Kaydırıcıları sıfırla",["All sliders & toggles reset to default."]="Tüm kaydırıcılar ve anahtarlar sıfırlandı.",["RAINBOW — FREE COLORS (NO HOST)"]="GÖKKUŞAĞI — BOŞ RENKLER (HOST YOK)",["Rainbow (only free colors)"]="Gökkuşağı (sadece boş renkler)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Rengini yalnızca odadaki boş renkler arasında değiştirir, böylece host'un anti-cheat'i seni dolu renk için banlamaz.",["Pick free color:"]="Boş renk seç:",["none"]="yok",["Applied free color: "]="Boş renk uygulandı: ",["Free colors right now: "]="Şu an boş renkler: ",["GENERAL"]="GENEL",["SELF"]="OYUNCU",["VISUALS"]="GÖRSEL",["PLAYERS"]="OYUNCULAR",["SABOTAGES"]="SABOTAJLAR",["HOST ONLY"]="HOST",["OUTFITS"]="KIYAFETLER",["VOTEKICK"]="VOTEKICK",["MENU"]="MENÜ",["MAPS"]="HARİTALAR",["ANIMATIONS"]="ANİMASYONLAR",["INFORMATION"]="BİLGİ",["KEYBINDS"]="TUŞLAR",["WELCOME"]="HOŞ GELDİN",["CREDITS"]="KREDİLER",["Menu language:"]="Menü dili:",["FPS Limit"]="FPS sınırı",["Chat History"]="Sohbet geçmişi",["History:"]="Geçmiş:",["History size:"]="Geçmiş boyutu:",["CHAT UTILITY"]="SOHBET ARAÇLARI",["Always Show Chat"]="Sohbeti hep göster",["Read Ghost Chat"]="Hayalet sohbetini oku",["Extended Chat"]="Geniş sohbet",["Fast Chat"]="Hızlı sohbet",["Unlock Extra Characters"]="Ek karakterleri aç",["Spell Check"]="Yazım denetimi",["Clipboard"]="Pano",["Save Chat Log"]="Sohbet kaydını sakla",["Dark Chat Theme"]="Koyu sohbet teması",["Enable /color"]="/color aç",["Block Fortegreen"]="Fortegreen engelle",["Allow Duplicate Colors"]="Aynı renklere izin ver",["Auto Ghost After Start"]="Başlangıçtan sonra oto hayalet",["FAVORITE OUTFITS"]="FAVORİ KIYAFETLER",["Slot"]="Slot",["Empty"]="Boş",["Apply"]="Uygula",["Save Mine"]="Benimkini kaydet",["Save Selected"]="Seçileni kaydet",["Saved slot"]="Slot kaydedildi",["Applied slot"]="Slot uygulandı",["Cleared slot"]="Slot temizlendi",["Auto-Ban Platform Spoof (Host)"]="Platform spoof oto-ban",["Ban Custom Platforms From TXT"]="TXT özel platform ban",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC sınırı:",["RPC Local Drop"]="RPC yerel drop",["RPC Host Ban"]="RPC host ban" }, + ["cs"] = new Dictionary { ["RESET SETTINGS"]="RESETOVAT",["Resets all sliders back to their default values."]="Obnoví všechny posuvníky na výchozí hodnoty.",["Reset Sliders to Default"]="Resetovat posuvníky",["All sliders & toggles reset to default."]="Všechny posuvníky a přepínače resetovány.",["RAINBOW — FREE COLORS (NO HOST)"]="DUHA — VOLNÉ BARVY (BEZ HOSTA)",["Rainbow (only free colors)"]="Duha (jen volné barvy)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Mění tvou barvu jen mezi volnými barvami v místnosti, aby tě anti-cheat hostitele nezabanoval za obsazenou barvu.",["Pick free color:"]="Vyber volnou barvu:",["none"]="žádná",["Applied free color: "]="Volná barva použita: ",["Free colors right now: "]="Volné barvy teď: ",["GENERAL"]="OBECNÉ",["SELF"]="HRÁČ",["VISUALS"]="VIZUÁLY",["PLAYERS"]="HRÁČI",["SABOTAGES"]="SABOTÁŽE",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITY",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="MAPY",["ANIMATIONS"]="ANIMACE",["INFORMATION"]="INFORMACE",["KEYBINDS"]="KLÁVESY",["WELCOME"]="VÍTEJ",["CREDITS"]="AUTOŘI",["Menu language:"]="Jazyk menu:",["FPS Limit"]="Limit FPS",["Chat History"]="Historie chatu",["History:"]="Historie:",["History size:"]="Velikost historie:",["CHAT UTILITY"]="NÁSTROJE CHATU",["Always Show Chat"]="Vždy zobrazit chat",["Read Ghost Chat"]="Číst chat duchů",["Extended Chat"]="Rozšířený chat",["Fast Chat"]="Rychlý chat",["Unlock Extra Characters"]="Povolit další znaky",["Spell Check"]="Kontrola pravopisu",["Clipboard"]="Schránka",["Save Chat Log"]="Uložit log chatu",["Dark Chat Theme"]="Tmavý chat",["Enable /color"]="Zapnout /color",["Block Fortegreen"]="Blokovat Fortegreen",["Allow Duplicate Colors"]="Povolit duplicitní barvy",["Auto Ghost After Start"]="Auto duch po startu",["FAVORITE OUTFITS"]="OBLÍBENÉ OUTFITY",["Slot"]="Slot",["Empty"]="Prázdné",["Apply"]="Použít",["Save Mine"]="Uložit můj",["Save Selected"]="Uložit vybraný",["Saved slot"]="Slot uložen",["Applied slot"]="Slot použit",["Cleared slot"]="Slot vymazán",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof platformy",["Ban Custom Platforms From TXT"]="Ban platforem z TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="Limit RPC:",["RPC Local Drop"]="Místní drop RPC",["RPC Host Ban"]="RPC host ban" }, + ["ro"] = new Dictionary { ["RESET SETTINGS"]="RESETARE",["Resets all sliders back to their default values."]="Resetează toate glisoarele la valorile implicite.",["Reset Sliders to Default"]="Resetează glisoarele",["All sliders & toggles reset to default."]="Toate glisoarele şi comutatoarele resetate.",["RAINBOW — FREE COLORS (NO HOST)"]="CURCUBEU — CULORI LIBERE (FĂRĂ HOST)",["Rainbow (only free colors)"]="Curcubeu (doar culori libere)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Îți schimbă culoarea doar printre culorile libere din cameră, ca anti-cheat-ul host-ului să nu te banheze pentru o culoare ocupată.",["Pick free color:"]="Alege culoare liberă:",["none"]="niciuna",["Applied free color: "]="Culoare liberă aplicată: ",["Free colors right now: "]="Culori libere acum: ",["GENERAL"]="GENERAL",["SELF"]="JUCĂTOR",["VISUALS"]="VIZUAL",["PLAYERS"]="JUCĂTORI",["SABOTAGES"]="SABOTAJE",["HOST ONLY"]="HOST",["OUTFITS"]="ȚINUTE",["VOTEKICK"]="VOTEKICK",["MENU"]="MENIU",["MAPS"]="HĂRȚI",["ANIMATIONS"]="ANIMAȚII",["INFORMATION"]="INFORMAȚII",["KEYBINDS"]="TASTE",["WELCOME"]="BUN VENIT",["CREDITS"]="CREDITE",["Menu language:"]="Limba meniului:",["FPS Limit"]="Limită FPS",["Chat History"]="Istoric chat",["History:"]="Istoric:",["History size:"]="Mărime istoric:",["CHAT UTILITY"]="UTILITARE CHAT",["Always Show Chat"]="Arată chat mereu",["Read Ghost Chat"]="Citește chat fantome",["Extended Chat"]="Chat extins",["Fast Chat"]="Chat rapid",["Unlock Extra Characters"]="Permite caractere extra",["Spell Check"]="Ortografie",["Clipboard"]="Clipboard",["Save Chat Log"]="Salvează log chat",["Dark Chat Theme"]="Temă chat întunecată",["Enable /color"]="Activează /color",["Block Fortegreen"]="Blochează Fortegreen",["Allow Duplicate Colors"]="Permite culori duplicate",["Auto Ghost After Start"]="Fantoma auto după start",["FAVORITE OUTFITS"]="ȚINUTE FAVORITE",["Slot"]="Slot",["Empty"]="Gol",["Apply"]="Aplică",["Save Mine"]="Salvează al meu",["Save Selected"]="Salvează selectat",["Saved slot"]="Slot salvat",["Applied slot"]="Slot aplicat",["Cleared slot"]="Slot golit",["Auto-Ban Platform Spoof (Host)"]="Auto-ban spoof platformă",["Ban Custom Platforms From TXT"]="Ban platforme din TXT",["RPC Anti-Cheat"]="Anti-cheat RPC",["RPC limit:"]="Limită RPC:",["RPC Local Drop"]="Drop RPC local",["RPC Host Ban"]="Ban RPC host" }, + ["hu"] = new Dictionary { ["RESET SETTINGS"]="VISSZAÁLLÍTÁS",["Resets all sliders back to their default values."]="Visszaállítja az összes csúszkát az alapértelmezett értékre.",["Reset Sliders to Default"]="Csúszkák visszaállítása",["All sliders & toggles reset to default."]="Minden csúszka és kapcsoló visszaállítva.",["RAINBOW — FREE COLORS (NO HOST)"]="SZIVÁRVÁNY — SZABAD SZÍNEK (NINCS HOST)",["Rainbow (only free colors)"]="Szivárvány (csak szabad színek)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="A színedet csak a szobában szabad színek között váltja, hogy a host anti-cheatje ne banoljon foglalt színért.",["Pick free color:"]="Válassz szabad színt:",["none"]="nincs",["Applied free color: "]="Szabad szín alkalmazva: ",["Free colors right now: "]="Szabad színek most: ",["GENERAL"]="ÁLTALÁNOS",["SELF"]="JÁTÉKOS",["VISUALS"]="LÁTVÁNY",["PLAYERS"]="JÁTÉKOSOK",["SABOTAGES"]="SZABOTÁZS",["HOST ONLY"]="HOST",["OUTFITS"]="RUHÁK",["VOTEKICK"]="VOTEKICK",["MENU"]="MENÜ",["MAPS"]="PÁLYÁK",["ANIMATIONS"]="ANIMÁCIÓK",["INFORMATION"]="INFORMÁCIÓ",["KEYBINDS"]="BILLENTYŰK",["WELCOME"]="ÜDV",["CREDITS"]="KÉSZÍTŐK",["Menu language:"]="Menü nyelve:",["FPS Limit"]="FPS limit",["Chat History"]="Chat előzmények",["History:"]="Előzmény:",["History size:"]="Előzmény méret:",["CHAT UTILITY"]="CHAT ESZKÖZÖK",["Always Show Chat"]="Chat mindig látszik",["Read Ghost Chat"]="Szellem chat olvasás",["Extended Chat"]="Bővített chat",["Fast Chat"]="Gyors chat",["Unlock Extra Characters"]="Extra karakterek engedélyezése",["Spell Check"]="Helyesírás",["Clipboard"]="Vágólap",["Save Chat Log"]="Chat log mentése",["Dark Chat Theme"]="Sötét chat téma",["Enable /color"]="/color bekapcsolása",["Block Fortegreen"]="Fortegreen tiltása",["Allow Duplicate Colors"]="Dupla színek engedése",["Auto Ghost After Start"]="Auto szellem start után",["FAVORITE OUTFITS"]="KEDVENC RUHÁK",["Slot"]="Slot",["Empty"]="Üres",["Apply"]="Alkalmaz",["Save Mine"]="Saját mentése",["Save Selected"]="Kiválasztott mentése",["Saved slot"]="Slot mentve",["Applied slot"]="Slot alkalmazva",["Cleared slot"]="Slot törölve",["Auto-Ban Platform Spoof (Host)"]="Platform spoof auto-ban",["Ban Custom Platforms From TXT"]="Platform ban TXT-ből",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC limit:",["RPC Local Drop"]="RPC helyi drop",["RPC Host Ban"]="RPC host ban" }, + ["sv"] = new Dictionary { ["RESET SETTINGS"]="ÅTERSTÄLL",["Resets all sliders back to their default values."]="Återställer alla reglage till standardvärden.",["Reset Sliders to Default"]="Återställ reglage",["All sliders & toggles reset to default."]="Alla reglage och knappar återställda.",["RAINBOW — FREE COLORS (NO HOST)"]="REGNBÅGE — LEDIGA FÄRGER (INGEN HOST)",["Rainbow (only free colors)"]="Regnbåge (endast lediga färger)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Växlar din färg endast bland lediga färger i rummet, så att hostens anti-cheat inte bannar dig för en upptagen färg.",["Pick free color:"]="Välj ledig färg:",["none"]="ingen",["Applied free color: "]="Ledig färg använd: ",["Free colors right now: "]="Lediga färger nu: ",["GENERAL"]="ALLMÄNT",["SELF"]="SPELARE",["VISUALS"]="VISUELLT",["PLAYERS"]="SPELARE",["SABOTAGES"]="SABOTAGE",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENY",["MAPS"]="KARTOR",["ANIMATIONS"]="ANIMATIONER",["INFORMATION"]="INFO",["KEYBINDS"]="TANGENTER",["WELCOME"]="VÄLKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menyspråk:",["FPS Limit"]="FPS-gräns",["Chat History"]="Chatthistorik",["History:"]="Historik:",["History size:"]="Historikstorlek:",["CHAT UTILITY"]="CHATTVERKTYG",["Always Show Chat"]="Visa alltid chatt",["Read Ghost Chat"]="Läs spökchatt",["Extended Chat"]="Utökad chatt",["Fast Chat"]="Snabb chatt",["Unlock Extra Characters"]="Tillåt extra tecken",["Spell Check"]="Stavning",["Clipboard"]="Urklipp",["Save Chat Log"]="Spara chattlogg",["Dark Chat Theme"]="Mörkt chatt-tema",["Enable /color"]="Aktivera /color",["Block Fortegreen"]="Blockera Fortegreen",["Allow Duplicate Colors"]="Tillåt dubbla färger",["Auto Ghost After Start"]="Auto-spöke efter start",["FAVORITE OUTFITS"]="FAVORITOUTFITS",["Slot"]="Slot",["Empty"]="Tom",["Apply"]="Använd",["Save Mine"]="Spara min",["Save Selected"]="Spara vald",["Saved slot"]="Slot sparad",["Applied slot"]="Slot använd",["Cleared slot"]="Slot rensad",["Auto-Ban Platform Spoof (Host)"]="Auto-ban plattformsspoof",["Ban Custom Platforms From TXT"]="Ban plattformar från TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-gräns:",["RPC Local Drop"]="RPC lokal drop",["RPC Host Ban"]="RPC host-ban" }, + ["da"] = new Dictionary { ["RESET SETTINGS"]="NULSTIL",["Resets all sliders back to their default values."]="Nulstiller alle skydere til deres standardværdier.",["Reset Sliders to Default"]="Nulstil skydere",["All sliders & toggles reset to default."]="Alle skydere og knapper nulstillet.",["RAINBOW — FREE COLORS (NO HOST)"]="REGNBUE — LEDIGE FARVER (INGEN HOST)",["Rainbow (only free colors)"]="Regnbue (kun ledige farver)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Skifter kun din farve blandt ledige farver i rummet, så hostens anti-cheat ikke banner dig for en optaget farve.",["Pick free color:"]="Vælg ledig farve:",["none"]="ingen",["Applied free color: "]="Ledig farve anvendt: ",["Free colors right now: "]="Ledige farver nu: ",["GENERAL"]="GENERELT",["SELF"]="SPILLER",["VISUALS"]="VISUELT",["PLAYERS"]="SPILLERE",["SABOTAGES"]="SABOTAGER",["HOST ONLY"]="HOST",["OUTFITS"]="OUTFITS",["VOTEKICK"]="VOTEKICK",["MENU"]="MENU",["MAPS"]="BANER",["ANIMATIONS"]="ANIMATIONER",["INFORMATION"]="INFO",["KEYBINDS"]="TASTER",["WELCOME"]="VELKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menusprog:",["FPS Limit"]="FPS-grænse",["Chat History"]="Chathistorik",["History:"]="Historik:",["History size:"]="Historikstørrelse:",["CHAT UTILITY"]="CHATVÆRKTØJ",["Always Show Chat"]="Vis altid chat",["Read Ghost Chat"]="Læs spøgelses-chat",["Extended Chat"]="Udvidet chat",["Fast Chat"]="Hurtig chat",["Unlock Extra Characters"]="Tillad ekstra tegn",["Spell Check"]="Stavekontrol",["Clipboard"]="Udklipsholder",["Save Chat Log"]="Gem chatlog",["Dark Chat Theme"]="Mørkt chattema",["Enable /color"]="Aktiver /color",["Block Fortegreen"]="Bloker Fortegreen",["Allow Duplicate Colors"]="Tillad dubletfarver",["Auto Ghost After Start"]="Auto-spøgelse efter start",["FAVORITE OUTFITS"]="FAVORITOUTFITS",["Slot"]="Slot",["Empty"]="Tom",["Apply"]="Anvend",["Save Mine"]="Gem min",["Save Selected"]="Gem valgt",["Saved slot"]="Slot gemt",["Applied slot"]="Slot anvendt",["Cleared slot"]="Slot ryddet",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban platforme fra TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-grænse:",["RPC Local Drop"]="RPC lokal drop",["RPC Host Ban"]="RPC host-ban" }, + ["fi"] = new Dictionary { ["RESET SETTINGS"]="NOLLAA",["Resets all sliders back to their default values."]="Palauttaa kaikki liukusäätimet oletusarvoihin.",["Reset Sliders to Default"]="Nollaa liukusäätimet",["All sliders & toggles reset to default."]="Kaikki liukusäätimet ja kytkimet nollattu.",["RAINBOW — FREE COLORS (NO HOST)"]="SATEENKAARI — VAPAAT VÄRIT (EI HOST)",["Rainbow (only free colors)"]="Sateenkaari (vain vapaat värit)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Vaihtaa väriäsi vain huoneen vapaiden värien välillä, jotta hostin anti-cheat ei banaa sinua varatusta väristä.",["Pick free color:"]="Valitse vapaa väri:",["none"]="ei mitään",["Applied free color: "]="Vapaa väri käytetty: ",["Free colors right now: "]="Vapaat värit nyt: ",["GENERAL"]="YLEISET",["SELF"]="PELAAJA",["VISUALS"]="VISUAALIT",["PLAYERS"]="PELAAJAT",["SABOTAGES"]="SABOTAASIT",["HOST ONLY"]="HOST",["OUTFITS"]="ASUT",["VOTEKICK"]="VOTEKICK",["MENU"]="VALIKKO",["MAPS"]="KARTAT",["ANIMATIONS"]="ANIMAATIOT",["INFORMATION"]="TIEDOT",["KEYBINDS"]="NÄPPÄIMET",["WELCOME"]="TERVETULOA",["CREDITS"]="TEKIJÄT",["Menu language:"]="Valikon kieli:",["FPS Limit"]="FPS-raja",["Chat History"]="Chat-historia",["History:"]="Historia:",["History size:"]="Historian koko:",["CHAT UTILITY"]="CHAT-TYÖKALUT",["Always Show Chat"]="Näytä chat aina",["Read Ghost Chat"]="Lue haamuchat",["Extended Chat"]="Laajennettu chat",["Fast Chat"]="Nopea chat",["Unlock Extra Characters"]="Salli lisämerkit",["Spell Check"]="Oikoluku",["Clipboard"]="Leikepöytä",["Save Chat Log"]="Tallenna chat-loki",["Dark Chat Theme"]="Tumma chat-teema",["Enable /color"]="Ota /color käyttöön",["Block Fortegreen"]="Estä Fortegreen",["Allow Duplicate Colors"]="Salli samat värit",["Auto Ghost After Start"]="Auto-haamu alun jälkeen",["FAVORITE OUTFITS"]="SUOSIKKIASUT",["Slot"]="Paikka",["Empty"]="Tyhjä",["Apply"]="Käytä",["Save Mine"]="Tallenna oma",["Save Selected"]="Tallenna valittu",["Saved slot"]="Paikka tallennettu",["Applied slot"]="Paikka käytetty",["Cleared slot"]="Paikka tyhjennetty",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban alustat TXT:stä",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-raja:",["RPC Local Drop"]="RPC paikallinen drop",["RPC Host Ban"]="RPC host-ban" }, + ["no"] = new Dictionary { ["RESET SETTINGS"]="TILBAKESTILL",["Resets all sliders back to their default values."]="Tilbakestiller alle glidebrytere til standardverdiene.",["Reset Sliders to Default"]="Tilbakestill glidebrytere",["All sliders & toggles reset to default."]="Alle glidebrytere og brytere tilbakestilt.",["RAINBOW — FREE COLORS (NO HOST)"]="REGNBUE — LEDIGE FARGER (INGEN HOST)",["Rainbow (only free colors)"]="Regnbue (kun ledige farger)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Bytter fargen din kun blant ledige farger i rommet, så hostens anti-cheat ikke banner deg for en opptatt farge.",["Pick free color:"]="Velg ledig farge:",["none"]="ingen",["Applied free color: "]="Ledig farge brukt: ",["Free colors right now: "]="Ledige farger nå: ",["GENERAL"]="GENERELT",["SELF"]="SPILLER",["VISUALS"]="VISUELT",["PLAYERS"]="SPILLERE",["SABOTAGES"]="SABOTASJER",["HOST ONLY"]="HOST",["OUTFITS"]="ANTREKK",["VOTEKICK"]="VOTEKICK",["MENU"]="MENY",["MAPS"]="KART",["ANIMATIONS"]="ANIMASJONER",["INFORMATION"]="INFO",["KEYBINDS"]="TASTER",["WELCOME"]="VELKOMMEN",["CREDITS"]="CREDITS",["Menu language:"]="Menyspråk:",["FPS Limit"]="FPS-grense",["Chat History"]="Chat-historikk",["History:"]="Historikk:",["History size:"]="Historikkstørrelse:",["CHAT UTILITY"]="CHAT-VERKTØY",["Always Show Chat"]="Vis alltid chat",["Read Ghost Chat"]="Les spøkelseschat",["Extended Chat"]="Utvidet chat",["Fast Chat"]="Rask chat",["Unlock Extra Characters"]="Tillat ekstra tegn",["Spell Check"]="Stavekontroll",["Clipboard"]="Utklippstavle",["Save Chat Log"]="Lagre chatlogg",["Dark Chat Theme"]="Mørkt chattema",["Enable /color"]="Aktiver /color",["Block Fortegreen"]="Blokker Fortegreen",["Allow Duplicate Colors"]="Tillat like farger",["Auto Ghost After Start"]="Auto-spøkelse etter start",["FAVORITE OUTFITS"]="FAVORITTANTREKK",["Slot"]="Slot",["Empty"]="Tom",["Apply"]="Bruk",["Save Mine"]="Lagre min",["Save Selected"]="Lagre valgt",["Saved slot"]="Slot lagret",["Applied slot"]="Slot brukt",["Cleared slot"]="Slot tømt",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban plattformer fra TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="RPC-grense:",["RPC Local Drop"]="RPC lokal drop",["RPC Host Ban"]="RPC host-ban" }, + ["uk"] = new Dictionary { ["RESET SETTINGS"]="СКИНУТИ НАЛАШТУВАННЯ",["Resets all sliders back to their default values."]="Скидає всі повзунки до значень за замовчуванням.",["Reset Sliders to Default"]="Скинути повзунки",["All sliders & toggles reset to default."]="Усі повзунки та перемикачі скинуто.",["RAINBOW — FREE COLORS (NO HOST)"]="РАЙДУГА — ВІЛЬНІ КОЛЬОРИ (БЕЗ ХОСТА)",["Rainbow (only free colors)"]="Райдуга (лише вільні кольори)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Перемикає ваш колір лише серед вільних кольорів у кімнаті, щоб античит хоста не забанив за зайнятий колір.",["Pick free color:"]="Вибрати вільний колір:",["none"]="немає",["Applied free color: "]="Застосовано вільний колір: ",["Free colors right now: "]="Вільних кольорів зараз: ",["GENERAL"]="ЗАГАЛЬНЕ",["SELF"]="ГРАВЕЦЬ",["VISUALS"]="ВІЗУАЛ",["PLAYERS"]="ГРАВЦІ",["SABOTAGES"]="САБОТАЖІ",["HOST ONLY"]="ХОСТ",["OUTFITS"]="ОДЯГ",["VOTEKICK"]="КІК",["MENU"]="МЕНЮ",["MAPS"]="КАРТИ",["ANIMATIONS"]="АНІМАЦІЇ",["INFORMATION"]="ІНФОРМАЦІЯ",["KEYBINDS"]="БІНДИ",["WELCOME"]="ВІТАННЯ",["CREDITS"]="АВТОРИ",["Menu language:"]="Мова меню:",["FPS Limit"]="Ліміт FPS",["Chat History"]="Історія чату",["History:"]="Історія:",["History size:"]="Розмір історії:",["CHAT UTILITY"]="УТИЛІТИ ЧАТУ",["Always Show Chat"]="Завжди показувати чат",["Read Ghost Chat"]="Читати чат привидів",["Extended Chat"]="Розширений чат",["Fast Chat"]="Швидкий чат",["Unlock Extra Characters"]="Дозволити всі символи",["Spell Check"]="Перевірка орфографії",["Clipboard"]="Буфер обміну",["Save Chat Log"]="Зберігати лог чату",["Dark Chat Theme"]="Темна тема чату",["Enable /color"]="Увімкнути /color",["Block Fortegreen"]="Блок Fortegreen",["Allow Duplicate Colors"]="Дозволити однакові кольори",["Auto Ghost After Start"]="Авто-привид після старту",["FAVORITE OUTFITS"]="УЛЮБЛЕНІ ОБРАЗИ",["Slot"]="Слот",["Empty"]="Пусто",["Apply"]="Надіти",["Save Mine"]="Зберегти мій",["Save Selected"]="Зберегти вибраний",["Saved slot"]="Слот збережено",["Applied slot"]="Слот застосовано",["Cleared slot"]="Слот очищено",["Auto-Ban Platform Spoof (Host)"]="Авто-бан Platform Spoof",["Ban Custom Platforms From TXT"]="Бан платформ з TXT",["RPC Anti-Cheat"]="RPC античит",["RPC limit:"]="RPC ліміт:",["RPC Local Drop"]="RPC локальний дроп",["RPC Host Ban"]="RPC бан хоста" }, + ["el"] = new Dictionary { ["RESET SETTINGS"]="ΕΠΑΝΑΦΟΡΑ",["Resets all sliders back to their default values."]="Επαναφέρει όλα τα ρυθμιστικά στις προεπιλεγμένες τιμές.",["Reset Sliders to Default"]="Επαναφορά ρυθμιστικών",["All sliders & toggles reset to default."]="Όλα τα ρυθμιστικά και οι διακόπτες επαναφέρθηκαν.",["RAINBOW — FREE COLORS (NO HOST)"]="ΟΥΡΑΝΙΟ ΤΟΞΟ — ΕΛΕΥΘΕΡΑ ΧΡΩΜΑΤΑ (ΧΩΡΙΣ HOST)",["Rainbow (only free colors)"]="Ουράνιο τόξο (μόνο ελεύθερα χρώματα)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="Αλλάζει το χρώμα σου μόνο ανάμεσα στα ελεύθερα χρώματα του δωματίου, ώστε το anti-cheat του host να μη σε μπανάρει για πιασμένο χρώμα.",["Pick free color:"]="Επίλεξε ελεύθερο χρώμα:",["none"]="κανένα",["Applied free color: "]="Εφαρμόστηκε ελεύθερο χρώμα: ",["Free colors right now: "]="Ελεύθερα χρώματα τώρα: ",["GENERAL"]="ΓΕΝΙΚΑ",["SELF"]="ΠΑΙΚΤΗΣ",["VISUALS"]="ΟΠΤΙΚΑ",["PLAYERS"]="ΠΑΙΚΤΕΣ",["SABOTAGES"]="ΣΑΜΠΟΤΑΖ",["HOST ONLY"]="HOST",["OUTFITS"]="ΣΤΟΛΕΣ",["VOTEKICK"]="VOTEKICK",["MENU"]="ΜΕΝΟΥ",["MAPS"]="ΧΑΡΤΕΣ",["ANIMATIONS"]="ANIMATIONS",["INFORMATION"]="ΠΛΗΡΟΦΟΡΙΕΣ",["KEYBINDS"]="ΠΛΗΚΤΡΑ",["WELCOME"]="ΚΑΛΩΣ ΗΡΘΕΣ",["CREDITS"]="CREDITS",["Menu language:"]="Γλώσσα μενού:",["FPS Limit"]="Όριο FPS",["Chat History"]="Ιστορικό chat",["History:"]="Ιστορικό:",["History size:"]="Μέγεθος ιστορικού:",["CHAT UTILITY"]="ΕΡΓΑΛΕΙΑ CHAT",["Always Show Chat"]="Πάντα εμφάνιση chat",["Read Ghost Chat"]="Ανάγνωση ghost chat",["Extended Chat"]="Εκτεταμένο chat",["Fast Chat"]="Γρήγορο chat",["Unlock Extra Characters"]="Επιπλέον χαρακτήρες",["Spell Check"]="Ορθογραφία",["Clipboard"]="Πρόχειρο",["Save Chat Log"]="Αποθήκευση chat log",["Dark Chat Theme"]="Σκούρο chat",["Enable /color"]="Ενεργοποίηση /color",["Block Fortegreen"]="Block Fortegreen",["Allow Duplicate Colors"]="Να επιτρέπονται ίδια χρώματα",["Auto Ghost After Start"]="Auto ghost μετά την έναρξη",["FAVORITE OUTFITS"]="ΑΓΑΠΗΜΕΝΕΣ ΣΤΟΛΕΣ",["Slot"]="Θέση",["Empty"]="Άδειο",["Apply"]="Εφαρμογή",["Save Mine"]="Αποθ. δικό μου",["Save Selected"]="Αποθ. επιλογής",["Saved slot"]="Θέση αποθηκεύτηκε",["Applied slot"]="Θέση εφαρμόστηκε",["Cleared slot"]="Θέση καθαρίστηκε",["Auto-Ban Platform Spoof (Host)"]="Auto-ban platform spoof",["Ban Custom Platforms From TXT"]="Ban platforms από TXT",["RPC Anti-Cheat"]="RPC anti-cheat",["RPC limit:"]="Όριο RPC:",["RPC Local Drop"]="RPC local drop",["RPC Host Ban"]="RPC host ban" }, + ["zh"] = new Dictionary { ["RESET SETTINGS"]="重置设置",["Resets all sliders back to their default values."]="将所有滑块重置为默认值。",["Reset Sliders to Default"]="重置滑块",["All sliders & toggles reset to default."]="所有滑块和开关已重置。",["RAINBOW — FREE COLORS (NO HOST)"]="彩虹 — 空闲颜色 (非房主)",["Rainbow (only free colors)"]="彩虹 (仅空闲颜色)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="仅在房间内空闲的颜色之间循环你的颜色,这样房主的反作弊就不会因抢占已用颜色而封��你。",["Pick free color:"]="选择空闲颜色:",["none"]="无",["Applied free color: "]="已应用空闲颜色: ",["Free colors right now: "]="当前空闲颜色: ",["GENERAL"]="常规",["SELF"]="玩家",["VISUALS"]="视觉",["PLAYERS"]="玩家",["SABOTAGES"]="破坏",["HOST ONLY"]="房主",["OUTFITS"]="装扮",["VOTEKICK"]="投票踢人",["MENU"]="菜单",["MAPS"]="地图",["ANIMATIONS"]="动画",["INFORMATION"]="信息",["KEYBINDS"]="按键",["WELCOME"]="欢迎",["CREDITS"]="鸣谢",["Menu language:"]="菜单语言:",["FPS Limit"]="FPS 限制",["Chat History"]="聊天历史",["History:"]="历史:",["History size:"]="历史大小:",["CHAT UTILITY"]="聊天工具",["Always Show Chat"]="始终显示聊天",["Read Ghost Chat"]="读取幽灵聊天",["Extended Chat"]="扩展聊天",["Fast Chat"]="快速聊天",["Unlock Extra Characters"]="允许额外字符",["Spell Check"]="拼写检查",["Clipboard"]="剪贴板",["Save Chat Log"]="保存聊天日志",["Dark Chat Theme"]="深色聊天主题",["Enable /color"]="启用 /color",["Block Fortegreen"]="阻止 Fortegreen",["Allow Duplicate Colors"]="允许重复颜色",["Auto Ghost After Start"]="开始后自动幽灵",["FAVORITE OUTFITS"]="收藏装扮",["Slot"]="槽位",["Empty"]="空",["Apply"]="应用",["Save Mine"]="保存自己",["Save Selected"]="保存选中",["Saved slot"]="槽位已保存",["Applied slot"]="槽位已应用",["Cleared slot"]="槽位已清空",["Auto-Ban Platform Spoof (Host)"]="自动封禁平台伪装",["Ban Custom Platforms From TXT"]="从 TXT 封禁自定义平台",["RPC Anti-Cheat"]="RPC 反作弊",["RPC limit:"]="RPC 限制:",["RPC Local Drop"]="RPC 本地丢弃",["RPC Host Ban"]="RPC 房主封禁" }, + ["ja"] = new Dictionary { ["RESET SETTINGS"]="設定をリセット",["Resets all sliders back to their default values."]="すべてのスライダーを既定値に戻します。",["Reset Sliders to Default"]="スライダーをリセット",["All sliders & toggles reset to default."]="すべてのスライダーとスイッチをリセットしました。",["RAINBOW — FREE COLORS (NO HOST)"]="レインボー — 空き色 (ホスト以外)",["Rainbow (only free colors)"]="レインボー (空き色のみ)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="部屋で空いている色だけで色を循環させ、ホストのアンチチートが使用中の色の奪用でBANしないようにします。",["Pick free color:"]="空き色を選択:",["none"]="なし",["Applied free color: "]="空き色を適用: ",["Free colors right now: "]="現在の空き色: ",["GENERAL"]="一般",["SELF"]="プレイヤー",["VISUALS"]="表示",["PLAYERS"]="プレイヤー",["SABOTAGES"]="サボタージュ",["HOST ONLY"]="ホスト",["OUTFITS"]="衣装",["VOTEKICK"]="投票キック",["MENU"]="メニュー",["MAPS"]="マップ",["ANIMATIONS"]="アニメーション",["INFORMATION"]="情報",["KEYBINDS"]="キー設定",["WELCOME"]="ようこそ",["CREDITS"]="クレジット",["Menu language:"]="メニュー言語:",["FPS Limit"]="FPS制限",["Chat History"]="チャット履歴",["History:"]="履歴:",["History size:"]="履歴サイズ:",["CHAT UTILITY"]="チャット機能",["Always Show Chat"]="常にチャット表示",["Read Ghost Chat"]="ゴーストチャット読む",["Extended Chat"]="拡張チャット",["Fast Chat"]="高速チャット",["Unlock Extra Characters"]="追加文字を許可",["Spell Check"]="スペルチェック",["Clipboard"]="クリップボード",["Save Chat Log"]="チャットログ保存",["Dark Chat Theme"]="ダークチャット",["Enable /color"]="/color 有効",["Block Fortegreen"]="Fortegreen ブロック",["Allow Duplicate Colors"]="同じ色を許可",["Auto Ghost After Start"]="開始後に自動ゴースト",["FAVORITE OUTFITS"]="お気に入り衣装",["Slot"]="スロット",["Empty"]="空",["Apply"]="適用",["Save Mine"]="自分を保存",["Save Selected"]="選択を保存",["Saved slot"]="スロット保存",["Applied slot"]="スロット適用",["Cleared slot"]="スロット消去",["Auto-Ban Platform Spoof (Host)"]="平台偽装を自動BAN",["Ban Custom Platforms From TXT"]="TXTから平台BAN",["RPC Anti-Cheat"]="RPCアンチチート",["RPC limit:"]="RPC制限:",["RPC Local Drop"]="RPCローカル破棄",["RPC Host Ban"]="RPCホストBAN" }, + ["ko"] = new Dictionary { ["RESET SETTINGS"]="설정 초기화",["Resets all sliders back to their default values."]="모든 슬라이더를 기본값으로 되돌립니다.",["Reset Sliders to Default"]="슬라이더 초기화",["All sliders & toggles reset to default."]="모든 슬라이더와 토글이 초기화되었습니다.",["RAINBOW — FREE COLORS (NO HOST)"]="무지개 — 빈 색상 (호스트 아님)",["Rainbow (only free colors)"]="무지개 (빈 색상만)",["Cycles your color only through colors that are free in the room, so the host's anti-cheat won't ban you for stealing a taken color."]="방에서 비어 있는 색상만으로 색을 순환시켜, 호스트의 안티치트가 사용 중인 색상 도용으로 밴하지 않도록 합니다.",["Pick free color:"]="빈 색상 선택:",["none"]="없음",["Applied free color: "]="빈 색상 적용됨: ",["Free colors right now: "]="현재 빈 색상: ",["GENERAL"]="일반",["SELF"]="플레이어",["VISUALS"]="비주얼",["PLAYERS"]="플레이어",["SABOTAGES"]="사보타주",["HOST ONLY"]="호스트",["OUTFITS"]="의상",["VOTEKICK"]="투표킥",["MENU"]="메뉴",["MAPS"]="맵",["ANIMATIONS"]="애니메이션",["INFORMATION"]="정보",["KEYBINDS"]="키 설정",["WELCOME"]="환영",["CREDITS"]="크레딧",["Menu language:"]="메뉴 언어:",["FPS Limit"]="FPS 제한",["Chat History"]="채팅 기록",["History:"]="기록:",["History size:"]="기록 크기:",["CHAT UTILITY"]="채팅 도구",["Always Show Chat"]="항상 채팅 표시",["Read Ghost Chat"]="유령 채팅 읽기",["Extended Chat"]="확장 채팅",["Fast Chat"]="빠른 채팅",["Unlock Extra Characters"]="추가 문자 허용",["Spell Check"]="맞춤법 검사",["Clipboard"]="클립보드",["Save Chat Log"]="채팅 로그 저장",["Dark Chat Theme"]="어두운 채팅 테마",["Enable /color"]="/color 활성화",["Block Fortegreen"]="Fortegreen 차단",["Allow Duplicate Colors"]="중복 색상 허용",["Auto Ghost After Start"]="시작 후 자동 유령",["FAVORITE OUTFITS"]="즐겨찾기 의상",["Slot"]="슬롯",["Empty"]="비어 있음",["Apply"]="적용",["Save Mine"]="내 것 저장",["Save Selected"]="선택 저장",["Saved slot"]="슬롯 저장됨",["Applied slot"]="슬롯 적용됨",["Cleared slot"]="슬롯 삭제됨",["Auto-Ban Platform Spoof (Host)"]="플랫폼 위장 자동 밴",["Ban Custom Platforms From TXT"]="TXT 커스텀 플랫폼 밴",["RPC Anti-Cheat"]="RPC 안티치트",["RPC limit:"]="RPC 제한:",["RPC Local Drop"]="RPC 로컬 드롭",["RPC Host Ban"]="RPC 호스트 밴" } + }; +} +} diff --git a/ui/PlayerAndSettingsState.cs b/ui/PlayerAndSettingsState.cs new file mode 100644 index 0000000..7e46461 --- /dev/null +++ b/ui/PlayerAndSettingsState.cs @@ -0,0 +1,773 @@ +#nullable disable +#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 +using AmongUs.Data.Player; +using AmongUs.GameOptions; +using AmongUs.InnerNet.GameDataMessages; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP.Utils; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using ElysiumModMenu; +using HarmonyLib; +using Hazel; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using RewiredConsts; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Events; +using UnityEngine.Playables; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UI; +using static ElysiumModMenu.ElysiumModMenuGUI; +using static Rewired.UI.ControlMapper.ControlMapper; +using Color = UnityEngine.Color; +using Object = UnityEngine.Object; +using Vector3 = UnityEngine.Vector3; + +namespace ElysiumModMenu +{ + public partial class ElysiumModMenuGUI : MonoBehaviour + { +private string[] visualsSubTabs => new string[] { L("IN-GAME", "В ИГРЕ"), L("OUTFITS", "ОДЕЖДА") }; + +private int currentSelfSubTab = 0; + +private string[] selfSubTabs = { "SPOOF", "MOVEMENT", "ROLES", "CHAT" }; + +private string[] selfOtherTabs = { "MOVEMENT", "ROLES", "CHAT" }; + +public static bool fakeStartCounterTroll = false; + +public static bool fakeStartCounterCustom = false; + +public static string fakeStartInput = "69"; + +public static bool isEditingFakeStart = false; + +public static float customStartTimer = -1f; + +public static bool localRainbow = false; + +public static List rainbowPlayers = new List(); + +public static float colorTimer = 0f; + +public static byte currentColorId = 0; + +public static bool lobbyRainbowAll = false; + +public static bool lobbyAllColor = false; + +public static int lobbyAllColorId = 0; + +public static bool localRainbowFreeOnly = false; + +public static float freeColorTimer = 0f; + +public static int freeRainbowIndex = 0; + +public static int selectedFreeColorIndex = 0; + +private Vector2 playerListScrollPos = Vector2.zero; + +private Vector2 playerActionScrollPos = Vector2.zero; + +private byte selectedAntiCheatPlayerId = 255; + +public static string spoofLevelString = "100"; + +public static bool enableLevelSpoof = true; + +private static bool hasLevelSpoofRestoreLevel = false; + +private static uint levelSpoofRestoreLevel = 0; + +public static string customNameInput = "хыхых"; + +public static string spoofFriendCodeInput = "crewmate01"; + +public static string localFriendCodeInput = "Steam#Local"; + +public static string ghostChatColorHex = "#D7B8FF"; + +public static bool isEditingLevel = false; + +public static bool isEditingName = false; + +public static bool isEditingFriendCode = false; + +public static bool isEditingLocalFriendCode = false; + +public static bool isEditingGhostChatColor = false; + +private static bool discordLaunchStatusSent = false; + +private static bool discordInvalidWebhookNotified = false; + +private static float discordLaunchStatusNextTryAt = 0f; + +private static readonly string relaySessionId = Guid.NewGuid().ToString("N").Substring(0, 12); + +private static readonly Dictionary watchedLogLineCounts = new Dictionary(); + +private static readonly DateTime logMonitorStartedUtc = DateTime.UtcNow; + +private static readonly object anomalyLogMonitorLock = new object(); + +private static System.Threading.Timer anomalyLogMonitorTimer; + +private static string anomalyReportDetailsCache = $"sessionId={relaySessionId}\nclientId=Unknown\nnetworkMode=Unknown\nhost=Unknown\nplatform=Unknown\ninGame=Unknown"; + +private static float logMonitorNextScanAt = 0f; + +private static float logBurstWindowStartedAt = -1f; + +private static float logBurstCooldownUntil = 0f; + +private static int logBurstLineCount = 0; + +private static bool anomalyLogWatchNotified = false; + +private const int LogBurstLineThreshold = 15; + +private const int InitialLogTailLineLimit = 120; + +private const float LogBurstWindowSeconds = 5f; + +private const float LogBurstScanIntervalSeconds = 1f; + +private const float LogBurstAlertCooldownSeconds = 60f; + +public static bool enableLocalNameSpoof = false; + +public static bool enableLocalFriendCodeSpoof = false; + +public static bool enableFriendCodeSpoof = false; + +public static bool enablePlatformSpoof = true; + +public static bool enableAnomalyLogReports = true; + +public static bool showEspFriendCode = true; + +public static bool allowDuplicateColors = false; + +public static bool autoGhostAfterStart = false; + +public static bool autoBanPlatformSpoof = false; + +public static bool banCustomPlatformsFromTxt = false; + +public static bool autoKickLowLevelEnabled = false; + +public static int autoKickMinLevel = 200; + +public static int fpsLimit = 60; + +public static int chatHistoryLimit = 20; + +public static int currentPlatformIndex = 1; + +private static float localNameRefreshTimer = 0f; + +private static float localFriendCodeRefreshTimer = 0f; + +private static float platformBanScanTimer = 0f; + +private static int lastAppliedFpsLimit = -1; + +private static bool autoGhostAppliedThisGame = false; + +private static bool wasGameStartedForAutoGhost = false; + +private static string originalLocalFriendCode = null; + +private static string originalLocalName = null; + +private static float friendEspIgnoreNextLoadAt = 0f; + +private static readonly HashSet friendEspIgnoreTokens = new HashSet(StringComparer.OrdinalIgnoreCase); + +private static string platformBanListPath = ""; + +private static float platformBanListNextLoadAt = 0f; + +private static readonly HashSet customPlatformBanTokens = new HashSet(StringComparer.OrdinalIgnoreCase); + +private static readonly HashSet platformSpoofPunishedOwners = new HashSet(); + +private float lowLevelKickScanTimer = 0f; + +private static readonly HashSet lowLevelKickPunishedOwners = new HashSet(); + +private const int FavoriteOutfitSlotCount = 4; + +private static readonly string[] favoriteOutfitSlots = new string[FavoriteOutfitSlotCount]; + +private float brokenFcScanTimer = 0f; + +private static readonly HashSet brokenFcPunishedOwners = new HashSet(); + +public static string[] platformNames = { + "Epic", "Steam", "Mac", "Microsoft", "Itch", "iOS", + "Android", "Switch", "Xbox", "PlayStation", "Starlight" + }; + +public static Platforms[] platformValues = { + (Platforms)1, + (Platforms)2, + (Platforms)3, + (Platforms)4, + (Platforms)5, + (Platforms)6, + (Platforms)7, + (Platforms)8, + (Platforms)9, + (Platforms)10, + (Platforms)112 + }; + +public static bool unlockFeatures = true; + +public class ElysiumNotification + { + public string title; + public string message; + public float ttl; + public float lifetime; + public bool HasExpired => lifetime > ttl; + + public ElysiumNotification(string title, string message, float ttl) + { + this.title = title; + this.message = message; + this.ttl = ttl; + this.lifetime = 0f; + } + } + +public static List bannedEntries = new List(); + +public static string banListPath = ""; + +private Vector2 banListScroll = Vector2.zero; + +public static bool autoBanEnabled = true; + +public static string banInput = ""; + +public static bool isEditingBan = false; + +public static List botBannedEntries = new List(); + +public static string botBanListPath = ""; + +public static bool banBotsEnabled = false; + +public static bool oldAntiCheatVersion = false; + +public static readonly string[] botNameTokens = new string[] { "UCbot", "bot", "бот", "Ucбот", "sixseven", "лут", "67" }; + +public static void LoadBanList() + { + try + { + banListPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ElysiumModMenuBanList.txt"); + if (!System.IO.File.Exists(banListPath)) + { + System.IO.File.Create(banListPath).Dispose(); + } + bannedEntries = new List(System.IO.File.ReadAllLines(banListPath)); + } + catch { } + } + +public static void AddToBanList(string friendCode, string puid, string name, string reason) + { + try + { + if (string.IsNullOrEmpty(friendCode)) return; + + bool alreadyBanned = false; + string fcLower = friendCode.Trim().ToLower(); + + foreach (var e in bannedEntries) + { + string[] parts = e.Split('|'); + if (parts.Length > 0 && parts[0].Trim().ToLower() == fcLower) + { + alreadyBanned = true; + break; + } + } + + if (!alreadyBanned) + { + string date = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); + string entry = $"{friendCode}|{puid}|{name}|{date}|{reason}"; + bannedEntries.Add(entry); + System.IO.File.AppendAllText(banListPath, entry + Environment.NewLine); + } + } + catch { } + } + +public static void RemoveFromBanList(string entry) + { + try + { + bannedEntries.Remove(entry); + System.IO.File.WriteAllLines(banListPath, bannedEntries.ToArray()); + } + catch { } + } + +public static void LoadBotBanList() + { + try + { + botBanListPath = System.IO.Path.Combine(Plugin.ElysiumFolder, "ElysiumBotBanList.txt"); + if (!System.IO.File.Exists(botBanListPath)) + { + System.IO.File.Create(botBanListPath).Dispose(); + } + botBannedEntries = new List(System.IO.File.ReadAllLines(botBanListPath)); + } + catch { } + } + +public static void AddToBotBanList(string friendCode, string puid, string name, string reason) + { + try + { + string fc = string.IsNullOrWhiteSpace(friendCode) ? "Unknown" : friendCode.Trim(); + string nm = string.IsNullOrWhiteSpace(name) ? "Unknown" : name.Trim(); + string fcLower = fc.ToLower(); + string nameLower = nm.ToLower(); + + bool already = false; + foreach (var e in botBannedEntries) + { + if (string.IsNullOrWhiteSpace(e) || e.TrimStart().StartsWith("#")) continue; + string[] parts = e.Split('|'); + if (parts.Length > 0 && fcLower != "unknown" && parts[0].Trim().ToLower() == fcLower) { already = true; break; } + if (fcLower == "unknown" && parts.Length >= 3 && parts[2].Trim().ToLower() == nameLower) { already = true; break; } + } + + if (!already) + { + if (string.IsNullOrEmpty(botBanListPath)) LoadBotBanList(); + string date = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); + string entry = $"{fc}|{puid}|{nm}|{date}|{reason}"; + botBannedEntries.Add(entry); + System.IO.File.AppendAllText(botBanListPath, entry + Environment.NewLine); + } + } + catch { } + } + +public static bool IsBotName(string name) + { + try + { + if (string.IsNullOrWhiteSpace(name)) return false; + string n = name.Trim().ToLowerInvariant(); + + foreach (var token in botNameTokens) + { + if (string.IsNullOrWhiteSpace(token)) continue; + if (n.Contains(token.Trim().ToLowerInvariant())) return true; + } + + foreach (var e in botBannedEntries) + { + if (string.IsNullOrWhiteSpace(e) || e.TrimStart().StartsWith("#")) continue; + string[] parts = e.Split('|'); + string nick = parts.Length >= 3 ? parts[2].Trim().ToLowerInvariant() : e.Trim().ToLowerInvariant(); + if (!string.IsNullOrWhiteSpace(nick) && nick != "unknown" && n.Contains(nick)) return true; + } + } + catch { } + return false; + } + +public static bool IsBotBannedFc(string fc) + { + try + { + if (string.IsNullOrWhiteSpace(fc)) return false; + string f = fc.Trim().ToLowerInvariant(); + foreach (var e in botBannedEntries) + { + if (string.IsNullOrWhiteSpace(e) || e.TrimStart().StartsWith("#")) continue; + string[] parts = e.Split('|'); + if (parts.Length > 0 && parts[0].Trim().ToLowerInvariant() == f) return true; + } + } + catch { } + return false; + } + +public static bool killReach = false, killAnyone = false; + +public static bool endlessSsDuration = false, noVitalsCooldown = false; + +public static bool endlessBattery = false, endlessVentTime = false, noVentCooldown = false, noMapCooldowns = false; + +public static bool unlockVents = false, walkInVents = false; + +public static bool reactorSab = false, oxygenSab = false, commsSab = false, elecSab = false, unfixableLights = false; + +private static bool unfixableLightsApplied = false; + +public static bool autoOpenDoors = false; + +public static bool moonWalk = false; + +public static bool SeePlayersInVent = false; + +public static bool seeGhosts = false; + +public static bool seeRoles = false; + +public static bool showPlayerInfo = false; + +public static bool revealMeetingRoles = false; + +public static bool showTracers = false; + +public static bool fullBright = false; + +public static bool seeProtections = false; + +public static bool seeKillCooldown = false; + +public static bool extendedLobby = false; + +public static bool DarkModeEnabled = true; + +public static bool enableChatDarkMode = true; + +public static float customLightRadius = 5f; + +private static Dictionary lastKillTimestamps = new Dictionary(); + +public static bool alwaysChat = false; + +public static bool readGhostChat = false; + +public static bool enableSpellCheck = false; + +public static bool neverEndGame = false; + +public static void ShowNotification(string text) + { + string title = "ElysiumModMenu"; + string msg = text; + + if (text.Contains("[") && text.Contains("]")) + { + int start = text.IndexOf("["); + int end = text.IndexOf("]"); + if (end > start) + { + string rawTitle = text.Substring(start + 1, end - start - 1); + title = System.Text.RegularExpressions.Regex.Replace(rawTitle, "<.*?>", string.Empty); + msg = System.Text.RegularExpressions.Regex.Replace(msg, @"(]+>)?\[.*?\]()?\s*", ""); + } + } + SendNotification(title, msg.Trim(), 3.5f); + } + +public static void SendNotification(string title, string message, float ttl = 3.5f) + { + if (!EnableCustomNotifs) return; + screenNotifications.Add(new ElysiumNotification(title, message, ttl)); + } + + private static string GetNotificationTextForTheme(string message) + { + if (!whiteMenuTheme || string.IsNullOrEmpty(message)) + return message; + + return System.Text.RegularExpressions.Regex.Replace(message, @"]+)?>", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase); + } + + + + public static HashSet forcedImpostors = new HashSet(); + +public static Dictionary forcedPreGameRoles = new Dictionary(); + +public static bool enablePreGameRoleForce = false; + +private Vector2 preRolesListScrollPos = Vector2.zero; + +private Vector2 preRolesActionScrollPos = Vector2.zero; + +private byte selectedPreRoleId = 255; + +public static List lockedPlayersList = new List(); + +public static bool LogAllRPCs = true; + +public static bool blockRainbowChat = true; + +public static bool blockFortegreenChat = true; + +public static bool EnableCustomNotifs = true; + +public static Vector2 notificationBoxSize = new Vector2(260f, 65f); + +public static List screenNotifications = new List(); + +private bool stylesInited = false; + +private GUIStyle windowStyle, btnStyle, activeTabStyle, headerStyle, boxStyle; + +private GUIStyle sidebarStyle, sidebarBtnStyle, activeSidebarBtnStyle, titleStyle; + +private GUIStyle toggleOnStyle, toggleOffStyle, toggleLabelStyle, safeLineStyle, trackOnStyle, trackOffStyle, knobStyle; + +private GUIStyle sliderStyle, sliderThumbStyle, subTabStyle, activeSubTabStyle; + +public GUIStyle inputBlockStyle; + +private Texture2D texWindowBg, texBoxBg, texBtnBg, texAccent, texSidebarBg; + +private Texture2D texToggleOff, texToggleOn, texSliderBg, texSliderThumb, texInputBg, texColorBtn, texScrollThumb, texTrackOff, texTrackOn, texKnobWhite, texSwatchSquare; + +private Texture2D texMenuCard; + +private GUIStyle menuCardStyle, menuSectionTitleStyle, menuDescStyle, menuBadgeStyle, menuAccentBarStyle, menuSwatchStyle, menuSwatchSquareStyle; + +private void DrawHostOnlyTab() + { + GUILayout.BeginHorizontal(); + for (int i = 0; i < hostOnlySubTabs.Length; i++) + { + if (GUILayout.Button(hostOnlySubTabs[i], currentHostOnlySubTab == i ? activeSubTabStyle : subTabStyle, GUILayout.Height(18))) + { + currentHostOnlySubTab = i; + scrollPosition = Vector2.zero; + } + } + GUILayout.EndHorizontal(); + GUILayout.Space(8); + + if (currentHostOnlySubTab == 0) DrawLobbyControls(); + else if (currentHostOnlySubTab == 1) DrawPlayersRoles(); + else if (currentHostOnlySubTab == 2) DrawAntiCheatTab(); + else if (currentHostOnlySubTab == 3) DrawAutoHostTab(); + else if (currentHostOnlySubTab == 4) DrawMapsTab(); + } + +private void DrawVisualsInGame() + { + GUILayout.BeginVertical(menuCardStyle); + DrawMenuSectionHeader(L("VISIBILITY", "ВИДИМОСТЬ")); + + GUILayout.BeginHorizontal(); + seeGhosts = DrawToggle(seeGhosts, L("See Ghosts", "Видеть призраков"), 210); + seeRoles = DrawToggle(seeRoles, L("See Roles", "Видеть роли"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + showPlayerInfo = DrawToggle(showPlayerInfo, L("Show Player Info (ESP)", "Инфо об игроке (ESP)"), 210); + revealMeetingRoles = DrawToggle(revealMeetingRoles, L("Reveal Roles (Meeting)", "Показывать роли на собрании"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + showEspFriendCode = DrawToggle(showEspFriendCode, L("Show FC In ESP", "FriendCode в ESP"), 210); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + removePenalty = DrawToggle(removePenalty, L("No Disconnect Penalty", "Нет штрафа за выход"), 210); + alwaysShowLobbyTimer = DrawToggle(alwaysShowLobbyTimer, L("Always Show Lobby Timer", "Всегда показывать таймер лобби"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + showTracers = DrawToggle(showTracers, L("Show Tracers", "Показывать линии (Tracer)"), 210); + fullBright = DrawToggle(fullBright, L("Full Bright (No Shadows)", "Полная яркость (Нет теней)"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + alwaysChat = DrawToggle(alwaysChat, L("Always Show Chat", "Всегда показывать чат"), 210); + readGhostChat = DrawToggle(readGhostChat, L("Read Ghost Chat", "Читать чат призраков"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + freecam = DrawToggle(freecam, L("Freecam (WASD)", "Свободная камера (WASD)"), 210); + cameraZoom = DrawToggle(cameraZoom, L("Camera Zoom (Scroll)", "Зум камеры (Колесико)"), 210); + GUILayout.EndHorizontal(); + GUILayout.Space(5); + + GUILayout.BeginHorizontal(); + RevealVotesEnabled = DrawToggle(RevealVotesEnabled, L("Reveal Votes (Meeting)", "Показывать голоса (Собрание)"), 210); + SeePlayersInVent = DrawToggle(SeePlayersInVent, L("See Players In Vents", "Видеть игроков в люках"), 210); + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + GUILayout.BeginHorizontal(); + seeProtections = DrawToggle(seeProtections, L("See Protections", "Видеть щиты"), 210); + seeKillCooldown = DrawToggle(seeKillCooldown, L("See Kill Cooldown", "Видеть килл-кд"), 210); + GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + } + +public static bool enableLocalPetSpamDrop = true; + +public static bool enableHostPetSpamBan = false; + +public static bool enableMalformedPacketGuard = true; + +public static bool banMalformedPacketSender = false; + +public static bool enableQuickChatEmptyGuard = true; + +public static bool banQuickChatEmptySpammer = true; + +public static bool enableUnownedSpawnGuard = true; + +static class HazelThings + { + static bool isShouldProtect => PlayerControl.LocalPlayer && AmongUsClient.Instance.NetworkMode != NetworkModes.FreePlay; + + static bool isCooling; + static void GetHazelError(string errorType) + { + if (!isCooling) + { + isCooling = true; + DestroyableSingleton.Instance.Notifier.AddDisconnectMessage($"Got Hazel error - {errorType}"); + if (banMalformedPacketSender) + { + KeyValuePair keyValuePair = HandleMessage.LastJoin.OrderBy((KeyValuePair pair) => pair.Value).FirstOrDefault(); + AmongUsClient.Instance.KickPlayer(keyValuePair.Key, ban: true); + } + new LateTask(delegate + { + isCooling = false; + }, 10f); + } + } + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadPackedUInt32))] + class SafePackedUInt32 + { + static bool Prefix(MessageReader __instance, ref uint __result) + { + if (__instance.Length <= __instance.Position && enableMalformedPacketGuard && isShouldProtect) + { + __result = 0; + GetHazelError("ReadPackedUInt32"); + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadPackedInt32))] + class SafePackedInt32 + { + static bool Prefix(MessageReader __instance, ref int __result) + { + if (__instance.Length <= __instance.Position && enableMalformedPacketGuard && isShouldProtect) + { + __result = 0; + GetHazelError("ReadPackedInt32"); + return false; + } + + return true; + } + } + } + +internal class LateTask + { + public string name; + + public float timer; + + public System.Action action; + + public static List Tasks = new List(); + + public bool Run(float deltaTime) + { + timer -= deltaTime; + if (timer <= 0f) + { + action(); + return true; + } + return false; + } + + public LateTask(System.Action action, float time, string name = "No Name Task") + { + this.action = action; + timer = time; + this.name = name; + Tasks.Add(this); + } + + public static void Stop(string name) + { + Tasks.RemoveAll((LateTask task) => task.name == name); + } + + public static void Stop(LateTask task) + { + Tasks.Remove(task); + } + + public static void Update(float deltaTime) + { + List list = new List(); + for (int i = 0; i < Tasks.Count; i++) + { + LateTask lateTask = Tasks[i]; + try + { + if (lateTask.Run(deltaTime)) + { + list.Add(lateTask); + } + } + catch (Exception) + { + list.Add(lateTask); + } + } + list.ForEach(delegate (LateTask task) + { + Tasks.Remove(task); + }); + } + } + } +} From 68e7487993e15acb0b3b8ade75dab5f1dab70696 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Thu, 18 Jun 2026 12:21:08 +0200 Subject: [PATCH 38/39] Update ElysiumModMenu --- Plugin.cs | 2 +- features/EspFormatting.cs | 2 +- features/GeneralPanel.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Plugin.cs b/Plugin.cs index 6b14f34..cb49578 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -40,7 +40,7 @@ namespace ElysiumModMenu { - [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.5.1")] + [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.6")] public class Plugin : BasePlugin { public static ModPlayer modClass; diff --git a/features/EspFormatting.cs b/features/EspFormatting.cs index 4892def..99299e9 100644 --- a/features/EspFormatting.cs +++ b/features/EspFormatting.cs @@ -596,7 +596,7 @@ public static void Postfix(PingTracker __instance) if (ElysiumModMenuGUI.showWatermark) { - string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.5.1"); + string shimmerTitle = ElysiumModMenuGUI.ApplyMenuShimmer("ElysiumModMenu v1.3.6"); finalString = $"{shimmerTitle} • " + finalString; } diff --git a/features/GeneralPanel.cs b/features/GeneralPanel.cs index 3527828..82da414 100644 --- a/features/GeneralPanel.cs +++ b/features/GeneralPanel.cs @@ -152,7 +152,7 @@ private void DrawGeneralInfoTab() string contributorHex = rgbText ? accentHex : ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(109, 138, 255, 255)) : new Color32(109, 138, 255, 255)); string dangerHex = rgbText ? accentHex : ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(231, 76, 60, 255)) : new Color32(231, 76, 60, 255)); string safeHex = rgbText ? accentHex : ColorUtility.ToHtmlStringRGB(whiteMenuTheme ? GetThemeAccentColor(new Color32(57, 255, 20, 255)) : new Color32(57, 255, 20, 255)); - string versionText = "1.3.5.1"; + string versionText = "1.3.6"; GUIStyle textStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 12 }; textStyle.normal.textColor = whiteMenuTheme ? new Color(0.16f, 0.16f, 0.16f, 1f) : new Color(0.85f, 0.85f, 0.85f, 1f); From 57326aa0ee28dd71643c569132b831982ea3a003 Mon Sep 17 00:00:00 2001 From: meowchelo Date: Thu, 18 Jun 2026 18:09:43 +0200 Subject: [PATCH 39/39] Update ElysiumModMenu v1.3.7 --- ElysiumModMenu.csproj | 233 +++++++++--------- Network.cs | 44 +++- Plugin.cs | 30 ++- README.md | 2 +- features/EspFormatting.cs | 17 -- features/FavoriteOutfitsPanel.cs | 28 +++ features/HushWhisper.cs | 136 ++++++++++ features/LocalIdentity.cs | 14 -- features/MenuTheme.cs | 2 +- features/PlayerEsp.cs | 3 - features/Reports.cs | 1 - routines/ChatController_AddChat_Patch.cs | 29 --- routines/PlatformSpooferPatch.cs | 3 +- ...Cosmetics_HatManager_Initialize_Postfix.cs | 57 ----- ..._PlayerPurchasesData_GetPurchase_Prefix.cs | 10 +- ui/MenuKeybinds.cs | 2 + ui/PlayerAndSettingsState.cs | 55 ----- 17 files changed, 358 insertions(+), 308 deletions(-) create mode 100644 features/HushWhisper.cs delete mode 100644 routines/UnlockCosmetics_HatManager_Initialize_Postfix.cs diff --git a/ElysiumModMenu.csproj b/ElysiumModMenu.csproj index 0e4ecef..36aa7d8 100644 --- a/ElysiumModMenu.csproj +++ b/ElysiumModMenu.csproj @@ -7,20 +7,21 @@ latest ElysiumModMenu ElysiumModMenu - 1.3.4 - 1.3.4.0 - 1.3.4.0 - C:\Program Files (x86)\Steam\steamapps\common\Among Us + 1.3.7 + 1.3.7.0 + 1.3.7.0 + E:\AmongUs + C:\Program Files (x86)\Steam\steamapps\common\Among Us - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\AddressablesPlayAssetDelivery.dll + $(AmongUsDir)\BepInEx\interop\AddressablesPlayAssetDelivery.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\AmongUsCaching.dll + $(AmongUsDir)\BepInEx\interop\AmongUsCaching.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Assembly-CSharp-firstpass.dll + $(AmongUsDir)\BepInEx\interop\Assembly-CSharp-firstpass.dll $(AmongUsDir)\BepInEx\core\BepInEx.Core.dll @@ -32,10 +33,10 @@ $(AmongUsDir)\BepInEx\core\0Harmony.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\CsvHelper.dll + $(AmongUsDir)\BepInEx\interop\CsvHelper.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Hazel.dll + $(AmongUsDir)\BepInEx\interop\Hazel.dll $(AmongUsDir)\BepInEx\core\Il2CppInterop.Runtime.dll @@ -44,268 +45,268 @@ $(AmongUsDir)\BepInEx\interop\Assembly-CSharp.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppMono.Security.dll + $(AmongUsDir)\BepInEx\interop\Il2CppMono.Security.dll $(AmongUsDir)\BepInEx\interop\Il2Cppmscorlib.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.dll + $(AmongUsDir)\BepInEx\interop\Il2CppSystem.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Configuration.dll + $(AmongUsDir)\BepInEx\interop\Il2CppSystem.Configuration.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Core.dll + $(AmongUsDir)\BepInEx\interop\Il2CppSystem.Core.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Data.dll + $(AmongUsDir)\BepInEx\interop\Il2CppSystem.Data.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Drawing.dll + $(AmongUsDir)\BepInEx\interop\Il2CppSystem.Drawing.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Net.Http.dll + $(AmongUsDir)\BepInEx\interop\Il2CppSystem.Net.Http.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Numerics.dll + $(AmongUsDir)\BepInEx\interop\Il2CppSystem.Numerics.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Runtime.Serialization.dll + $(AmongUsDir)\BepInEx\interop\Il2CppSystem.Runtime.Serialization.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Xml.dll + $(AmongUsDir)\BepInEx\interop\Il2CppSystem.Xml.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Il2CppSystem.Xml.Linq.dll + $(AmongUsDir)\BepInEx\interop\Il2CppSystem.Xml.Linq.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\io.sentry.unity.runtime.dll + $(AmongUsDir)\BepInEx\interop\io.sentry.unity.runtime.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Newtonsoft.Json.dll + $(AmongUsDir)\BepInEx\interop\Newtonsoft.Json.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\QRCoder.dll + $(AmongUsDir)\BepInEx\interop\QRCoder.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Rewired_Core.dll + $(AmongUsDir)\BepInEx\interop\Rewired_Core.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Rewired_Windows.dll + $(AmongUsDir)\BepInEx\interop\Rewired_Windows.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Rewired_Windows_Functions.dll + $(AmongUsDir)\BepInEx\interop\Rewired_Windows_Functions.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.dll + $(AmongUsDir)\BepInEx\interop\Sentry.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.Microsoft.Bcl.AsyncInterfaces.dll + $(AmongUsDir)\BepInEx\interop\Sentry.Microsoft.Bcl.AsyncInterfaces.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Buffers.dll + $(AmongUsDir)\BepInEx\interop\Sentry.System.Buffers.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Collections.Immutable.dll + $(AmongUsDir)\BepInEx\interop\Sentry.System.Collections.Immutable.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Memory.dll + $(AmongUsDir)\BepInEx\interop\Sentry.System.Memory.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Numerics.Vectors.dll + $(AmongUsDir)\BepInEx\interop\Sentry.System.Numerics.Vectors.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Reflection.Metadata.dll + $(AmongUsDir)\BepInEx\interop\Sentry.System.Reflection.Metadata.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Runtime.CompilerServices.Unsafe.dll + $(AmongUsDir)\BepInEx\interop\Sentry.System.Runtime.CompilerServices.Unsafe.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Text.Encodings.Web.dll + $(AmongUsDir)\BepInEx\interop\Sentry.System.Text.Encodings.Web.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Text.Json.dll + $(AmongUsDir)\BepInEx\interop\Sentry.System.Text.Json.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.System.Threading.Tasks.Extensions.dll + $(AmongUsDir)\BepInEx\interop\Sentry.System.Threading.Tasks.Extensions.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Sentry.Unity.dll + $(AmongUsDir)\BepInEx\interop\Sentry.Unity.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Addressables.dll + $(AmongUsDir)\BepInEx\interop\Unity.Addressables.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.LevelPlay.dll + $(AmongUsDir)\BepInEx\interop\Unity.LevelPlay.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.ProBuilder.dll + $(AmongUsDir)\BepInEx\interop\Unity.ProBuilder.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.ResourceManager.dll + $(AmongUsDir)\BepInEx\interop\Unity.ResourceManager.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.dll + $(AmongUsDir)\BepInEx\interop\Unity.Services.Core.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Configuration.dll + $(AmongUsDir)\BepInEx\interop\Unity.Services.Core.Configuration.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Device.dll + $(AmongUsDir)\BepInEx\interop\Unity.Services.Core.Device.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Environments.Internal.dll + $(AmongUsDir)\BepInEx\interop\Unity.Services.Core.Environments.Internal.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Internal.dll + $(AmongUsDir)\BepInEx\interop\Unity.Services.Core.Internal.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Registration.dll + $(AmongUsDir)\BepInEx\interop\Unity.Services.Core.Registration.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Scheduler.dll + $(AmongUsDir)\BepInEx\interop\Unity.Services.Core.Scheduler.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Telemetry.dll + $(AmongUsDir)\BepInEx\interop\Unity.Services.Core.Telemetry.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.Services.Core.Threading.dll + $(AmongUsDir)\BepInEx\interop\Unity.Services.Core.Threading.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\Unity.TextMeshPro.dll + $(AmongUsDir)\BepInEx\interop\Unity.TextMeshPro.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AccessibilityModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.AccessibilityModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AIModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.AIModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AndroidJNIModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.AndroidJNIModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AnimationModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.AnimationModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AssetBundleModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.AssetBundleModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.AudioModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.AudioModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ClothModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.ClothModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ClusterInputModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.ClusterInputModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ClusterRendererModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.ClusterRendererModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ContentLoadModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.ContentLoadModule.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.CoreModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.CrashReportingModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.CrashReportingModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.DirectorModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.DirectorModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.DSPGraphModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.DSPGraphModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.GameCenterModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.GameCenterModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.GIModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.GIModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.GridModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.GridModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.HotReloadModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.HotReloadModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ImageConversionModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.ImageConversionModule.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.IMGUIModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.InputModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.InputModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.JSONSerializeModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.JSONSerializeModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.LocalizationModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.LocalizationModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ParticleSystemModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.ParticleSystemModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.PerformanceReportingModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.PerformanceReportingModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.Physics2DModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.Physics2DModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.PhysicsModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.PhysicsModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ProfilerModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.ProfilerModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.PropertiesModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.PropertiesModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.Purchasing.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.Purchasing.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.Purchasing.SecurityCore.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.Purchasing.SecurityCore.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.ScreenCaptureModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.ScreenCaptureModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.SharedInternalsModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.SharedInternalsModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.SpriteMaskModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.SpriteMaskModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.SpriteShapeModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.SpriteShapeModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.StreamingModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.StreamingModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.SubstanceModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.SubstanceModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.SubsystemsModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.SubsystemsModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TerrainModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.TerrainModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TerrainPhysicsModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.TerrainPhysicsModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TextCoreFontEngineModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.TextCoreFontEngineModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TextCoreTextEngineModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.TextCoreTextEngineModule.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.TextRenderingModule.dll @@ -314,76 +315,76 @@ $(AmongUsDir)\BepInEx\interop\UnityEngine.InputLegacyModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TilemapModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.TilemapModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.TLSModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.TLSModule.dll $(AmongUsDir)\BepInEx\interop\UnityEngine.UI.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UIElementsModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UIElementsModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UIModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UIModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UmbraModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UmbraModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityAnalyticsCommonModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UnityAnalyticsCommonModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityAnalyticsModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UnityAnalyticsModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityConnectModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UnityConnectModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityCurlModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UnityCurlModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityTestProtocolModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UnityTestProtocolModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityWebRequestAssetBundleModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UnityWebRequestAssetBundleModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityWebRequestAudioModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UnityWebRequestAudioModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityWebRequestModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UnityWebRequestModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityWebRequestTextureModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UnityWebRequestTextureModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.UnityWebRequestWWWModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.UnityWebRequestWWWModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.VehiclesModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.VehiclesModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.VFXModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.VFXModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.VideoModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.VideoModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.VirtualTexturingModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.VirtualTexturingModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.VRModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.VRModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.WindModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.WindModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\UnityEngine.XRModule.dll + $(AmongUsDir)\BepInEx\interop\UnityEngine.XRModule.dll - ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Among Us\BepInEx\interop\__Generated.dll + $(AmongUsDir)\BepInEx\interop\__Generated.dll diff --git a/Network.cs b/Network.cs index 1bfe2d1..b7303e8 100644 --- a/Network.cs +++ b/Network.cs @@ -508,6 +508,12 @@ public static bool Prefix(InnerNetClient __instance, MessageReader reader, SendO { if (enableMalformedPacketGuard == false) return true; + if (reader == null) + { + Crash("Empty network packet"); + return false; + } + switch (reader.Tag) { case 1: @@ -649,6 +655,17 @@ public static bool Prefix(InnerNetClient __instance, MessageReader reader, SendO } return false; } + + // Catch malformed-reader failures at the packet boundary. Patching Hazel's + // packed integer methods directly recurses on Epic's IL2CPP wrappers. + public static Exception Finalizer(Exception __exception) + { + if (__exception == null || !enableMalformedPacketGuard) + return __exception; + + Crash("Malformed network packet (" + __exception.GetType().Name + ")"); + return null; + } } [HarmonyPatch(typeof(InnerNetClient), "HandleGameData")] @@ -674,6 +691,10 @@ public static void HandleData(InnerNetClient __instance, MessageReader parentRea __instance.StartCoroutine(BepInEx.Unity.IL2CPP.Utils.Collections.CollectionExtensions.WrapToIl2Cpp(Handle(__instance, reader, msgNum, TargetClientId, sendOption, num))); } } + catch (Exception error) + { + HandleMessage.Crash("Malformed GameData frame (" + error.GetType().Name + ")"); + } finally { parentReader.Recycle(); @@ -689,16 +710,23 @@ public static IEnumerator Handle(InnerNetClient __instance, MessageReader reader { case GameDataTypes.SceneChangeFlag: { - int num3 = reader.ReadPackedInt32(); - ClientData clientData2 = __instance.FindClientById(num3); - string text = reader.ReadString(); - if (clientData2 != null && !string.IsNullOrWhiteSpace(text)) + try { - MonoBehaviourExtensions.StartCoroutine(__instance, AmongUsClientUtils.CoOnPlayerChangedScene(__instance, clientData2, text)); - Debug.Log($"SceneChangeFlag for {num3} to {text}"); - break; + int num3 = reader.ReadPackedInt32(); + ClientData clientData2 = __instance.FindClientById(num3); + string text = reader.ReadString(); + if (clientData2 != null && !string.IsNullOrWhiteSpace(text)) + { + MonoBehaviourExtensions.StartCoroutine(__instance, AmongUsClientUtils.CoOnPlayerChangedScene(__instance, clientData2, text)); + Debug.Log($"SceneChangeFlag for {num3} to {text}"); + break; + } + Debug.Log($"(SceneChangeFlag) Couldn't find client {num3} to change scene to {text}"); + } + catch (Exception error) + { + HandleMessage.Crash("Malformed scene packet (" + error.GetType().Name + ")"); } - Debug.Log($"(SceneChangeFlag) Couldn't find client {num3} to change scene to {text}"); reader.Recycle(); break; } diff --git a/Plugin.cs b/Plugin.cs index cb49578..cca14ef 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -40,7 +40,7 @@ namespace ElysiumModMenu { - [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.6")] + [BepInPlugin("com.elysiummodmenu.menu", "ElysiumModMenu", "1.3.7")] public class Plugin : BasePlugin { public static ModPlayer modClass; @@ -57,6 +57,7 @@ public class Plugin : BasePlugin public static ConfigEntry EnablePlatformSpoof; public static ConfigEntry AutoBanBrokenFriendCodeConfig; public static ConfigEntry PlatformIndex; + private static ConfigEntry StorePlatformMigrated; public static ConfigEntry ShowWatermarkConfig; public static ConfigEntry MenuColorIndexConfig; public static ConfigEntry RgbMenuModeConfig; @@ -114,7 +115,14 @@ public override void Load() SpoofFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Spoofing", "FriendCode", "crewmate01", ""); EnablePlatformSpoof = MenuConfig.Bind("ElysiumModMenu.Spoofing", "EnablePlatformSpoof", true, ""); AutoBanBrokenFriendCodeConfig = MenuConfig.Bind("ElysiumModMenu.Anticheat", "AutoBanBrokenFriendCode", false, ""); - PlatformIndex = MenuConfig.Bind("ElysiumModMenu.Spoofing", "PlatformIndex", 1, ""); + int nativePlatformIndex = DetectNativePlatformIndex(); + PlatformIndex = MenuConfig.Bind("ElysiumModMenu.Spoofing", "PlatformIndex", nativePlatformIndex, ""); + StorePlatformMigrated = MenuConfig.Bind("ElysiumModMenu.Compatibility", "StorePlatformMigrated", false, "Internal one-time Epic/Steam platform migration flag."); + if (!StorePlatformMigrated.Value) + { + PlatformIndex.Value = nativePlatformIndex; + StorePlatformMigrated.Value = true; + } ShowWatermarkConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "ShowWatermark", true, ""); MenuColorIndexConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "MenuColorIndex", 10, ""); RgbMenuModeConfig = MenuConfig.Bind("ElysiumModMenu.GUI", "RgbMenuMode", false, ""); @@ -142,6 +150,24 @@ public override void Load() harmony.PatchAll(); } + private static int DetectNativePlatformIndex() + { + try + { + string gameRoot = System.IO.Directory.GetCurrentDirectory(); + bool epicInstall = System.IO.Directory.Exists(System.IO.Path.Combine(gameRoot, ".egstore")); + bool epicLaunch = Environment.GetCommandLineArgs().Any(argument => + argument.IndexOf("epic", StringComparison.OrdinalIgnoreCase) >= 0 || + argument.StartsWith("-AUTH_", StringComparison.OrdinalIgnoreCase)); + + return epicInstall || epicLaunch ? 0 : 1; + } + catch + { + return 1; + } + } + private static void RemoveLegacyPlaintextWebhookConfig(string configPath) { try diff --git a/README.md b/README.md index a24c8d9..8350231 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Advanced BepInEx IL2CPP mod menu for Among Us with host tools, anti-cheat utilit | Version | Status | Download | | :--- | :--- | :--- | -| v1.3.2 | Latest | [Download ElysiumModMenu.dll](https://github.com/meowchelo/ElysiumModMenu/releases/latest) | +| v1.3.7 | Latest | [Download ElysiumModMenu.dll](https://github.com/meowchelo/ElysiumModMenu/releases/latest) | ## Highlights diff --git a/features/EspFormatting.cs b/features/EspFormatting.cs index 99299e9..9f70ad1 100644 --- a/features/EspFormatting.cs +++ b/features/EspFormatting.cs @@ -621,23 +621,6 @@ public static void Postfix(PingTracker __instance) } } -[HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Update))] - public static class GameStartManager_Update_Patch - { - public static void Postfix(GameStartManager __instance) - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; - if (ElysiumModMenuGUI.fakeStartCounterTroll) - { - try { sbyte[] arr = { -123, -111, -100, -69, -67, -52, -42, 0, 42, 52, 67, 69, 100, 111, 123 }; sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; PlayerControl.LocalPlayer.RpcSetStartCounter(b); __instance.SetStartCounter(b); } catch { } - } - else if (ElysiumModMenuGUI.fakeStartCounterCustom && int.TryParse(ElysiumModMenuGUI.fakeStartInput, out int custom)) - { - try { PlayerControl.LocalPlayer.RpcSetStartCounter(custom); __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); } catch { } - } - } - } - [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] public static class InfiniteGamePatch { public static bool Prefix() { try { if (ElysiumModMenuGUI.neverEndGame && AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost) return false; } catch { } return true; } } diff --git a/features/FavoriteOutfitsPanel.cs b/features/FavoriteOutfitsPanel.cs index 390313f..ee08bd5 100644 --- a/features/FavoriteOutfitsPanel.cs +++ b/features/FavoriteOutfitsPanel.cs @@ -45,6 +45,8 @@ public partial class ElysiumModMenuGUI : MonoBehaviour public void Update() { + TickFakeStartCounter(); + bool isTypingOrBinding = isEditingName || isEditingLevel || isEditingFriendCode || isEditingLocalFriendCode || isEditingGhostChatColor || isEditingBan || customChatInputFocused || isWaitingForBind || isWaitBindMassMorph || isWaitBindSpawnLobby || isWaitBindDespawnLobby || isWaitBindCloseMeeting || isWaitBindInstaStart || @@ -543,6 +545,32 @@ public void Update() } } + private static void TickFakeStartCounter() + { + if (customStartTimer > 0f || (!fakeStartCounterTroll && !fakeStartCounterCustom)) return; + + try + { + if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; + GameStartManager manager = GameStartManager.Instance; + if (manager == null) return; + + if (fakeStartCounterTroll) + { + sbyte[] values = { -123, -111, -100, -69, -67, -52, -42, 0, 42, 52, 67, 69, 100, 111, 123 }; + sbyte value = values[UnityEngine.Random.Range(0, values.Length)]; + PlayerControl.LocalPlayer.RpcSetStartCounter(value); + manager.SetStartCounter(value); + } + else if (int.TryParse(fakeStartInput, out int custom)) + { + PlayerControl.LocalPlayer.RpcSetStartCounter(custom); + manager.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); + } + } + catch { } + } + public static List GetFreeColorIds() { HashSet used = new HashSet(); diff --git a/features/HushWhisper.cs b/features/HushWhisper.cs new file mode 100644 index 0000000..b8fd2e2 --- /dev/null +++ b/features/HushWhisper.cs @@ -0,0 +1,136 @@ +#nullable disable + +using System.Linq; +using System.Text.RegularExpressions; +using Hazel; + +namespace ElysiumModMenu +{ + /// + /// Handles private chat commands: /w, /pm, and /msg. + /// + public static class HushWhisper + { + private static readonly Regex RichTextTag = new Regex("<.*?>"); + + /// + /// Sends a private message when the chat input contains a whisper command. + /// Returns true when the input was handled and normal chat sending must be blocked. + /// + public static bool TryHandle(ChatController chat) + { + if (chat?.freeChatField?.textArea == null) + return false; + + string text = chat.freeChatField.Text; + if (string.IsNullOrWhiteSpace(text)) + return false; + + string lowerText = text.ToLowerInvariant(); + if (!lowerText.StartsWith("/w ") && + !lowerText.StartsWith("/pm ") && + !lowerText.StartsWith("/msg ")) + { + return false; + } + + string[] parts = text.Split(new[] { ' ' }, 3); + if (parts.Length < 3 || string.IsNullOrWhiteSpace(parts[2])) + { + ShowLocalMessage("[ERROR] Usage: /w [ID, color, or name] [message]"); + ClearInput(chat); + return true; + } + + string targetInput = parts[1].ToLowerInvariant().Trim(); + string safeMessage = StripRichText(parts[2]); + PlayerControl target = FindTarget(targetInput); + + if (target == null || target == PlayerControl.LocalPlayer) + { + ShowLocalMessage("[ERROR] Player not found. Enter an ID, color, or name."); + ClearInput(chat); + return true; + } + + if (AmongUsClient.Instance != null && PlayerControl.LocalPlayer != null) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately( + PlayerControl.LocalPlayer.NetId, + 13, + SendOption.Reliable, + target.OwnerId); + + writer.Write($"Whispers in your ear:\n{safeMessage}"); + AmongUsClient.Instance.FinishRpcImmediately(writer); + + string targetName = StripRichText(target.Data.PlayerName); + ShowLocalMessage($"You whisper to {targetName}:\n{safeMessage}"); + } + + ClearInput(chat); + return true; + } + + private static PlayerControl FindTarget(string targetInput) + { + if (PlayerControl.AllPlayerControls == null) + return null; + + PlayerControl target = null; + + if (byte.TryParse(targetInput, out byte playerId)) + { + target = PlayerControl.AllPlayerControls + .ToArray() + .FirstOrDefault(player => player != null && player.PlayerId == playerId); + } + + if (target != null) + return target; + + PlayerControl partialMatch = null; + int targetColorId = ElysiumModMenuGUI.GetColorIdByName(targetInput); + + foreach (PlayerControl player in PlayerControl.AllPlayerControls) + { + if (player == null || + player.Data == null || + player.Data.Disconnected || + player == PlayerControl.LocalPlayer) + { + continue; + } + + string playerName = StripRichText(player.Data.PlayerName).ToLowerInvariant().Trim(); + int colorId = (int)player.Data.DefaultOutfit.ColorId; + + if (playerName == targetInput || (targetColorId != -1 && colorId == targetColorId)) + return player; + + if (partialMatch == null && playerName.StartsWith(targetInput)) + partialMatch = player; + } + + return partialMatch; + } + + private static string StripRichText(string value) + { + return RichTextTag.Replace(value ?? string.Empty, string.Empty) + .Replace("<", string.Empty) + .Replace(">", string.Empty); + } + + private static void ShowLocalMessage(string message) + { + if (HudManager.Instance?.Chat != null && PlayerControl.LocalPlayer != null) + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, message); + } + + private static void ClearInput(ChatController chat) + { + chat.freeChatField.textArea.SetText(string.Empty, string.Empty); + } + } +} diff --git a/features/LocalIdentity.cs b/features/LocalIdentity.cs index c5c2eb5..537df02 100644 --- a/features/LocalIdentity.cs +++ b/features/LocalIdentity.cs @@ -403,20 +403,6 @@ private void SpawnLobby() catch { } } -public static void UnlockCosmetics() - { - if (HatManager.Instance == null) return; - try - { - foreach (var h in HatManager.Instance.allHats) h.Free = true; - foreach (var s in HatManager.Instance.allSkins) s.Free = true; - foreach (var v in HatManager.Instance.allVisors) v.Free = true; - foreach (var p in HatManager.Instance.allPets) p.Free = true; - foreach (var n in HatManager.Instance.allNamePlates) n.Free = true; - } - catch { } - } - public static void ChangeNameGlobalHost(PlayerControl target, string newName) { if (target == null) return; diff --git a/features/MenuTheme.cs b/features/MenuTheme.cs index fc78526..7d8faa0 100644 --- a/features/MenuTheme.cs +++ b/features/MenuTheme.cs @@ -53,7 +53,7 @@ private void LoadConfig() spoofFriendCodeInput = Plugin.SpoofFriendCodeConfig.Value; enablePlatformSpoof = Plugin.EnablePlatformSpoof.Value; autoBanBrokenFriendCode = Plugin.AutoBanBrokenFriendCodeConfig.Value; - currentPlatformIndex = Plugin.PlatformIndex.Value; + currentPlatformIndex = Mathf.Clamp(Plugin.PlatformIndex.Value, 0, platformValues.Length - 1); showWatermark = Plugin.ShowWatermarkConfig.Value; unlockCosmetics = Plugin.UnlockCosmeticsConfig.Value; moreLobbyInfo = Plugin.MoreLobbyInfoConfig.Value; diff --git a/features/PlayerEsp.cs b/features/PlayerEsp.cs index 4739c1b..ca8d8b7 100644 --- a/features/PlayerEsp.cs +++ b/features/PlayerEsp.cs @@ -251,9 +251,6 @@ public static void Postfix(HudManager __instance) } } -[HarmonyPatch(typeof(PlatformSpecificData), nameof(PlatformSpecificData.Serialize))] - public static class PlatformSpooferPatch { public static void Prefix(PlatformSpecificData __instance) { try { if (ElysiumModMenuGUI.enablePlatformSpoof && __instance != null) __instance.Platform = ElysiumModMenuGUI.platformValues[ElysiumModMenuGUI.currentPlatformIndex]; } catch { } } } - [HarmonyPatch(typeof(FullAccount), nameof(FullAccount.CanSetCustomName))] public static class FullAccount_CanSetCustomName_Patch { public static void Prefix(ref bool canSetName) { try { if (ElysiumModMenuGUI.unlockFeatures) canSetName = true; } catch { } } } diff --git a/features/Reports.cs b/features/Reports.cs index 7c83a84..e73cd64 100644 --- a/features/Reports.cs +++ b/features/Reports.cs @@ -81,7 +81,6 @@ public void Start() { activeGui = this; if (enableBackground) LoadBackgroundImage(); - UnlockCosmetics(); LoadConfig(); LoadBanList(); LoadBotBanList(); diff --git a/routines/ChatController_AddChat_Patch.cs b/routines/ChatController_AddChat_Patch.cs index 3f9ae03..335360a 100644 --- a/routines/ChatController_AddChat_Patch.cs +++ b/routines/ChatController_AddChat_Patch.cs @@ -184,33 +184,4 @@ private static bool ShowGhostMessage(PlayerControl sourcePlayer, string chatText return true; } } - - - - public static void Postfix(GameStartManager __instance) - { - if (AmongUsClient.Instance == null || !AmongUsClient.Instance.AmHost || PlayerControl.LocalPlayer == null) return; - if (ElysiumModMenuGUI.customStartTimer > 0f) return; - - if (ElysiumModMenuGUI.fakeStartCounterTroll) - { - try - { - sbyte[] arr = { -123, -100, -69, -42, 0, 42, 69, 100, 123 }; - sbyte b = arr[UnityEngine.Random.Range(0, arr.Length)]; - PlayerControl.LocalPlayer.RpcSetStartCounter((int)b); - __instance.SetStartCounter(b); - } - catch { } - } - else if (ElysiumModMenuGUI.fakeStartCounterCustom && int.TryParse(ElysiumModMenuGUI.fakeStartInput, out int custom)) - { - try - { - PlayerControl.LocalPlayer.RpcSetStartCounter(custom); - __instance.SetStartCounter((sbyte)Mathf.Clamp(custom, -128, 127)); - } - catch { } - } - } } diff --git a/routines/PlatformSpooferPatch.cs b/routines/PlatformSpooferPatch.cs index a2c64c0..20cf6a9 100644 --- a/routines/PlatformSpooferPatch.cs +++ b/routines/PlatformSpooferPatch.cs @@ -49,7 +49,8 @@ public static void Prefix(PlatformSpecificData __instance) { if (!ElysiumModMenuGUI.enablePlatformSpoof) return; - __instance.Platform = ElysiumModMenuGUI.platformValues[ElysiumModMenuGUI.currentPlatformIndex]; + int platformIndex = Mathf.Clamp(ElysiumModMenuGUI.currentPlatformIndex, 0, ElysiumModMenuGUI.platformValues.Length - 1); + __instance.Platform = ElysiumModMenuGUI.platformValues[platformIndex]; __instance.PlatformName = "ElysiumModMenu by Meowchelo (and one silly guy :p) https://github.com/meowchelo/ElysiumModMenu"; } } diff --git a/routines/UnlockCosmetics_HatManager_Initialize_Postfix.cs b/routines/UnlockCosmetics_HatManager_Initialize_Postfix.cs deleted file mode 100644 index 66215c4..0000000 --- a/routines/UnlockCosmetics_HatManager_Initialize_Postfix.cs +++ /dev/null @@ -1,57 +0,0 @@ -#nullable disable -#pragma warning disable CS0162, CS0108, CS0219, CS0661, CS0660, CS8632, CS0168, CS0659 -using AmongUs.Data.Player; -using AmongUs.GameOptions; -using AmongUs.InnerNet.GameDataMessages; -using BepInEx; -using BepInEx.Configuration; -using BepInEx.Unity.IL2CPP; -using BepInEx.Unity.IL2CPP.Utils; -using BepInEx.Unity.IL2CPP.Utils.Collections; -using ElysiumModMenu; -using HarmonyLib; -using Hazel; -using Il2CppInterop.Runtime.Attributes; -using Il2CppInterop.Runtime.Injection; -using Il2CppInterop.Runtime.InteropTypes.Arrays; -using InnerNet; -using RewiredConsts; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.RegularExpressions; -using TMPro; -using UnityEngine; -using UnityEngine.AddressableAssets; -using UnityEngine.Events; -using UnityEngine.Playables; -using UnityEngine.ResourceManagement.AsyncOperations; -using UnityEngine.UI; -using static ElysiumModMenu.ElysiumModMenuGUI; -using static Rewired.UI.ControlMapper.ControlMapper; -using Color = UnityEngine.Color; -using Object = UnityEngine.Object; -using Vector3 = UnityEngine.Vector3; - - -[HarmonyPatch(typeof(HatManager), nameof(HatManager.Initialize))] -public static class UnlockCosmetics_HatManager_Initialize_Postfix -{ - public static void Postfix(HatManager __instance) - { - if (!ElysiumModMenuGUI.unlockCosmetics) return; - - foreach (var bundle in __instance.allBundles) bundle.Free = true; - foreach (var hat in __instance.allHats) hat.Free = true; - foreach (var nameplate in __instance.allNamePlates) nameplate.Free = true; - foreach (var pet in __instance.allPets) pet.Free = true; - foreach (var skin in __instance.allSkins) skin.Free = true; - foreach (var visor in __instance.allVisors) visor.Free = true; - foreach (var starBundle in __instance.allStarBundles) starBundle.price = 0; - } -} diff --git a/routines/UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix.cs b/routines/UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix.cs index 6d92135..c34bdda 100644 --- a/routines/UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix.cs +++ b/routines/UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix.cs @@ -44,8 +44,12 @@ public static class UnlockCosmetics_PlayerPurchasesData_GetPurchase_Prefix { public static bool Prefix(ref bool __result) { - if (!ElysiumModMenuGUI.unlockCosmetics) return true; - __result = true; - return false; + if (ElysiumModMenuGUI.unlockCosmetics) + { + __result = true; + return false; + } + + return true; } } diff --git a/ui/MenuKeybinds.cs b/ui/MenuKeybinds.cs index a2efdbe..b084e22 100644 --- a/ui/MenuKeybinds.cs +++ b/ui/MenuKeybinds.cs @@ -598,6 +598,8 @@ public static class AllowURLS_ChatController_SendFreeChat_Patch { public static bool Prefix(ChatController __instance) { + if (HushWhisper.TryHandle(__instance)) return false; + if (!ElysiumModMenuGUI.allowLinksAndSymbols) return true; string text = __instance.freeChatField.Text; diff --git a/ui/PlayerAndSettingsState.cs b/ui/PlayerAndSettingsState.cs index 7e46461..164e8c9 100644 --- a/ui/PlayerAndSettingsState.cs +++ b/ui/PlayerAndSettingsState.cs @@ -651,61 +651,6 @@ private void DrawVisualsInGame() public static bool enableUnownedSpawnGuard = true; -static class HazelThings - { - static bool isShouldProtect => PlayerControl.LocalPlayer && AmongUsClient.Instance.NetworkMode != NetworkModes.FreePlay; - - static bool isCooling; - static void GetHazelError(string errorType) - { - if (!isCooling) - { - isCooling = true; - DestroyableSingleton.Instance.Notifier.AddDisconnectMessage($"Got Hazel error - {errorType}"); - if (banMalformedPacketSender) - { - KeyValuePair keyValuePair = HandleMessage.LastJoin.OrderBy((KeyValuePair pair) => pair.Value).FirstOrDefault(); - AmongUsClient.Instance.KickPlayer(keyValuePair.Key, ban: true); - } - new LateTask(delegate - { - isCooling = false; - }, 10f); - } - } - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadPackedUInt32))] - class SafePackedUInt32 - { - static bool Prefix(MessageReader __instance, ref uint __result) - { - if (__instance.Length <= __instance.Position && enableMalformedPacketGuard && isShouldProtect) - { - __result = 0; - GetHazelError("ReadPackedUInt32"); - return false; - } - - return true; - } - } - - [HarmonyPatch(typeof(MessageReader), nameof(MessageReader.ReadPackedInt32))] - class SafePackedInt32 - { - static bool Prefix(MessageReader __instance, ref int __result) - { - if (__instance.Length <= __instance.Position && enableMalformedPacketGuard && isShouldProtect) - { - __result = 0; - GetHazelError("ReadPackedInt32"); - return false; - } - - return true; - } - } - } - internal class LateTask { public string name;