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);
}
}