diff --git a/Reactor.Debugger/AutoJoin/AutoJoinClientConnection.cs b/Reactor.Debugger/AutoJoin/AutoJoinClientConnection.cs new file mode 100644 index 0000000..2c05665 --- /dev/null +++ b/Reactor.Debugger/AutoJoin/AutoJoinClientConnection.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Pipes; +using InnerNet; +using Reactor.Debugger.AutoJoin.Messages; +using Reactor.Utilities; + +namespace Reactor.Debugger.AutoJoin; + +internal sealed class AutoJoinClientConnection : AutoJoinConnection +{ + public const string PipeName = "Reactor.Debugger.AutoJoin"; + + private AutoJoinClientConnection(NamedPipeClientStream pipe) : base(pipe) + { + } + + protected override void Handle(BinaryReader reader, MessageType messageType) + { + switch (messageType) + { + case MessageType.JoinGame: + { + Handle(JoinGameMessage.Deserialize(reader)); + break; + } + + default: + { + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, null); + } + } + } + + private static void Handle(in JoinGameMessage message) + { + var (address, port, gameCode) = message; + + Dispatcher.Instance.Enqueue(() => + { + var gameCodeText = gameCode == InnerNetServer.LocalGameId ? "" : GameCode.IntToGameName(gameCode); + Info($"Joining {gameCodeText} on {address}:{port}"); + + if (gameCode == InnerNetServer.LocalGameId) + { + AmongUsClient.Instance.NetworkMode = NetworkModes.LocalGame; + AmongUsClient.Instance.GameId = gameCode; + AmongUsClient.Instance.StartCoroutine(AmongUsClient.Instance.CoConnectToGameServer(MatchMakerModes.Client, address, port, null)); + } + else + { + AmongUsClient.Instance.StartCoroutine(AmongUsClient.Instance.CoJoinOnlineGameDirect(gameCode, address, port, null)); + } + }); + } + + public void SendRequestJoin() + { + Write(default(RequestJoinGameMessage)); + } + + public static bool TryConnect([NotNullWhen(true)] out AutoJoinClientConnection? client) + { + var pipeClient = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + + try + { + pipeClient.Connect(100); + + client = new AutoJoinClientConnection(pipeClient); + return true; + } + catch (TimeoutException) + { + pipeClient.Dispose(); + + client = null; + return false; + } + } +} diff --git a/Reactor.Debugger/AutoJoin/AutoJoinConnection.cs b/Reactor.Debugger/AutoJoin/AutoJoinConnection.cs new file mode 100644 index 0000000..a85e74a --- /dev/null +++ b/Reactor.Debugger/AutoJoin/AutoJoinConnection.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading.Tasks; +using Reactor.Debugger.AutoJoin.Messages; + +namespace Reactor.Debugger.AutoJoin; + +internal abstract class AutoJoinConnection : IDisposable +{ + private readonly BinaryWriter _writer; + + protected AutoJoinConnection(PipeStream pipe) + { + _writer = new BinaryWriter(pipe); + + Task.Run(() => + { + try + { + var reader = new BinaryReader(pipe); + + while (pipe.IsConnected) + { + try + { + var messageType = (MessageType) reader.ReadByte(); + Debug($"Received {messageType}"); + Handle(reader, messageType); + } + catch (EndOfStreamException) + { + break; + } + } + + pipe.Dispose(); + Disconnected?.Invoke(); + } + catch (Exception e) + { + Error(e); + } + }); + } + + public event Action? Disconnected; + + protected abstract void Handle(BinaryReader reader, MessageType messageType); + + public void Write(in T message) where T : IMessage + { + Debug($"Writing {message}"); + _writer.Write(message); + } + + public void Dispose() + { + _writer.Dispose(); + } +} diff --git a/Reactor.Debugger/AutoJoin/AutoJoinConnectionListener.cs b/Reactor.Debugger/AutoJoin/AutoJoinConnectionListener.cs new file mode 100644 index 0000000..8e1e445 --- /dev/null +++ b/Reactor.Debugger/AutoJoin/AutoJoinConnectionListener.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO.Pipes; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using InnerNet; +using Reactor.Debugger.AutoJoin.Messages; + +namespace Reactor.Debugger.AutoJoin; + +internal sealed class AutoJoinConnectionListener : IDisposable +{ + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly Mutex _mutex; + + private AutoJoinConnectionListener(Mutex mutex) + { + _mutex = mutex; + + Task.Run(async () => + { + while (!_cancellationTokenSource.IsCancellationRequested) + { + try + { + Info("Listening for connections"); + + var pipeServer = new NamedPipeServerStream( + AutoJoinClientConnection.PipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous + ); + + try + { + await pipeServer.WaitForConnectionAsync(_cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + pipeServer.Dispose(); + break; + } + + Info("Client connected"); + + var client = new AutoJoinServerConnection(pipeServer); + Clients.Add(client); + client.Disconnected += () => + { + Clients.Remove(client); + client.Dispose(); + }; + } + catch (Exception e) + { + Error(e); + } + } + + Info("Stopped"); + Stopped?.Invoke(); + }); + } + + public event Action? Stopped; + + public List Clients { get; } = new(); + + public void Dispose() + { + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + + foreach (var client in Clients) + { + client.Dispose(); + } + + Clients.Clear(); + + _mutex.Dispose(); + } + + public void SendJoinMe(InnerNetClient innerNetClient) + { + var joinGameMessage = JoinGameMessage.From(innerNetClient); + + foreach (var client in Clients) + { + client.Write(joinGameMessage); + } + } + + public static bool TryStart([NotNullWhen(true)] out AutoJoinConnectionListener? connectionListener) + { + var mutex = new Mutex(false, $@"Global\{AutoJoinClientConnection.PipeName}"); + + if (mutex.WaitOne(100)) + { + connectionListener = new AutoJoinConnectionListener(mutex); + return true; + } + + mutex.Dispose(); + + connectionListener = null; + return false; + } +} diff --git a/Reactor.Debugger/AutoJoin/AutoJoinConnectionManager.cs b/Reactor.Debugger/AutoJoin/AutoJoinConnectionManager.cs new file mode 100644 index 0000000..df962cb --- /dev/null +++ b/Reactor.Debugger/AutoJoin/AutoJoinConnectionManager.cs @@ -0,0 +1,66 @@ +using HarmonyLib; +using InnerNet; + +namespace Reactor.Debugger.AutoJoin; + +internal static class AutoJoinConnectionManager +{ + public static AutoJoinConnectionListener? Server { get; set; } + public static AutoJoinClientConnection? Client { get; set; } + + public static void StartOrConnect() + { + if (AutoJoinConnectionListener.TryStart(out var connectionListener)) + { + Info("Started an AutoJoin session"); + Server = connectionListener; + Server.Stopped += () => + { + if (Server == connectionListener) + { + Server = null; + } + }; + } + else if (AutoJoinClientConnection.TryConnect(out var client)) + { + Info("Connected to an AutoJoin session"); + Client = client; + client.Disconnected += () => + { + if (Client == client) + { + Client = null; + + Info("Disconnected from an AutoJoin session, trying to reconnect"); + StartOrConnect(); + } + }; + } + else + { + Error("Failed to start or connect to an AutoJoin session"); + } + } + + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.Start))] + public static class ConnectPatch + { + public static void Postfix(InnerNetClient __instance) + { + if (!DebuggerConfig.JoinGameOnStart.Value) return; + + if (__instance.TryCast() is { } amongUsClient) + { + if (Server != null) + { + amongUsClient.StartCoroutine(amongUsClient.CoCreateOnlineGame()); + } + else if (Client is { } client) + { + client.SendRequestJoin(); + } + } + } + } +} diff --git a/Reactor.Debugger/AutoJoin/AutoJoinServerConnection.cs b/Reactor.Debugger/AutoJoin/AutoJoinServerConnection.cs new file mode 100644 index 0000000..ae581fb --- /dev/null +++ b/Reactor.Debugger/AutoJoin/AutoJoinServerConnection.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.IO.Pipes; +using Reactor.Debugger.AutoJoin.Messages; + +namespace Reactor.Debugger.AutoJoin; + +internal sealed class AutoJoinServerConnection : AutoJoinConnection +{ + public AutoJoinServerConnection(NamedPipeServerStream pipe) : base(pipe) + { + } + + protected override void Handle(BinaryReader reader, MessageType messageType) + { + switch (messageType) + { + case MessageType.RequestJoinGame: + { + Handle(RequestJoinGameMessage.Deserialize(reader)); + break; + } + + default: + { + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, null); + } + } + } + + private void Handle(in RequestJoinGameMessage message) + { + if (AmongUsClient.Instance.AmConnected) + { + Write(JoinGameMessage.From(AmongUsClient.Instance)); + } + } +} diff --git a/Reactor.Debugger/AutoJoin/Messages/Extensions.cs b/Reactor.Debugger/AutoJoin/Messages/Extensions.cs new file mode 100644 index 0000000..598f3c2 --- /dev/null +++ b/Reactor.Debugger/AutoJoin/Messages/Extensions.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace Reactor.Debugger.AutoJoin.Messages; + +internal static class Extensions +{ + public static void Write(this BinaryWriter writer, in T message) where T : IMessage + { + writer.Write((byte) T.Type); + message.Serialize(writer); + } +} diff --git a/Reactor.Debugger/AutoJoin/Messages/IMessage.cs b/Reactor.Debugger/AutoJoin/Messages/IMessage.cs new file mode 100644 index 0000000..6b238cd --- /dev/null +++ b/Reactor.Debugger/AutoJoin/Messages/IMessage.cs @@ -0,0 +1,15 @@ +#pragma warning disable CA2252 // TODO Remove after BepInEx updates the .NET version +using System.IO; + +namespace Reactor.Debugger.AutoJoin.Messages; + +internal interface IMessage where TSelf : IMessage +{ + static abstract MessageType Type { get; } + + void Serialize(BinaryWriter writer); + + static abstract TSelf Deserialize(BinaryReader reader); + + string ToString(); +} diff --git a/Reactor.Debugger/AutoJoin/Messages/JoinGameMessage.cs b/Reactor.Debugger/AutoJoin/Messages/JoinGameMessage.cs new file mode 100644 index 0000000..13ba499 --- /dev/null +++ b/Reactor.Debugger/AutoJoin/Messages/JoinGameMessage.cs @@ -0,0 +1,40 @@ +using System.IO; +using InnerNet; + +namespace Reactor.Debugger.AutoJoin.Messages; + +internal readonly record struct JoinGameMessage( + string Address, + ushort Port, + int GameCode +) : IMessage +{ + public static MessageType Type => MessageType.JoinGame; + + public void Serialize(BinaryWriter writer) + { + writer.Write(Address); + writer.Write(Port); + writer.Write(GameCode); + } + + public static JoinGameMessage Deserialize(BinaryReader reader) + { + return new JoinGameMessage + { + Address = reader.ReadString(), + Port = reader.ReadUInt16(), + GameCode = reader.ReadInt32(), + }; + } + + public static JoinGameMessage From(InnerNetClient innerNetClient) + { + return new JoinGameMessage + { + Address = innerNetClient.networkAddress, + Port = (ushort) innerNetClient.networkPort, + GameCode = innerNetClient.GameId, + }; + } +} diff --git a/Reactor.Debugger/AutoJoin/Messages/MessageType.cs b/Reactor.Debugger/AutoJoin/Messages/MessageType.cs new file mode 100644 index 0000000..dccde3b --- /dev/null +++ b/Reactor.Debugger/AutoJoin/Messages/MessageType.cs @@ -0,0 +1,7 @@ +namespace Reactor.Debugger.AutoJoin.Messages; + +internal enum MessageType : byte +{ + JoinGame, + RequestJoinGame, +} diff --git a/Reactor.Debugger/AutoJoin/Messages/RequestJoinGameMessage.cs b/Reactor.Debugger/AutoJoin/Messages/RequestJoinGameMessage.cs new file mode 100644 index 0000000..bea2eba --- /dev/null +++ b/Reactor.Debugger/AutoJoin/Messages/RequestJoinGameMessage.cs @@ -0,0 +1,17 @@ +using System.IO; + +namespace Reactor.Debugger.AutoJoin.Messages; + +internal readonly record struct RequestJoinGameMessage : IMessage +{ + public static MessageType Type => MessageType.RequestJoinGame; + + public void Serialize(BinaryWriter writer) + { + } + + public static RequestJoinGameMessage Deserialize(BinaryReader reader) + { + return default; + } +} diff --git a/Reactor.Debugger/DebuggerConfig.cs b/Reactor.Debugger/DebuggerConfig.cs new file mode 100644 index 0000000..3ae44de --- /dev/null +++ b/Reactor.Debugger/DebuggerConfig.cs @@ -0,0 +1,26 @@ +using BepInEx.Configuration; + +namespace Reactor.Debugger; + +internal static class DebuggerConfig +{ + public const string FeaturesSection = "Features"; + public const string AutoJoinSection = "AutoJoin"; + + public static ConfigEntry DisableGameEnd { get; private set; } = null!; + public static ConfigEntry RedirectLogger { get; private set; } = null!; + public static ConfigEntry AutoPlayAgain { get; private set; } = null!; + public static ConfigEntry DisableTimeout { get; private set; } = null!; + + public static ConfigEntry JoinGameOnStart { get; private set; } = null!; + + public static void Bind(ConfigFile config) + { + DisableGameEnd = config.Bind(FeaturesSection, nameof(DisableGameEnd), false, "Stops the game from ending regardless of conditions"); + RedirectLogger = config.Bind(FeaturesSection, nameof(RedirectLogger), false, "Redirect base game Logger calls into BepInEx logging"); + AutoPlayAgain = config.Bind(FeaturesSection, nameof(AutoPlayAgain), true, "Automatically calls Play Again after game ends"); + DisableTimeout = config.Bind(FeaturesSection, nameof(DisableTimeout), false, "Disable the network disconnection timeout"); + + JoinGameOnStart = config.Bind(AutoJoinSection, nameof(JoinGameOnStart), false, "Automatically hosts/joins a game on start"); + } +} diff --git a/Reactor.Debugger/DebuggerPlugin.cs b/Reactor.Debugger/DebuggerPlugin.cs index ff6e0ae..9ce45d8 100644 --- a/Reactor.Debugger/DebuggerPlugin.cs +++ b/Reactor.Debugger/DebuggerPlugin.cs @@ -1,16 +1,10 @@ -using System; -using System.Linq; -using AmongUs.Data; -using AmongUs.GameOptions; +global using static Reactor.Utilities.Logger; using BepInEx; using BepInEx.Unity.IL2CPP; using HarmonyLib; -using Il2CppInterop.Runtime.Attributes; -using Il2CppInterop.Runtime.InteropTypes.Arrays; -using InnerNet; -using Reactor.Utilities.Attributes; -using Reactor.Utilities.ImGui; -using UnityEngine; +using Reactor.Debugger.AutoJoin; +using Reactor.Debugger.Patches; +using Reactor.Debugger.Window; namespace Reactor.Debugger; @@ -20,107 +14,17 @@ namespace Reactor.Debugger; public partial class DebuggerPlugin : BasePlugin { public Harmony Harmony { get; } = new(Id); - public DebuggerComponent Component { get; private set; } = null!; public override void Load() { - Component = this.AddComponent(); + DebuggerConfig.Bind(Config); - var maxImpostors = (Il2CppStructArray) Enumerable.Repeat((int) byte.MaxValue, byte.MaxValue).ToArray(); - GameOptionsData.MaxImpostors = GameOptionsData.RecommendedImpostors = maxImpostors; - NormalGameOptionsV07.MaxImpostors = NormalGameOptionsV07.RecommendedImpostors = maxImpostors; - HideNSeekGameOptionsV07.MaxImpostors = maxImpostors; + this.AddComponent(); - var minPlayers = (Il2CppStructArray) Enumerable.Repeat(1, byte.MaxValue).ToArray(); - GameOptionsData.MinPlayers = minPlayers; - NormalGameOptionsV07.MinPlayers = minPlayers; - HideNSeekGameOptionsV07.MinPlayers = minPlayers; + GameOptionsPatches.Initialize(); Harmony.PatchAll(); - } - [RegisterInIl2Cpp] - public class DebuggerComponent : MonoBehaviour - { - [HideFromIl2Cpp] - public bool DisableGameEnd { get; set; } - - [HideFromIl2Cpp] - public DragWindow TestWindow { get; } - - public DebuggerComponent(IntPtr ptr) : base(ptr) - { - TestWindow = new DragWindow(new Rect(20, 20, 0, 0), "Debugger", () => - { - GUILayout.Label("Name: " + DataManager.Player.Customization.Name); - DisableGameEnd = GUILayout.Toggle(DisableGameEnd, "Disable game end"); - - if (ShipStatus.Instance && AmongUsClient.Instance.AmHost) - { - if (GUILayout.Button("Force game end")) - { - ShipStatus.Instance.enabled = false; - GameManager.Instance.RpcEndGame(GameOverReason.ImpostorDisconnect, false); - } - - if (GUILayout.Button("Call a meeting")) - { - PlayerControl.LocalPlayer.CmdReportDeadBody(null); - } - } - - if (TutorialManager.InstanceExists && PlayerControl.LocalPlayer) - { - var data = PlayerControl.LocalPlayer.Data; - - var newIsImpostor = GUILayout.Toggle(data.Role.IsImpostor, "Is Impostor"); - if (data.Role.IsImpostor != newIsImpostor) - { - PlayerControl.LocalPlayer.RpcSetRole(newIsImpostor ? RoleTypes.Impostor : RoleTypes.Crewmate); - } - - if (GUILayout.Button("Spawn a dummy")) - { - var playerControl = Instantiate(TutorialManager.Instance.PlayerPrefab); - var i = playerControl.PlayerId = (byte) GameData.Instance.GetAvailableId(); - GameData.Instance.AddPlayer(playerControl); - AmongUsClient.Instance.Spawn(playerControl, -2, SpawnFlags.None); - playerControl.transform.position = PlayerControl.LocalPlayer.transform.position; - playerControl.GetComponent().enabled = true; - playerControl.NetTransform.enabled = false; - playerControl.SetName($"{TranslationController.Instance.GetString(StringNames.Dummy, Array.Empty())} {i}"); - var color = (byte) (i % Palette.PlayerColors.Length); - playerControl.SetColor(color); - playerControl.SetHat(HatManager.Instance.allHats[i % HatManager.Instance.allHats.Count].ProdId, playerControl.Data.DefaultOutfit.ColorId); - playerControl.SetPet(HatManager.Instance.allPets[i % HatManager.Instance.allPets.Count].ProdId); - playerControl.SetSkin(HatManager.Instance.allSkins[i % HatManager.Instance.allSkins.Count].ProdId, color); - GameData.Instance.RpcSetTasks(playerControl.PlayerId, new Il2CppStructArray(0)); - } - } - - if (PlayerControl.LocalPlayer) - { - var position = PlayerControl.LocalPlayer.transform.position; - GUILayout.Label($"x: {position.x}"); - GUILayout.Label($"y: {position.y}"); - } - }) - { - Enabled = false, - }; - } - - private void Update() - { - if (Input.GetKeyDown(KeyCode.F1)) - { - TestWindow.Enabled = !TestWindow.Enabled; - } - } - - private void OnGUI() - { - TestWindow.OnGUI(); - } + AutoJoinConnectionManager.StartOrConnect(); } } diff --git a/Reactor.Debugger/Patches.cs b/Reactor.Debugger/Patches.cs deleted file mode 100644 index 3ce26a9..0000000 --- a/Reactor.Debugger/Patches.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Linq; -using AmongUs.GameOptions; -using HarmonyLib; -using Il2CppInterop.Runtime.InteropTypes.Arrays; -using Reactor.Utilities; -using UnityEngine; - -namespace Reactor.Debugger; - -internal static class Patches -{ - [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Update))] - public static class UpdatePatch - { - public static void Prefix(GameStartManager __instance) - { - __instance.MinPlayers = 1; - } - } - - [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Update))] - public static class CountdownPatch - { - public static void Prefix(GameStartManager __instance) - { - __instance.countDownTimer = 0; - } - } - - [HarmonyPatch(typeof(StatsManager), nameof(StatsManager.AmBanned), MethodType.Getter)] - public static class AmBannedPatch - { - public static void Postfix(out bool __result) - { - __result = false; - } - } - - [HarmonyPatch] - [HarmonyPriority(Priority.First)] - public static class CheckEndCriteriaPatch - { - [HarmonyPatch(typeof(LogicGameFlow), nameof(LogicGameFlow.CheckEndCriteria))] - [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlow.CheckEndCriteria))] - [HarmonyPatch(typeof(LogicGameFlowHnS), nameof(LogicGameFlow.CheckEndCriteria))] - public static bool Prefix() - { - return !PluginSingleton.Instance.Component.DisableGameEnd; - } - } - - [HarmonyPatch(typeof(GameSettingMenu), nameof(GameSettingMenu.Start))] - public static class GameSettingMenuPatch - { - public static void Prefix(GameSettingMenu __instance) - { - // Unlocks map/impostor amount changing in online (for testing on your custom servers) - __instance.HideForOnline = new Il2CppReferenceArray(0); - } - } - - [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.Start))] - [HarmonyPriority(Priority.First)] - public static class GameOptionsMenuPatch - { - public static void Postfix(GameOptionsMenu __instance) - { - if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.Normal) return; - - __instance.Children - .Single(o => o.Title == StringNames.GameNumImpostors) - .Cast() - .ValidRange = new FloatRange(0, byte.MaxValue); - } - } -} diff --git a/Reactor.Debugger/Patches/AutoPlayAgainPatch.cs b/Reactor.Debugger/Patches/AutoPlayAgainPatch.cs new file mode 100644 index 0000000..2c7ef84 --- /dev/null +++ b/Reactor.Debugger/Patches/AutoPlayAgainPatch.cs @@ -0,0 +1,14 @@ +using HarmonyLib; + +namespace Reactor.Debugger.Patches; + +[HarmonyPatch(typeof(EndGameNavigation), nameof(EndGameNavigation.ShowDefaultNavigation))] +internal static class AutoPlayAgainPatch +{ + public static void Postfix(EndGameNavigation __instance) + { + if (!DebuggerConfig.AutoPlayAgain.Value) return; + + __instance.NextGame(); + } +} diff --git a/Reactor.Debugger/Patches/DisableGameEndPatch.cs b/Reactor.Debugger/Patches/DisableGameEndPatch.cs new file mode 100644 index 0000000..3ead7b9 --- /dev/null +++ b/Reactor.Debugger/Patches/DisableGameEndPatch.cs @@ -0,0 +1,18 @@ +using HarmonyLib; + +namespace Reactor.Debugger.Patches; + +[HarmonyPatch] +[HarmonyPriority(Priority.First)] +internal static class DisableGameEndPatch +{ + private static bool Enabled => DebuggerConfig.DisableGameEnd.Value; + + [HarmonyPatch(typeof(LogicGameFlow), nameof(LogicGameFlow.CheckEndCriteria))] + [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlow.CheckEndCriteria))] + [HarmonyPatch(typeof(LogicGameFlowHnS), nameof(LogicGameFlow.CheckEndCriteria))] + public static bool Prefix() + { + return !Enabled; + } +} diff --git a/Reactor.Debugger/Patches/DisableTimeoutPatch.cs b/Reactor.Debugger/Patches/DisableTimeoutPatch.cs new file mode 100644 index 0000000..eeef3c4 --- /dev/null +++ b/Reactor.Debugger/Patches/DisableTimeoutPatch.cs @@ -0,0 +1,27 @@ +using HarmonyLib; +using Hazel.Udp; + +namespace Reactor.Debugger.Patches; + +[HarmonyPatch] +internal static class DisableTimeoutPatch +{ + private static bool Enabled => DebuggerConfig.DisableTimeout.Value; + + [HarmonyPatch(typeof(UdpConnection), nameof(UdpConnection.HandleKeepAlive))] + [HarmonyPrefix] + public static bool DisableKeepAlive() + { + return !Enabled; + } + + [HarmonyPatch(typeof(UnityUdpClientConnection), nameof(UnityUdpClientConnection.ConnectAsync))] + [HarmonyPostfix] + public static void DisableTimeout(UnityUdpClientConnection __instance) + { + if (!Enabled) return; + + __instance.DisconnectTimeoutMs = int.MaxValue; + __instance.ResendLimit = 0; + } +} diff --git a/Reactor.Debugger/Patches/GameOptionsPatches.cs b/Reactor.Debugger/Patches/GameOptionsPatches.cs new file mode 100644 index 0000000..be7f0e4 --- /dev/null +++ b/Reactor.Debugger/Patches/GameOptionsPatches.cs @@ -0,0 +1,70 @@ +using System.Linq; +using AmongUs.GameOptions; +using HarmonyLib; +using Il2CppInterop.Runtime; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Il2CppSystem.Reflection; +using UnityEngine; + +namespace Reactor.Debugger.Patches; + +[HarmonyPatch] +internal static class GameOptionsPatches +{ + public static void Initialize() + { + var maxImpostors = (Il2CppStructArray) Enumerable.Repeat((int) byte.MaxValue, byte.MaxValue).ToArray(); + GameOptionsData.MaxImpostors = maxImpostors; + NormalGameOptionsV07.MaxImpostors = maxImpostors; + HideNSeekGameOptionsV07.MaxImpostors = maxImpostors; + + var minPlayers = (Il2CppStructArray) Enumerable.Repeat(1, byte.MaxValue).ToArray(); + GameOptionsData.MinPlayers = minPlayers; + NormalGameOptionsV07.MinPlayers = minPlayers; + HideNSeekGameOptionsV07.MinPlayers = minPlayers; + } + + [HarmonyPatch(typeof(GameSettingMenu), nameof(GameSettingMenu.Start))] + [HarmonyPrefix] + public static void UnlockAllOptions(GameSettingMenu __instance) + { + __instance.HideForOnline = new Il2CppReferenceArray(0); + } + + [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.OnEnable))] + [HarmonyPostfix] + public static void UnlockOptionRange(NumberOption __instance) + { + __instance.ValidRange.min = float.MinValue; + __instance.ValidRange.max = float.MaxValue; + } + + [HarmonyPatch(typeof(CreateOptionsPicker), nameof(CreateOptionsPicker.SetImpostorButtons))] + public static class DisableImpostorCountReset + { + private static readonly MethodInfo _refreshMethod = Il2CppType.Of().GetMethod("Refresh", BindingFlags.Public | BindingFlags.Instance); + + public static bool Prefix() + { + foreach (var stackFrame in new Il2CppSystem.Diagnostics.StackTrace().GetFrames()) + { + if (_refreshMethod.Equals(stackFrame.GetMethod())) + { + return false; + } + } + + return true; + } + } + + [HarmonyPatch(typeof(CrewVisualizer), nameof(CrewVisualizer.SetCrewSize))] + [HarmonyPrefix] + public static void SetCrewSizePatch(int numPlayers, ref int numImpostors) + { + if (numImpostors >= numPlayers) + { + numImpostors = 0; + } + } +} diff --git a/Reactor.Debugger/Patches/GameStartManagerPatches.cs b/Reactor.Debugger/Patches/GameStartManagerPatches.cs new file mode 100644 index 0000000..f197e14 --- /dev/null +++ b/Reactor.Debugger/Patches/GameStartManagerPatches.cs @@ -0,0 +1,21 @@ +using HarmonyLib; + +namespace Reactor.Debugger.Patches; + +[HarmonyPatch(typeof(GameStartManager))] +internal static class GameStartManagerPatches +{ + [HarmonyPatch(nameof(GameStartManager.Start))] + [HarmonyPrefix] + public static void StartPatch(GameStartManager __instance) + { + __instance.MinPlayers = 1; + } + + [HarmonyPatch(nameof(GameStartManager.Update))] + [HarmonyPrefix] + public static void UpdatePatch(GameStartManager __instance) + { + __instance.countDownTimer = 0; + } +} diff --git a/Reactor.Debugger/Patches/MiscellaneousPatches.cs b/Reactor.Debugger/Patches/MiscellaneousPatches.cs new file mode 100644 index 0000000..d355cf8 --- /dev/null +++ b/Reactor.Debugger/Patches/MiscellaneousPatches.cs @@ -0,0 +1,14 @@ +using HarmonyLib; + +namespace Reactor.Debugger.Patches; + +[HarmonyPatch] +internal static class MiscellaneousPatches +{ + [HarmonyPatch(typeof(StatsManager), nameof(StatsManager.AmBanned), MethodType.Getter)] + [HarmonyPrefix] + public static void AmBannedPatch(out bool __result) + { + __result = false; + } +} diff --git a/Reactor.Debugger/Patches/RandomizeFreeplayServerPortPatch.cs b/Reactor.Debugger/Patches/RandomizeFreeplayServerPortPatch.cs new file mode 100644 index 0000000..8acf245 --- /dev/null +++ b/Reactor.Debugger/Patches/RandomizeFreeplayServerPortPatch.cs @@ -0,0 +1,39 @@ +using System; +using System.Net; +using HarmonyLib; +using InnerNet; + +namespace Reactor.Debugger.Patches; + +[HarmonyPatch] +internal static class RandomizeFreeplayServerPortPatch +{ + private static ushort? _lastPort; + + [HarmonyPatch(typeof(InnerNetServer), nameof(InnerNetServer.StartAsLocalServer))] + public static class StartServerPatch + { + public static void Prefix(InnerNetServer __instance) + { + var port = (ushort) Random.Shared.Next(1024, IPEndPoint.MaxPort); + _lastPort = port; + __instance.Port = port; + } + + public static void Postfix(InnerNetServer __instance) + { + __instance.Port = Constants.GamePlayPort; + } + } + + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.SetEndpoint))] + [HarmonyPrefix] + public static void SetEndpointPatch(InnerNetClient __instance, string addr, ref ushort port) + { + if (_lastPort != null && __instance.NetworkMode == NetworkModes.FreePlay) + { + port = _lastPort.Value; + _lastPort = null; + } + } +} diff --git a/Reactor.Debugger/Patches/RedirectLoggerPatch.cs b/Reactor.Debugger/Patches/RedirectLoggerPatch.cs new file mode 100644 index 0000000..0e02897 --- /dev/null +++ b/Reactor.Debugger/Patches/RedirectLoggerPatch.cs @@ -0,0 +1,74 @@ +using System; +using System.Globalization; +using System.Text; +using BepInEx.Logging; +using HarmonyLib; +using Object = UnityEngine.Object; + +namespace Reactor.Debugger.Patches; + +[HarmonyPatch(typeof(Logger))] +internal static class RedirectLoggerPatch +{ + private static readonly ManualLogSource _log = BepInEx.Logging.Logger.CreateLogSource("Among Us"); + + private static bool Enabled => DebuggerConfig.RedirectLogger.Value; + + private static void Log(Logger logger, Logger.Level level, Il2CppSystem.Object message, Object? context = null) + { + var finalMessage = new StringBuilder(); + + if (logger.category != Logger.Category.None) finalMessage.Append(CultureInfo.InvariantCulture, $"[{logger.category}] "); + if (!string.IsNullOrEmpty(logger.tag)) finalMessage.Append(CultureInfo.InvariantCulture, $"[{logger.tag}] "); + if (context != null) finalMessage.Append(CultureInfo.InvariantCulture, $"[{context.name} ({context.GetIl2CppType().FullName})]"); + finalMessage.Append(CultureInfo.InvariantCulture, $" {message.ToString()} "); + + _log.Log( + level switch + { + Logger.Level.Debug => LogLevel.Debug, + Logger.Level.Error => LogLevel.Error, + Logger.Level.Warning => LogLevel.Warning, + Logger.Level.Info => LogLevel.Info, + _ => throw new ArgumentOutOfRangeException(nameof(level), level, null), + }, + finalMessage + ); + } + + [HarmonyPatch(nameof(Logger.Debug))] + [HarmonyPrefix] + public static bool DebugPatch(Logger __instance, Il2CppSystem.Object message, Object? context) + { + if (!Enabled) return true; + Log(__instance, Logger.Level.Debug, message, context); + return false; + } + + [HarmonyPatch(nameof(Logger.Info))] + [HarmonyPrefix] + public static bool InfoPatch(Logger __instance, Il2CppSystem.Object message, Object? context) + { + if (!Enabled) return true; + Log(__instance, Logger.Level.Info, message, context); + return false; + } + + [HarmonyPatch(nameof(Logger.Warning))] + [HarmonyPrefix] + public static bool WarningPatch(Logger __instance, Il2CppSystem.Object message, Object? context) + { + if (!Enabled) return true; + Log(__instance, Logger.Level.Warning, message, context); + return false; + } + + [HarmonyPatch(nameof(Logger.Error))] + [HarmonyPrefix] + public static bool ErrorPatch(Logger __instance, Il2CppSystem.Object message, Object? context) + { + if (!Enabled) return true; + Log(__instance, Logger.Level.Error, message, context); + return false; + } +} diff --git a/Reactor.Debugger/Utilities/Extensions.cs b/Reactor.Debugger/Utilities/Extensions.cs new file mode 100644 index 0000000..cfa9ce6 --- /dev/null +++ b/Reactor.Debugger/Utilities/Extensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using Il2CppInterop.Runtime; +using InnerNet; + +namespace Reactor.Debugger.Utilities; + +internal static class Extensions +{ + public static IEnumerator CoCreateLocalGame(this AmongUsClient client) + { + try + { + client.NetworkMode = NetworkModes.LocalGame; + + InnerNetServer.Instance.StartAsServer(); + + client.SetEndpoint(Constants.LocalNetAddress, Constants.GamePlayPort, false); + client.MainMenuScene = "MatchMaking"; + client.OnlineScene = "OnlineGame"; + } + catch (Il2CppException e) + { + Error(e); + DisconnectPopup.Instance.ShowCustom(e.Message[..e.Message.IndexOf("\n--- BEGIN IL2CPP STACK TRACE ---\n", StringComparison.Ordinal)]); + MatchMaker.Instance.NotConnecting(); + yield break; + } + + client.Connect(MatchMakerModes.HostAndClient, null); + + yield return client.WaitForConnectionOrFail(); + + DestroyableSingleton.Instance.NotConnecting(); + } +} diff --git a/Reactor.Debugger/Window/DebuggerWindow.cs b/Reactor.Debugger/Window/DebuggerWindow.cs new file mode 100644 index 0000000..460098b --- /dev/null +++ b/Reactor.Debugger/Window/DebuggerWindow.cs @@ -0,0 +1,70 @@ +using System; +using AmongUs.Data; +using Il2CppInterop.Runtime.Attributes; +using Reactor.Debugger.Window.Tabs; +using Reactor.Utilities.Attributes; +using Reactor.Utilities.ImGui; +using UnityEngine; + +namespace Reactor.Debugger.Window; + +[RegisterInIl2Cpp] +internal sealed class DebuggerWindow : MonoBehaviour +{ + private readonly DragWindow _window; + + [HideFromIl2Cpp] + public BaseTab[] Tabs { get; } = + { + new ConfigTab(), + new GameTab(), + new AutoJoinTab(), + }; + + [HideFromIl2Cpp] + public BaseTab SelectedTab { get; private set; } + + public DebuggerWindow(IntPtr ptr) : base(ptr) + { + SelectedTab = Tabs[0]; + + _window = new DragWindow(new Rect(20, 20, 0, 0), "Reactor.Debugger", () => + { + var clientName = DataManager.Player.Customization.Name; + if (AmongUsClient.Instance && AmongUsClient.Instance.AmHost) clientName += " (host)"; + GUILayout.Label("Name: " + clientName); + + GUILayout.BeginHorizontal(); + { + foreach (var tab in Tabs) + { + if (GUILayout.Toggle(tab == SelectedTab, tab.Name, new GUIStyle(GUI.skin.button))) + { + SelectedTab = tab; + } + } + } + GUILayout.EndHorizontal(); + + GUILayout.Space(5f); + + SelectedTab.OnGUI(); + }) + { + Enabled = false, + }; + } + + public void Update() + { + if (Input.GetKeyDown(KeyCode.F1)) + { + _window.Enabled = !_window.Enabled; + } + } + + public void OnGUI() + { + _window.OnGUI(); + } +} diff --git a/Reactor.Debugger/Window/Tabs/AutoJoinTab.cs b/Reactor.Debugger/Window/Tabs/AutoJoinTab.cs new file mode 100644 index 0000000..1de7203 --- /dev/null +++ b/Reactor.Debugger/Window/Tabs/AutoJoinTab.cs @@ -0,0 +1,68 @@ +using BepInEx.Unity.IL2CPP.Utils; +using Reactor.Debugger.AutoJoin; +using Reactor.Debugger.Utilities; +using UnityEngine; + +namespace Reactor.Debugger.Window.Tabs; + +internal sealed class AutoJoinTab : BaseTab +{ + public override string Name => "AutoJoin"; + + public override void OnGUI() + { + if (AutoJoinConnectionManager.Server is { } server) + { + GUILayout.Label($"Hosting ({server.Clients.Count} clients)"); + + if (GUILayout.Button("Stop")) + { + server.Dispose(); + } + + if (AmongUsClient.Instance) + { + if (GUILayout.Button("Create an online game")) + { + AmongUsClient.Instance.StartCoroutine(AmongUsClient.Instance.CoCreateOnlineGame()); + } + + if (GUILayout.Button("Create a local game")) + { + AmongUsClient.Instance.StartCoroutine(AmongUsClient.Instance.CoCreateLocalGame()); + } + + if ( + AmongUsClient.Instance.AmConnected && + AmongUsClient.Instance.NetworkMode != NetworkModes.FreePlay && + GUILayout.Button("Join me") + ) + { + server.SendJoinMe(AmongUsClient.Instance); + } + } + } + else if (AutoJoinConnectionManager.Client is { } client) + { + GUILayout.Label("Connected"); + + if (GUILayout.Button("Disconnect")) + { + AutoJoinConnectionManager.Client = null; + client.Dispose(); + } + + if (GUILayout.Button("Request join")) + { + client.SendRequestJoin(); + } + } + else + { + if (GUILayout.Button("Connect")) + { + AutoJoinConnectionManager.StartOrConnect(); + } + } + } +} diff --git a/Reactor.Debugger/Window/Tabs/BaseTab.cs b/Reactor.Debugger/Window/Tabs/BaseTab.cs new file mode 100644 index 0000000..0ea24da --- /dev/null +++ b/Reactor.Debugger/Window/Tabs/BaseTab.cs @@ -0,0 +1,8 @@ +namespace Reactor.Debugger.Window.Tabs; + +internal abstract class BaseTab +{ + public abstract string Name { get; } + + public abstract void OnGUI(); +} diff --git a/Reactor.Debugger/Window/Tabs/ConfigTab.cs b/Reactor.Debugger/Window/Tabs/ConfigTab.cs new file mode 100644 index 0000000..5746f72 --- /dev/null +++ b/Reactor.Debugger/Window/Tabs/ConfigTab.cs @@ -0,0 +1,29 @@ +using System.Linq; +using BepInEx.Configuration; +using Reactor.Utilities; +using UnityEngine; + +namespace Reactor.Debugger.Window.Tabs; + +internal sealed class ConfigTab : BaseTab +{ + public override string Name => "Config"; + + public override void OnGUI() + { + var configs = PluginSingleton.Instance.Config.Concat(PluginSingleton.Instance.Config); + + foreach (var section in configs.GroupBy(e => e.Key.Section)) + { + GUILayout.Label(section.Key); + + foreach (var (definition, entry) in section) + { + if (entry is ConfigEntry booleanEntry) + { + booleanEntry.Value = GUILayout.Toggle(booleanEntry.Value, definition.Key); + } + } + } + } +} diff --git a/Reactor.Debugger/Window/Tabs/GameTab.cs b/Reactor.Debugger/Window/Tabs/GameTab.cs new file mode 100644 index 0000000..7747bae --- /dev/null +++ b/Reactor.Debugger/Window/Tabs/GameTab.cs @@ -0,0 +1,95 @@ +using AmongUs.GameOptions; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Reactor.Debugger.Window.Tabs; + +internal sealed class GameTab : BaseTab +{ + public override string Name => "Game"; + + public override void OnGUI() + { + var localPlayer = PlayerControl.LocalPlayer; + if (!localPlayer) + { + return; + } + + var amHost = AmongUsClient.Instance.AmHost; + + var position = localPlayer.transform.position; + GUILayout.Label($"Position: ({position.x:F3}, {position.y:F3})"); + + var physics = localPlayer.MyPhysics; + GUILayout.Label($"Speed: {physics.Speed}"); + physics.Speed = GUILayout.HorizontalSlider(physics.Speed, 0, 25); + + localPlayer.Collider.enabled = GUILayout.Toggle(localPlayer.Collider.enabled, "Collisions"); + + if (ShipStatus.Instance) + { + if (amHost && GUILayout.Button("Force game end")) + { + ShipStatus.Instance.enabled = false; + GameManager.Instance.RpcEndGame(GameOverReason.ImpostorDisconnect, false); + } + + if (MeetingHud.Instance) + { + if (GUILayout.Button("Stop the meeting")) + { + MeetingHud.Instance.RpcClose(); + } + } + else + { + if (GUILayout.Button("Call a meeting")) + { + localPlayer.CmdReportDeadBody(null); + } + } + + if (TutorialManager.InstanceExists) + { + var newIsImpostor = GUILayout.Toggle(localPlayer.Data.Role.IsImpostor, "Is Impostor"); + if (localPlayer.Data.Role.IsImpostor != newIsImpostor) + { + localPlayer.RpcSetRole(newIsImpostor ? RoleTypes.Impostor : RoleTypes.Crewmate); + } + + if (GUILayout.Button("Spawn a dummy")) + { + SpawnDummy(); + } + } + } + } + + private static void SpawnDummy() + { + var playerControl = Object.Instantiate(TutorialManager.Instance.PlayerPrefab); + var playerId = playerControl.PlayerId = (byte) GameData.Instance.GetAvailableId(); + + GameData.Instance.AddPlayer(playerControl); + AmongUsClient.Instance.Spawn(playerControl); + + playerControl.transform.position = PlayerControl.LocalPlayer.transform.position; + playerControl.GetComponent().enabled = true; + playerControl.NetTransform.enabled = false; + + playerControl.SetName($"{TranslationController.Instance.GetString(StringNames.Dummy)} {playerId}"); + + var color = (byte) (playerId % Palette.PlayerColors.Length); + playerControl.SetColor(color); + playerControl.SetHat(HatManager.Instance.allHats[playerId % HatManager.Instance.allHats.Count].ProdId, playerControl.Data.DefaultOutfit.ColorId); + playerControl.SetPet(HatManager.Instance.allPets[playerId % HatManager.Instance.allPets.Count].ProdId); + playerControl.SetSkin(HatManager.Instance.allSkins[playerId % HatManager.Instance.allSkins.Count].ProdId, color); + playerControl.SetVisor(HatManager.Instance.allVisors[playerId % HatManager.Instance.allVisors.Count].ProdId, color); + playerControl.SetNamePlate(HatManager.Instance.allNamePlates[playerId % HatManager.Instance.allNamePlates.Count].ProdId); + playerControl.Data.PlayerLevel = playerId; + + GameData.Instance.RpcSetTasks(playerControl.PlayerId, new Il2CppStructArray(0)); + } +}