using ACE.Server.Factories; namespace Aeshnidae.InstancesNoDat; /// /// /inst-keep: the shared-copy keep (Keep.cs) from the console - its counters, and the steps /// to prove it without a client: put an item on a shared copy's ground, close the copy (it /// saves), open it again (it loads), and load the real landblock (it must not). /// public static class KeepCommands { [CommandHandler("inst-keep", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Shared copies keep what's left in them, like retail landblocks: status, and console tests. Console-safe.", "/inst-keep on/off, saved/loaded counts, kept rows in the database, open shared copies\n" + "/inst-keep drop [beside] test: open the shared copy and put that item on its ground, beside the named object\n" + "/inst-keep close close the shared copy now (it saves what it keeps)\n" + "/inst-keep open open the shared copy (it loads what it kept)\n" + "/inst-keep find is that object in the shared copy? and in the real landblock (loaded to look)?\n" + "A layer's copy (InstancedLandblock.Layers) is named :, e.g. BAE8:2.")] public static void HandleKeep(Session session, params string[] parameters) { var verb = parameters.Length > 0 ? parameters[0].ToLowerInvariant() : ""; if (verb == "") { Status(session); return; } if (parameters.Length < 2 || !TryLandblock(parameters[1], out var landblock)) { Reply(session, "Give a hex landblock id, e.g. /inst-keep open 8A02."); return; } // : names a layer's copy; plain is the one shared copy. var sharedCopy = InstanceWorld.SharedCopy; if (parameters[1].Split(':') is { Length: 2 } split && int.TryParse(split[1], out var named)) sharedCopy = named; if (!Mod.IsSharedCopy(landblock, sharedCopy)) { Reply(session, $"copy {sharedCopy} of {landblock:X4} is not a shared copy (a Shared landblock's copy 1, or a layer's); only shared copies keep things."); return; } switch (verb) { case "open": OnWorld(() => { var before = Keep.Loaded; var block = InstanceWorld.GetOrCreate(landblock, sharedCopy); Later(3, () => Reply(session, block is null ? $"could not open the shared copy of {landblock:X4}" : $"shared copy of {landblock:X4} open: {InstanceWorld.ObjectCount(block)} object(s), {Keep.Loaded - before} kept object(s) loaded")); }); break; case "close": OnWorld(() => { var before = Keep.Saved; var closed = InstanceWorld.Close(landblock, sharedCopy); Reply(session, closed ? $"shared copy of {landblock:X4} closed; {Keep.Saved - before} object(s) saved" : $"the shared copy of {landblock:X4} was not open"); }); break; case "drop": if (parameters.Length < 3 || !uint.TryParse(parameters[2], out var wcid)) { Reply(session, "/inst-keep drop "); return; } OnWorld(() => { var block = InstanceWorld.GetOrCreate(landblock, sharedCopy); if (block is null) { Reply(session, $"could not open the shared copy of {landblock:X4}"); return; } // Once the copy has populated: set the item down where something already stands. var beside = parameters.Length > 3 ? string.Join(" ", parameters.Skip(3)) : null; Later(3, () => Reply(session, Drop(block, wcid, beside))); }); break; case "find": if (parameters.Length < 3 || !uint.TryParse(parameters[2].Replace("0x", "", StringComparison.OrdinalIgnoreCase), System.Globalization.NumberStyles.HexNumber, null, out var guid)) { Reply(session, "/inst-keep find "); return; } OnWorld(() => { var master = LandblockManager.GetLandblock(new LandblockId((uint)landblock << 16 | 0xFFFF), false); Later(3, () => { var copy = InstanceWorld.Find(landblock, sharedCopy); var inCopy = copy is not null && InstanceWorld.ObjectsOf(copy).Any(o => o.Guid.Full == guid); var inMaster = master?.GetObject(new ObjectGuid(guid)) is not null; Reply(session, $"0x{guid:X8}: in the shared copy {(copy is null ? "(not open)" : inCopy ? "YES" : "no")}; " + $"in the real {landblock:X4} {(master is null ? "(could not load)" : inMaster ? "YES - leaked!" : "no")}; " + $"real landblocks have skipped {Keep.SkippedInMaster} kept row(s) so far"); }); }); break; default: Reply(session, "/inst-keep [drop|close|open|find] - see /help inst-keep"); break; } } [CommandHandler("inst-shell", AccessLevel.Admin, CommandHandlerFlag.None, 1, "Build a terrain-only shell for a landblock (what a region's unopened landblock is at the seam) and report its ground. Console-safe test.", "/inst-shell e.g. /inst-shell BBEA")] public static void HandleShell(Session session, params string[] parameters) { if (!TryLandblock(parameters[0], out var landblock)) { Reply(session, "Give a hex landblock id, e.g. /inst-shell BBEA."); return; } OnWorld(() => { try { Reply(session, Regions.TestShell(landblock) + $"; shells built in regions since load: {Regions.ShellsBuilt}"); } catch (Exception ex) { Reply(session, $"shell {landblock:X4} failed: {ex}"); } }); } private static string Drop(Landblock block, uint wcid, string? beside) { var objects = InstanceWorld.ObjectsOf(block).Where(o => o is not Player && !o.IsGenerator && o.Location is not null).ToList(); // A portal or an NPC stands where a player can stand; a monster nest may not. var anchor = (beside is null ? null : objects.FirstOrDefault(o => o.Name.Equals(beside, StringComparison.OrdinalIgnoreCase))) ?? objects.FirstOrDefault(o => o is Portal) ?? objects.FirstOrDefault(o => o is Creature c && !c.Attackable) ?? objects.FirstOrDefault(o => o is Creature); if (anchor is null) return $"the shared copy of {block.Id.Landblock:X4} has nothing to set the item beside yet - try again"; var wo = WorldObjectFactory.CreateNewWorldObject(wcid); if (wo is null) return $"wcid {wcid} does not exist"; wo.Location = new Position(anchor.Location); wo.Location.PositionZ += 0.05f; // Scattered a few metres, as a generator places things: physics tries spots round the // anchor until one is clear, and finds the indoor cell it lands in. wo.ScatterPos = new ACE.Server.Physics.Common.SetPosition(new ACE.Server.Physics.Common.Position(wo.Location), ACE.Server.Physics.Common.SetPositionFlags.RandomScatter, 3f); var previous = Context.Ambient; Context.Ambient = block; bool ok; try { ok = wo.EnterWorld(); } finally { Context.Ambient = previous; wo.ScatterPos = null; } if (!ok) { wo.Destroy(); return $"{wo.Name} could not be placed beside {anchor.Name}"; } return $"{wo.Name} 0x{wo.Guid.Full:X8} set down in the shared copy of {block.Id.Landblock:X4} beside {anchor.Name} ({wo.Location.ToLOCString()}); " + $"persists: {wo.IsDynamicThatShouldPersistToShard()}"; } private static void Status(Session? session) { var rows = -1; try { using var ctx = new ACE.Database.Models.Shard.ShardDbContext(); rows = ctx.BiotaPropertiesInt.Count(p => p.Type == (ushort)Keep.Tag && p.Value != 0); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] inst-keep: could not count kept rows: {ex.Message}", ModManager.LogLevel.Warn); } var sb = new StringBuilder(); sb.AppendLine($"Keep {(Keep.On ? "ON" : "off")} (KeepSharedCopies {Mod.Settings.KeepSharedCopies}): since load {Keep.Saved} saved, {Keep.Loaded} loaded, " + $"{Keep.SkippedInMaster} skipped by real landblocks; {rows} object(s) kept in the database"); foreach (var (key, block) in InstanceWorld.All) { if (!Keep.IsShared(block)) continue; var objects = InstanceWorld.ObjectsOf(block); sb.AppendLine($" shared copy {key.Copy} of {key.Landblock:X4}: {objects.Count} object(s), {objects.Count(o => o is not Player && o.IsDynamicThatShouldPersistToShard())} it would keep"); } Reply(session, sb.ToString()); } private static bool TryLandblock(string text, out ushort landblock) { text = text.Trim().Replace("0x", "", StringComparison.OrdinalIgnoreCase); if (text.Length > 4) text = text[..4]; return ushort.TryParse(text, System.Globalization.NumberStyles.HexNumber, null, out landblock); } private static void OnWorld(Action action) => WorldManager.EnqueueAction(new ActionEventDelegate(action)); private static void Later(double seconds, Action action) { var chain = new ActionChain(); chain.AddDelaySeconds(seconds); chain.AddAction(WorldManager.ActionQueue, action); chain.EnqueueChain(); } private static void Reply(Session? session, string message) { if (session?.Player is null) { foreach (var line in (message ?? "").Split('\n')) if (!string.IsNullOrWhiteSpace(line)) ModManager.Log(line.TrimEnd('\r')); return; } foreach (var line in (message ?? "").Split('\n')) if (!string.IsNullOrWhiteSpace(line)) session.Player.SendMessage(line.TrimEnd('\r')); } }