namespace Aeshnidae.InstancesNoDat; /// /// Dungeon instances that keep the source landblock id, so no client dat is ever /// touched. See InstanceWorld for why that is possible, and Context for the one /// question the design turns on. /// /// Experimental. Off by default. /// public class Mod : IHarmonyMod { public static readonly string Name = typeof(Mod).Assembly.GetName().Name!; public static readonly string HarmonyId = $"mod.{Name.ToLowerInvariant()}"; public static readonly string ModPath = Path.Combine(ModManager.ModPath, Name); public static Settings Settings { get; private set; } = new(); /// /// Parsed instanced-dungeon config, by landblock. Cached because the entry hooks /// consult it on every teleport on the server, not just instanced ones. /// public static Dictionary Instanced { get; private set; } = new(); public static bool IsInstanced(ushort landblock) => Instanced.ContainsKey(landblock); public static InstanceScope ScopeFor(ushort landblock) => Instanced.TryGetValue(landblock, out var entry) ? entry.Scope : InstanceScope.Personal; public static string NameFor(ushort landblock) => Instanced.TryGetValue(landblock, out var entry) && !string.IsNullOrWhiteSpace(entry.Name) ? entry.Name : $"{landblock:X4}"; /// /// Re-reads the instanced dungeon list. /// /// Deliberately does NOT use ACE's Player.NoLog_Landblocks, which moves anyone who /// logged out there to their lifestone before they are placed. That was one answer /// to "these dungeons have no public version", but handing them a fresh copy is the /// better one - you come back where you were rather than across the world. It also /// would not have worked for the people most likely to test it: HandleNoLogLandblock /// returns early for Admin and Sentinel characters. /// /// /// Staff are never handed a copy automatically - not on login, not on walking in, /// not after a reload. An admin in an instanced dungeon is usually authoring it, and /// the master is the only version that can be authored, so quietly moving them into /// a private copy would break the one workflow that matters and look like the edit /// simply failed. /inst enter is how staff ask for a copy on purpose. /// public static bool IsStaff(Player player) => (player.Session?.AccessLevel ?? AccessLevel.Player) >= AccessLevel.Admin; /// A routing trace line, when Settings.LogRouting is on. public static void Trace(string message) { if (Settings.LogRouting) ModManager.Log($"[{Name}] {message}"); } public static void ReloadInstanced() => Instanced = Settings.InstancedMap(); private static Harmony? _harmony; private bool _disposed; public void Initialize() { Settings = Settings.Load(ModPath); ReloadInstanced(); _harmony = new Harmony(HarmonyId); // Explicit assembly: the parameterless overload walks the stack, and an // inlined Initialize() makes it resolve to ACE.Server and patch nothing. _harmony.PatchAllUncategorized(typeof(Mod).Assembly); ModManager.Log($"[{Name}] loaded - {(Settings.Enabled ? "ENABLED" : "disabled")}, " + $"up to {Settings.MaxCopiesPerLandblock} copies per landblock, " + $"{Instanced.Count} instanced dungeon(s)" + $"{(Settings.InteriorsOnly ? ", interiors only" : ", INTERIORS-ONLY RAIL IS OFF")}"); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { try { // Copies exist only in this mod's memory; leaving them behind would // strand anyone standing in one. Players are left where they stand // rather than recalled: the incoming assembly's first tick restores // them to a fresh copy, and being briefly in a drained master beats a // trip to the lifestone every time the mod is rebuilt. InstanceWorld.Clear(ejectPlayers: false); } catch (Exception ex) { ModManager.Log($"[{Name}] failed to close instances on shutdown: {ex}", ModManager.LogLevel.Error); } _harmony?.UnpatchAll(HarmonyId); _harmony = null; ModManager.Log($"[{Name}] shut down"); } _disposed = true; } }