diff --git a/src/JordanMod/Plugin.cs b/src/JordanMod/Plugin.cs index 029fdaf..112caae 100644 --- a/src/JordanMod/Plugin.cs +++ b/src/JordanMod/Plugin.cs @@ -1,16 +1,98 @@ -using BepInEx; +using System; +using System.Collections.Generic; +using BepInEx; using BepInEx.Logging; +using HarmonyLib; +using JordanMod.Events; +using JordanMod.Modules; namespace JordanMod; [BepInAutoPlugin] public partial class Plugin : BaseUnityPlugin { + public static Plugin Instance { get; private set; } = null!; internal static ManualLogSource Log { get; private set; } = null!; + private static Harmony? _harmony; + private ModuleManager? _moduleManager; + + private static readonly List _globalPatches = []; private void Awake() { + Instance = this; + + ConfigHandler.Initialize(Config); Log = Logger; - Log.LogInfo($"Plugin {Name} is loaded!"); + Debug.Log("JordanMod is starting..."); + SetupEvents(); + SetupModules(); + SetupPatches(); + } + + protected void SetupEvents() + { + GlobalEventListener.Initialize(); + } + + protected void SetupModules() + { + _moduleManager = new ModuleManager(); + _moduleManager.Initialize(); + } + + protected void SetupPatches() + { + _harmony ??= new Harmony(Info.Metadata.GUID); + Type[] patches = [.. _globalPatches]; + if (_moduleManager != null) + { + foreach (Module module in _moduleManager.GetAllModules()) + { + patches = [.. patches, .. module.GetPatches()]; + } + } + + foreach (var patchType in patches) + { + try + { + _harmony.PatchAll(patchType); + } + catch (Exception e) + { + Log.LogError($"Failed to patch {patchType.Name}: {e}"); + } + } + } + + protected void RemovePatches() + { + if (_harmony == null) return; + try + { + _harmony.UnpatchSelf(); + _harmony = null; + } + catch (Exception e) + { + Log.LogError($"Failed to remove patches: {e}"); + } + } + + private void Update() + { + _moduleManager?.Update(); + } + + private void FixedUpdate() + { + _moduleManager?.FixedUpdate(); + } + + private void OnDestroy() + { + RemovePatches(); + _moduleManager?.Destroy(); } } diff --git a/src/JordanMod/events/GlobalEventListener.cs b/src/JordanMod/events/GlobalEventListener.cs new file mode 100644 index 0000000..ca52d97 --- /dev/null +++ b/src/JordanMod/events/GlobalEventListener.cs @@ -0,0 +1,10 @@ +namespace JordanMod.Events; + +public class GlobalEventListener +{ + + public static void Initialize() + { + } + +} diff --git a/src/JordanMod/modules/ModuleManager.cs b/src/JordanMod/modules/ModuleManager.cs new file mode 100644 index 0000000..20bc140 --- /dev/null +++ b/src/JordanMod/modules/ModuleManager.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using ExitGames.Client.Photon; +using Photon.Pun; +using Photon.Realtime; +using UnityEngine; + +namespace JordanMod.Modules; + +public class ModuleManager +{ + private readonly Dictionary _modules = []; + private event Action? OnInitialize; + private event Action? OnUpdate; + private event Action? OnFixedUpdate; + private event Action? OnDestroy; + + public void Initialize() + { + AddNetworkEventListener(); + LoadModules(); + Plugin.Log.LogInfo($"ModuleManager initialized with {_modules.Count} modules"); + } + + public List GetAllModules() + { + return [.. _modules.Values]; + } + + private void AddNetworkEventListener() + { + GameObject listenerObject = new("PhotonNetworkEventListener"); + UnityEngine.Object.DontDestroyOnLoad(listenerObject); + listenerObject.AddComponent(); + } + + private void LoadModules() + { + // Auto-discover all types with the [Module] attribute + var moduleTypes = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(Module))) + .Select(t => new { Type = t, Attribute = t.GetCustomAttribute() }) + .Where(x => x.Attribute != null) + .ToList(); + + Plugin.Log.LogInfo($"Discovered {moduleTypes.Count} module(s) with [Module] attribute"); + + foreach (var moduleInfo in moduleTypes) + { + try + { + Module module = (Module)Activator.CreateInstance(moduleInfo.Type)!; + + if (!moduleInfo.Attribute!.Enabled) + continue; + + _modules[module.ModuleName] = module; + + if (HasOverriddenMethod(moduleInfo.Type, nameof(Module.Initialize))) + OnInitialize += module.Initialize; + + if (HasOverriddenMethod(moduleInfo.Type, nameof(Module.Update))) + OnUpdate += module.Update; + + if (HasOverriddenMethod(moduleInfo.Type, nameof(Module.FixedUpdate))) + OnFixedUpdate += module.FixedUpdate; + + if (HasOverriddenMethod(moduleInfo.Type, nameof(Module.Destroy))) + OnDestroy += module.Destroy; + + Plugin.Log.LogInfo($"Loaded module: {module.ModuleName}"); + } + catch (Exception e) + { + Plugin.Log.LogError($"Failed to load module {moduleInfo.Type.Name}: {e}"); + } + } + + OnInitialize?.Invoke(); + } + + private static bool HasOverriddenMethod(Type type, string methodName) + { + var method = type.GetMethod(methodName); + return method != null && method.DeclaringType != typeof(Module); + } + + public void Update() + { + OnUpdate?.Invoke(); + } + + public void FixedUpdate() + { + OnFixedUpdate?.Invoke(); + } + + public void Destroy() + { + OnDestroy?.Invoke(); + + _modules.Clear(); + OnInitialize = null; + OnUpdate = null; + OnFixedUpdate = null; + OnDestroy = null; + } + + public Module? GetModule(string moduleName) + { + _modules.TryGetValue(moduleName, out Module? module); + return module; + } + +} + +public abstract class Module +{ + public abstract string ModuleName { get; } + + public virtual void Initialize() + { + Plugin.Log.LogInfo($"Module '{ModuleName}' initialized"); + } + + public virtual void Update() + { + } + + public virtual void FixedUpdate() + { + } + + public virtual Type[] GetPatches() + { + return []; + } + + public virtual void Destroy() + { + Plugin.Log.LogInfo($"Module '{ModuleName}' destroyed"); + } + +} + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public class ModuleAttribute : Attribute +{ + public bool Enabled { get; set; } = true; +} + +class PhotonNetworkEventListener : MonoBehaviourPunCallbacks +{ + public static PhotonNetworkEventListener? Instance { get; private set; } + + public override void OnEnable() + { + if (Instance != null) return; + Instance = this; + base.OnEnable(); + } + + public event Action? Connected; + public event Action? LeftRoom; + public event Action? MasterClientSwitched; + public event Action? CreateRoomFailed; + public event Action? JoinRoomFailed; + public event Action? CreatedRoom; + public event Action? JoinedLobby; + public event Action? LeftLobby; + public event Action? Disconnected; + public event Action? RegionListReceived; + public event Action>? RoomListUpdate; + public event Action? JoinedRoom; + public event Action? PlayerEnteredRoom; + public event Action? PlayerLeftRoom; + public event Action? JoinRandomFailed; + public event Action? ConnectedToMaster; + public event Action? RoomPropertiesUpdate; + public event Action? PlayerPropertiesUpdate; + public event Action>? FriendListUpdate; + public event Action>? CustomAuthenticationResponse; + public event Action? CustomAuthenticationFailed; + public event Action? WebRpcResponse; + public event Action>? LobbyStatisticsUpdate; + public event Action? ErrorInfoEvent; + + // Register methods + public void RegisterOnConnected(Action handler) => Connected += handler; + public void RegisterOnLeftRoom(Action handler) => LeftRoom += handler; + public void RegisterOnMasterClientSwitched(Action handler) => MasterClientSwitched += handler; + public void RegisterOnCreateRoomFailed(Action handler) => CreateRoomFailed += handler; + public void RegisterOnJoinRoomFailed(Action handler) => JoinRoomFailed += handler; + public void RegisterOnCreatedRoom(Action handler) => CreatedRoom += handler; + public void RegisterOnJoinedLobby(Action handler) => JoinedLobby += handler; + public void RegisterOnLeftLobby(Action handler) => LeftLobby += handler; + public void RegisterOnDisconnected(Action handler) => Disconnected += handler; + public void RegisterOnRegionListReceived(Action handler) => RegionListReceived += handler; + public void RegisterOnRoomListUpdate(Action> handler) => RoomListUpdate += handler; + public void RegisterOnJoinedRoom(Action handler) => JoinedRoom += handler; + public void RegisterOnPlayerEnteredRoom(Action handler) => PlayerEnteredRoom += handler; + public void RegisterOnPlayerLeftRoom(Action handler) => PlayerLeftRoom += handler; + public void RegisterOnJoinRandomFailed(Action handler) => JoinRandomFailed += handler; + public void RegisterOnConnectedToMaster(Action handler) => ConnectedToMaster += handler; + public void RegisterOnRoomPropertiesUpdate(Action handler) => RoomPropertiesUpdate += handler; + public void RegisterOnPlayerPropertiesUpdate(Action handler) => PlayerPropertiesUpdate += handler; + public void RegisterOnFriendListUpdate(Action> handler) => FriendListUpdate += handler; + public void RegisterOnCustomAuthenticationResponse(Action> handler) => CustomAuthenticationResponse += handler; + public void RegisterOnCustomAuthenticationFailed(Action handler) => CustomAuthenticationFailed += handler; + public void RegisterOnWebRpcResponse(Action handler) => WebRpcResponse += handler; + public void RegisterOnLobbyStatisticsUpdate(Action> handler) => LobbyStatisticsUpdate += handler; + public void RegisterOnErrorInfo(Action handler) => ErrorInfoEvent += handler; + + // Override and invoke actions + public override void OnConnected() + { + Connected?.Invoke(); + } + + public override void OnLeftRoom() + { + LeftRoom?.Invoke(); + } + + public override void OnMasterClientSwitched(Photon.Realtime.Player newMasterClient) + { + MasterClientSwitched?.Invoke(newMasterClient); + } + + public override void OnCreateRoomFailed(short returnCode, string message) + { + CreateRoomFailed?.Invoke(returnCode, message); + } + + public override void OnJoinRoomFailed(short returnCode, string message) + { + JoinRoomFailed?.Invoke(returnCode, message); + } + + public override void OnCreatedRoom() + { + CreatedRoom?.Invoke(); + } + + public override void OnJoinedLobby() + { + JoinedLobby?.Invoke(); + } + + public override void OnLeftLobby() + { + LeftLobby?.Invoke(); + } + + public override void OnDisconnected(DisconnectCause cause) + { + Disconnected?.Invoke(cause); + } + + public override void OnRegionListReceived(RegionHandler regionHandler) + { + RegionListReceived?.Invoke(regionHandler); + } + + public override void OnRoomListUpdate(List roomList) + { + RoomListUpdate?.Invoke(roomList); + } + + public override void OnJoinedRoom() + { + JoinedRoom?.Invoke(); + } + + public override void OnPlayerEnteredRoom(Photon.Realtime.Player newPlayer) + { + PlayerEnteredRoom?.Invoke(newPlayer); + } + + public override void OnPlayerLeftRoom(Photon.Realtime.Player otherPlayer) + { + PlayerLeftRoom?.Invoke(otherPlayer); + } + + public override void OnJoinRandomFailed(short returnCode, string message) + { + JoinRandomFailed?.Invoke(returnCode, message); + } + + public override void OnConnectedToMaster() + { + ConnectedToMaster?.Invoke(); + } + + public override void OnRoomPropertiesUpdate(ExitGames.Client.Photon.Hashtable propertiesThatChanged) + { + RoomPropertiesUpdate?.Invoke(propertiesThatChanged); + } + + public override void OnPlayerPropertiesUpdate(Photon.Realtime.Player targetPlayer, ExitGames.Client.Photon.Hashtable changedProps) + { + PlayerPropertiesUpdate?.Invoke(targetPlayer, changedProps); + } + + public override void OnFriendListUpdate(List friendList) + { + FriendListUpdate?.Invoke(friendList); + } + + public override void OnCustomAuthenticationResponse(Dictionary data) + { + CustomAuthenticationResponse?.Invoke(data); + } + + public override void OnCustomAuthenticationFailed(string debugMessage) + { + CustomAuthenticationFailed?.Invoke(debugMessage); + } + + public override void OnWebRpcResponse(OperationResponse response) + { + WebRpcResponse?.Invoke(response); + } + + public override void OnLobbyStatisticsUpdate(List lobbyStatistics) + { + LobbyStatisticsUpdate?.Invoke(lobbyStatistics); + } + + public override void OnErrorInfo(ErrorInfo errorInfo) + { + ErrorInfoEvent?.Invoke(errorInfo); + } + +} \ No newline at end of file diff --git a/src/JordanMod/modules/_example/ExampleModule.cs b/src/JordanMod/modules/_example/ExampleModule.cs new file mode 100644 index 0000000..5efa9cd --- /dev/null +++ b/src/JordanMod/modules/_example/ExampleModule.cs @@ -0,0 +1,34 @@ +using System; + +namespace JordanMod.Modules.Example; + +[Module(Enabled = false)] +class ExampleModule : Module +{ + public override string ModuleName => "Example Module"; + + public override Type[] GetPatches() + { + return [typeof(Patches.ExamplePatch)]; + } + + public override void Initialize() + { + base.Initialize(); + } + + public override void Update() + { + base.Update(); + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + } + + public override void Destroy() + { + base.Destroy(); + } +} \ No newline at end of file diff --git a/src/JordanMod/patches/ExamplePatch.cs b/src/JordanMod/patches/ExamplePatch.cs new file mode 100644 index 0000000..458c8ac --- /dev/null +++ b/src/JordanMod/patches/ExamplePatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; + +namespace JordanMod.Patches; + +public class ExamplePatch +{ + [HarmonyPatch(typeof(Item), "Start")] + [HarmonyPostfix] + static void PostItemStartFix(Item __instance) + { + if (__instance.UIData.itemName.ToLower() == "passport") + { + __instance.UIData.canDrop = true; + __instance.UIData.canThrow = true; + } + } +} \ No newline at end of file