namespace Aeshnidae.RemoteConsole; /// /// /mod-load: bring ONE new mod online without touching any other - the restart-free way to add /// a mod to live (Tom, 2026-09-28: "whatever we can do to minimize restarts"). /// /// ACE's own `mod find` is FindMods: it disables every mod, builds a fresh container for every /// folder and enables them all again. ACE never unloads the old assemblies, so a mod holding /// state in memory - InstancesNoDat with copies open - ends up split between its old and new /// assembly, and live has treated `mod find` as restart-grade since 2026-09-20. A brand-new mod /// therefore cost a restart. /// /// This does for one folder exactly what FindMods does per folder - read its Meta.json into a /// ModContainer, Initialize (the plugin loader), Enable (assembly, instance, Initialize, commands) /// - and adds that container to ModManager's list, where `mod list`, `mod enable/disable` and /// every sibling mod's GetModContainerByName find it. No other container is disabled, rebuilt or /// enabled. The list is ModManager's private `Mods` property, reached by reflection; the list is /// re-sorted by Priority afterwards as FindMods sorts it. /// /// It refuses a folder ACE already knows (that is `mod enable`/`mod disable`, or a rebuild's hot /// reload), one without a Meta.json or dll, and a Meta.json that says Enabled false. /// public static class ModLoad { [CommandHandler("mod-load", AccessLevel.Admin, CommandHandlerFlag.None, 1, "Load and enable ONE new mod folder without reloading any other mod (instead of `mod find`, which reloads them all). Console-safe.", "/mod-load e.g. /mod-load Aeshnidae.Foo")] public static void HandleModLoad(Session session, params string[] parameters) { var name = string.Join(" ", parameters).Trim(); Reply(session, Load(name)); } public static string Load(string name) { if (string.IsNullOrWhiteSpace(name) || name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || name.Contains("..")) return "Give the mod's folder name, e.g. /mod-load Aeshnidae.Foo"; var folder = Path.Combine(ModManager.ModPath, name); var metaPath = Path.Combine(folder, ModMetadata.FILENAME); if (!Directory.Exists(folder)) return $"No folder {folder}."; if (ModManager.GetModContainerByPath(folder) is { } known) return $"{name} is already known to ACE ({known.Status}). Use `mod enable {name}` / `mod disable {name}`; a rebuilt dll hot-reloads by itself."; if (!File.Exists(metaPath)) return $"No {ModMetadata.FILENAME} in {folder}."; ModMetadata? meta; try { meta = JsonSerializer.Deserialize(File.ReadAllText(metaPath), ConfigManager.SerializerOptions); } catch (Exception ex) { return $"{metaPath} could not be read: {ex.Message}"; } if (meta is null) return $"{metaPath} is empty."; if (!meta.Enabled) return $"{name}'s Meta.json says Enabled false; set it true first."; if (ModManager.GetModContainerByName(meta.Name, allowPartial: false) is { } sameName) return $"A mod named {meta.Name} is already loaded from {sameName.FolderPath}."; var list = ModList(); if (list is null) return "Could not reach ModManager's mod list (ACE changed?) - nothing was loaded. `mod find` plus a restart is the fallback."; var container = new ModContainer { Meta = meta, FolderPath = folder }; if (!File.Exists(container.DllPath)) return $"No {Path.GetFileName(container.DllPath)} in {folder} - build it first."; try { container.Initialize(); lock (list) { list.Add(container); list.Sort((a, b) => b.Meta.Priority.CompareTo(a.Meta.Priority)); } container.Enable(); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] mod-load {name} failed: {ex}", ModManager.LogLevel.Error); return $"{name}: loading threw {ex.GetType().Name}: {ex.Message} (status {container.Status}). See the log."; } var line = $"{name} v{meta.Version}: {container.Status}" + (container.Status == ModStatus.Active ? " - loaded on its own; no other mod was touched." : " - see the log for why it is not active."); ModManager.Log($"[{Mod.Name}] mod-load: {line}"); return line; } /// ModManager's private `Mods` list - the one FindMods fills and every lookup reads. private static List? ModList() { try { return AccessTools.Property(typeof(ModManager), "Mods")?.GetValue(null) as List; } catch { return null; } } private static void Reply(Session? session, string message) { if (session?.Player is null) ModManager.Log(message); else session.Player.SendMessage(message); } }