MosswartMassacre/MosswartMassacre.Loader/LoaderCore.cs
2025-06-22 12:10:15 +02:00

264 lines
No EOL
8.8 KiB
C#

using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Win32;
using Decal.Adapter;
namespace MosswartMassacre.Loader
{
[FriendlyName("MosswartMassacre.Loader")]
public class LoaderCore : FilterBase
{
private Assembly pluginAssembly;
private Type pluginType;
private object pluginInstance;
private FileSystemWatcher pluginWatcher;
private bool isSubscribedToRenderFrame = false;
private bool needsReload;
public static string PluginAssemblyNamespace => "MosswartMassacre";
public static string PluginAssemblyName => $"{PluginAssemblyNamespace}.dll";
public static string PluginAssemblyGuid => "{8C97E839-4D05-4A5F-B0C8-E8E778654322}";
public static bool IsPluginLoaded { get; private set; }
/// <summary>
/// Assembly directory (contains both loader and plugin dlls)
/// </summary>
public static string AssemblyDirectory => System.IO.Path.GetDirectoryName(Assembly.GetAssembly(typeof(LoaderCore)).Location);
public DateTime LastDllChange { get; private set; }
#region Event Handlers
protected override void Startup()
{
try
{
Core.PluginInitComplete += Core_PluginInitComplete;
Core.PluginTermComplete += Core_PluginTermComplete;
Core.FilterInitComplete += Core_FilterInitComplete;
// Set up assembly resolution for hot-loaded plugin dependencies
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
// watch the AssemblyDirectory for any .dll file changes
pluginWatcher = new FileSystemWatcher();
pluginWatcher.Path = AssemblyDirectory;
pluginWatcher.NotifyFilter = NotifyFilters.LastWrite;
pluginWatcher.Filter = "*.dll";
pluginWatcher.Changed += PluginWatcher_Changed;
pluginWatcher.EnableRaisingEvents = true;
}
catch (Exception ex)
{
Log(ex);
}
}
private void Core_FilterInitComplete(object sender, EventArgs e)
{
Core.EchoFilter.ClientDispatch += EchoFilter_ClientDispatch;
}
private void EchoFilter_ClientDispatch(object sender, NetworkMessageEventArgs e)
{
try
{
// Login_SendEnterWorldRequest
if (e.Message.Type == 0xF7C8)
{
//EnsurePluginIsDisabledInRegistry();
}
}
catch (Exception ex)
{
Log(ex);
}
}
private void Core_PluginInitComplete(object sender, EventArgs e)
{
try
{
LoadPluginAssembly();
}
catch (Exception ex)
{
Log(ex);
}
}
private void Core_PluginTermComplete(object sender, EventArgs e)
{
try
{
UnloadPluginAssembly();
}
catch (Exception ex)
{
Log(ex);
}
}
protected override void Shutdown()
{
try
{
Core.PluginInitComplete -= Core_PluginInitComplete;
Core.PluginTermComplete -= Core_PluginTermComplete;
Core.FilterInitComplete -= Core_FilterInitComplete;
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
UnloadPluginAssembly();
}
catch (Exception ex)
{
Log(ex);
}
}
private void Core_RenderFrame(object sender, EventArgs e)
{
try
{
if (IsPluginLoaded && needsReload && DateTime.UtcNow - LastDllChange > TimeSpan.FromSeconds(1))
{
needsReload = false;
Core.RenderFrame -= Core_RenderFrame;
isSubscribedToRenderFrame = false;
LoadPluginAssembly();
}
}
catch (Exception ex)
{
Log(ex);
}
}
private void PluginWatcher_Changed(object sender, FileSystemEventArgs e)
{
try
{
// Only reload if it's the main plugin DLL
if (e.Name == PluginAssemblyName)
{
LastDllChange = DateTime.UtcNow;
needsReload = true;
if (!isSubscribedToRenderFrame)
{
isSubscribedToRenderFrame = true;
Core.RenderFrame += Core_RenderFrame;
}
}
}
catch (Exception ex)
{
Log(ex);
}
}
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
// Extract just the assembly name (without version info)
string assemblyName = args.Name.Split(',')[0] + ".dll";
string assemblyPath = System.IO.Path.Combine(AssemblyDirectory, assemblyName);
// If the dependency exists in our plugin directory, load it
if (File.Exists(assemblyPath))
{
return Assembly.LoadFrom(assemblyPath);
}
}
catch (Exception ex)
{
Log($"AssemblyResolve failed for {args.Name}: {ex.Message}");
}
// Return null to let default resolution continue
return null;
}
#endregion
#region Plugin Loading/Unloading
internal void LoadPluginAssembly()
{
try
{
if (IsPluginLoaded)
{
UnloadPluginAssembly();
try
{
CoreManager.Current.Actions.AddChatText($"[MosswartMassacre] Reloading {PluginAssemblyName}", 5);
}
catch { }
}
pluginAssembly = Assembly.Load(File.ReadAllBytes(System.IO.Path.Combine(AssemblyDirectory, PluginAssemblyName)));
pluginType = pluginAssembly.GetType($"{PluginAssemblyNamespace}.PluginCore");
pluginInstance = Activator.CreateInstance(pluginType);
// Set the AssemblyDirectory property if it exists
var assemblyDirProperty = pluginType.GetProperty("AssemblyDirectory", BindingFlags.Public | BindingFlags.Static);
assemblyDirProperty?.SetValue(null, AssemblyDirectory);
// Set the IsHotReload flag if it exists
var isHotReloadProperty = pluginType.GetProperty("IsHotReload", BindingFlags.Public | BindingFlags.Static);
isHotReloadProperty?.SetValue(null, true);
// The original template doesn't set up Host - it just calls Startup
// The plugin should use CoreManager.Current.Actions instead of MyHost for hot reload scenarios
// We'll set a flag so the plugin knows it's being hot loaded
// Call Startup method
var startupMethod = pluginType.GetMethod("Startup", BindingFlags.NonPublic | BindingFlags.Instance);
startupMethod.Invoke(pluginInstance, new object[] { });
IsPluginLoaded = true;
}
catch (Exception ex)
{
Log(ex);
}
}
private void UnloadPluginAssembly()
{
try
{
if (pluginInstance != null && pluginType != null)
{
MethodInfo shutdownMethod = pluginType.GetMethod("Shutdown", BindingFlags.NonPublic | BindingFlags.Instance);
shutdownMethod.Invoke(pluginInstance, null);
pluginInstance = null;
pluginType = null;
pluginAssembly = null;
}
IsPluginLoaded = false;
}
catch (Exception ex)
{
Log(ex);
}
}
#endregion
private void Log(Exception ex)
{
Log(ex.ToString());
}
private void Log(string message)
{
File.AppendAllText(System.IO.Path.Combine(AssemblyDirectory, "loader_log.txt"), $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}\n");
try
{
CoreManager.Current.Actions.AddChatText($"[MosswartMassacre.Loader] {message}", 3);
}
catch { }
}
}
}