namespace Aeshnidae.ClearWeenie;
///
/// Evicts one weenie from ACE's cache and brings its live instances up to date.
///
/// Why a global /clearcache is the wrong tool for a one-weenie edit. It drops EVERY
/// cache at once - weenies, landblock instances, recipes, spells, wielded treasure -
/// so the next touch of anything on the shard is a MySQL round trip, and on a busy
/// server that is a wall of round trips inside the tick loop. That is the lag spike.
/// Editing Brighteyes does not need the drudge weenie reloaded.
///
/// ACE already has the surgical primitive, ClearCachedWeenie(wcid); its own
/// /import-sql uses it. This wraps it in a command and adds the half it is missing.
///
/// The missing half. A spawned NPC does not get a private copy of everything - the
/// collections ACE considers "typically not modified" are SHARED BY REFERENCE with the
/// cached weenie (WeenieConverter.ConvertToBiota, referenceWeenieCollectionsForCommon
/// Properties). PropertiesEmote is one of them. So the NPC standing in Holtburg is
/// running its dialogue straight out of the cache entry, and evicting that entry does
/// not touch her: she keeps a reference to the OLD collection and answers from it
/// until she is despawned. The next spawn would be right; the one players are talking
/// to would not. So after reloading, every live object of that wcid is re-pointed at
/// the fresh collections, and the change is live on the next hand-in.
///
/// What this deliberately does NOT refresh, and why:
/// - Cloned properties (stats, attributes, skills, spell books). Those were copied
/// into each object at spawn, and updating a living monster's attributes mid-fight
/// is not something anyone wants. They take effect on the next spawn, which is the
/// expectation anyway.
/// - Generator profiles. A generator snapshots PropertiesGenerator into its own
/// GeneratorProfile objects at init, so re-pointing the biota's list would change
/// nothing it reads. Generator edits need the landblock reloaded.
/// - Items already in inventories or on the ground. Same as stock ACE: an item is
/// what it was when it was made.
/// - Objects inside instanced-dungeon copies (Aeshnidae.InstancesNoDat). Copies are
/// not in LandblockManager's table, so they are not walked here. They are short
/// lived by design - the next copy opened spawns from the fresh weenie.
///
public static class Refresh
{
public record Result(uint Wcid, string Name, bool WasCached, int LiveRefreshed, int LandblocksWalked, string? Error);
public static Result One(uint wcid)
{
var wasCached = DatabaseManager.World.ClearCachedWeenie(wcid);
// Reload straight away rather than leaving the next spawn to pay for it. Also
// the only way to find out whether the wcid exists at all - GetCachedWeenie
// answers null for a wcid that is not in the database.
var fresh = DatabaseManager.World.GetCachedWeenie(wcid);
if (fresh is null)
return new(wcid, "", wasCached, 0, 0, $"no weenie {wcid} in the world database");
var name = fresh.PropertiesString is not null && fresh.PropertiesString.TryGetValue(PropertyString.Name, out var n)
? n
: fresh.ClassName ?? "";
var refreshed = 0;
var walked = 0;
foreach (var landblock in LandblockManager.GetLoadedLandblocks())
{
walked++;
// A snapshot - the landblock keeps ticking on its own thread while this
// walks, and ToList() inside is what makes that safe to iterate.
foreach (var wo in landblock.GetAllWorldObjectsForDiagnostics())
{
if (wo.WeenieClassId != wcid || wo is Player)
continue;
RePoint(wo, fresh);
refreshed++;
}
}
return new(wcid, name, wasCached, refreshed, walked, null);
}
///
/// Swaps the reference-shared collections on a live object for the fresh weenie's.
///
/// Exactly the four ConvertToBiota shares when referenceWeenieCollectionsForCommon
/// Properties is true, minus PropertiesGenerator (see the class comment). Each
/// assignment is a single reference write, which is atomic in .NET - a landblock
/// thread that already grabbed the old collection finishes iterating the old one,
/// and the next lookup gets the new one. No lock, no torn state.
///
private static void RePoint(WorldObject wo, Weenie fresh)
{
var biota = wo.Biota;
biota.PropertiesEmote = fresh.PropertiesEmote;
biota.PropertiesCreateList = fresh.PropertiesCreateList;
biota.PropertiesEventFilter = fresh.PropertiesEventFilter;
}
}