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