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; } /// /// Assembly directory (contains both loader and plugin dlls) /// 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 { } } } }