using ACE.Server.Factories;
using ShardBiota = ACE.Database.Models.Shard.Biota;
namespace Aeshnidae.InstancesNoDat;
///
/// Shared copies keep what is left in them, as a retail landblock does (Tom, 2026-09-27:
/// "modify server-wide instances to behave the same as retail landblocks. Retaining items").
///
/// A real landblock saves its persistent objects - player corpses, items a player dropped:
/// ACE's IsDynamicThatShouldPersistToShard - to the shard database every five minutes and
/// when it unloads, and loads them back by landblock id when it loads (SpawnDynamicShardObjects).
/// Copies did neither (Patches.PreSaveDB, PreSpawnDynamicShardObjects), because a copy shares
/// its landblock's id: what it saved would reappear in the real landblock. That is right for a
/// personal copy, which is thrown away. A Shared copy - the Hub of Awakening, the Trials Hub,
/// the Aerlinthe island - is one place for everyone that is meant to last, so it now saves and
/// loads like ACE, with a label:
///
/// Tag (PropertyInt 9044 = N) on a saved object means "this lies in shared copy N of the
/// landblock in its location", not in the real landblock. A Shared landblock has one
/// shared copy, number 1 (InstanceWorld.SharedCopy), and one more per layer
/// (InstancedLandblock.Layers, 2026-09-28): the tag's value says which, so a corpse on the
/// PK island never turns up on the other one. Rows saved before layers carry 1.
///
/// Save: a shared copy tags and saves its persistent objects where ACE would save a real
/// landblock's - the timed save, the copy (or one landblock of a region) closing, and the
/// server shutting down.
/// Load: a shared copy loads only tagged objects; a real landblock skips them.
///
/// The tag lives only in the database, while the object lies on the copy's ground: it is
/// taken off the object in memory as it is loaded, so a tagged item a player picks up is
/// saved into their pack WITHOUT it and can never vanish from a real landblock later.
///
public static class Keep
{
public const PropertyInt Tag = (PropertyInt)9044;
public static bool On => Mod.Settings.Enabled && Mod.Settings.KeepSharedCopies;
/// Saved and loaded since the mod loaded, for /inst status and the log.
public static int Saved, Loaded, SkippedInMaster;
/// A shared copy of a landblock (its one, or a layer), by key - for a copy already taken out of the table (Close).
public static bool IsSharedKey(ushort landblock, int copy) =>
Mod.IsSharedCopy(landblock, copy);
/// One of our copies, and a shared one of its landblock (its one, or a layer).
public static bool IsShared(Landblock? block) =>
block is not null
&& InstanceWorld.IsOurs(block)
&& Mod.IsSharedCopy((ushort)block.Id.Landblock, InstanceWorld.CopyOf(block));
/// The shared copy a saved object lies in (its tag), or 0 for the real landblock.
private static int TagOf(ShardBiota biota) =>
biota.BiotaPropertiesInt?.FirstOrDefault(p => p.Type == (ushort)Tag)?.Value ?? 0;
///
/// Tag and save every persistent object of a shared copy, as ACE's Landblock.SaveDB saves
/// a real landblock's (containers with their contents). World thread. Returns how many.
/// `copy` is the copy's number when the caller knows it and the table no longer does - a
/// copy closing is taken out of the table first, and CopyOf would then answer 0, which is
/// the real landblock's tag.
///
public static int Save(Landblock block, string why, bool? shared = null, int copy = 0)
{
if (!On || !(shared ?? IsShared(block)))
return 0;
var biotas = new System.Collections.ObjectModel.Collection<(ACE.Entity.Models.Biota biota, ReaderWriterLockSlim rwLock)>();
var tagged = 0;
if (copy <= 0)
copy = InstanceWorld.CopyOf(block);
if (copy <= 0)
{
ModManager.Log($"[{Mod.Name}] keep: {why}: no copy number for a shared copy of {block.Id.Landblock:X4} - nothing saved", ModManager.LogLevel.Warn);
return 0;
}
foreach (var wo in InstanceWorld.ObjectsOf(block))
{
if (wo is Player)
continue;
try
{
if (!wo.IsDynamicThatShouldPersistToShard())
continue;
if (wo.GetProperty(Tag) != copy)
wo.SetProperty(Tag, copy); // also marks ChangesDetected, so it is written
Collect(wo, biotas);
tagged++;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] keep: could not save {wo.Name} (0x{wo.Guid.Full:X8}) in the shared copy of {block.Id.Landblock:X4}: {ex.Message}",
ModManager.LogLevel.Warn);
}
}
if (biotas.Count > 0)
DatabaseManager.Shard.SaveBiotasInParallel(biotas, null);
Saved += tagged;
if (tagged > 0)
Mod.Trace($"keep: {why}: {tagged} object(s) of the shared copy of {block.Id.Landblock:X4} saved ({biotas.Count} row set(s) written)");
return tagged;
}
/// ACE's Landblock.AddWorldObjectToBiotasSaveCollection, verbatim in effect.
private static void Collect(WorldObject wo, System.Collections.ObjectModel.Collection<(ACE.Entity.Models.Biota biota, ReaderWriterLockSlim rwLock)> biotas)
{
if (wo.ChangesDetected)
{
wo.SaveBiotaToDatabase(false);
biotas.Add((wo.Biota, wo.BiotaDatabaseLock));
}
if (wo is Container container)
{
foreach (var item in container.Inventory.Values)
Collect(item, biotas);
}
}
///
/// Replaces ACE's SpawnDynamicShardObjects for any landblock while Keep is on: a shared
/// copy loads the objects of its landblock id tagged with its copy number, a real
/// landblock the untagged ones.
/// Runs on Init's background task, as ACE's does; the adds are queued on the landblock.
/// Returns false for a personal copy (loads nothing, as before).
///
public static bool SpawnDynamics(Landblock block)
{
var ours = InstanceWorld.IsOurs(block);
var shared = ours && IsShared(block);
if (ours && !shared)
return false;
var dynamics = DatabaseManager.Shard.BaseDatabase.GetDynamicObjectsByLandblock(block.Id.Landblock);
var copy = shared ? InstanceWorld.CopyOf(block) : 0;
var wanted = dynamics.Where(b => TagOf(b) == copy).ToList();
if (!shared)
SkippedInMaster += dynamics.Count - wanted.Count;
if (wanted.Count == 0)
return true;
var objects = WorldObjectFactory.CreateWorldObjects(wanted);
if (shared)
{
// Off the object in memory: from here it is simply a thing on the ground. The
// next save of this copy tags it again; a player who picks it up saves it clean.
foreach (var wo in objects)
wo.RemoveProperty(Tag);
Loaded += objects.Count;
Mod.Trace($"keep: the shared copy of {block.Id.Landblock:X4} loads {objects.Count} kept object(s)");
}
block.EnqueueAction(new ActionEventDelegate(() =>
{
foreach (var wo in objects)
block.AddWorldObject(wo);
}));
return true;
}
///
/// At shutdown ACE unloads (and so saves) every landblock it lists - never our copies.
/// Queued to the world thread when ACE starts that unload; the shutdown waits for the
/// landblocks and then for the database queue, so these saves are written.
///
public static void SaveAllAtShutdown()
{
if (!On)
return;
WorldManager.EnqueueAction(new ActionEventDelegate(() =>
{
var n = 0;
foreach (var block in InstanceWorld.OpenCopies())
{
try { n += Save(block, "shutdown"); }
catch (Exception ex) { ModManager.Log($"[{Mod.Name}] keep: shutdown save of {block.Id.Landblock:X4} failed: {ex.Message}", ModManager.LogLevel.Warn); }
}
ModManager.Log($"[{Mod.Name}] keep: shutdown - {n} object(s) of shared copies saved");
}));
}
}