using ACE.Entity.Models;
namespace Aeshnidae.Expedition;
///
/// A reclaimed zone's monsters thin to about a third (Settings.ThinTo), and stay that way while
/// the zone is reclaimed - through respawns, and through its landblocks unloading and loading
/// again.
///
/// The seam is ACE's GeneratorProfile.ProcessQueue, where a profile's queued spawns become
/// monsters - the first spawn when a generator powers up and every respawn after. For a
/// monster profile (an attackable creature's weenie) of a generator standing in a reclaimed
/// zone of the island copy, the queue is trimmed so the profile never holds more than its
/// share: ThinTo of its MaxCreate when that is 3 or more; for a profile of one or two, a
/// fixed third of such profiles keep theirs and the rest spawn nothing (chosen by the
/// generator's guid and the profile, so the choice holds between checks). Nothing on the
/// generator changes: the trimmed queue simply fills again at its next regeneration and is
/// trimmed again, which costs a list operation, not a monster.
///
/// At the moment a zone is reclaimed, Cull removes what already stands over the share in its
/// open landblocks - only monsters not fighting anyone, so nobody's fight vanishes under them.
///
public static class Thinning
{
private static readonly ConcurrentDictionary _isMonster = new();
/// Monsters removed by Cull since the mod loaded.
public static int Culled;
/// Spawns held back by the queue trim since the mod loaded.
public static long Held;
/// The zone a generator stands in, if it is a reclaimed zone of the island copy; else null. Cheap first.
public static ZoneSetting? ReclaimedZoneOf(WorldObject? generator)
{
if (!Expedition.AnyReclaimed || generator?.CurrentLandblock is not { } block)
return null;
var lb = (ushort)block.Id.Landblock;
if (!Island.Landblocks.Contains(lb) || Zones.Of(lb) is not { } zone || !Expedition.IsReclaimed(zone))
return null;
return Island.IsIsland(block) ? zone : null;
}
/// Whether a weenie is a monster: a creature that can be attacked (not an NPC).
public static bool IsMonster(uint wcid) =>
_isMonster.GetOrAdd(wcid, id =>
{
var weenie = DatabaseManager.World.GetCachedWeenie(id);
return weenie is not null
&& weenie.WeenieType == WeenieType.Creature
&& (weenie.GetProperty(PropertyBool.Attackable) ?? true);
});
/// How many of this profile's monsters a thinned zone keeps.
public static int Share(GeneratorProfile profile)
{
var keep = Math.Clamp(Mod.Settings.ThinTo, 0, 1);
var max = profile.MaxCreate < 0 ? 1 : profile.MaxCreate;
if (max >= 3)
return (int)Math.Ceiling(max * keep);
var hash = (profile.Generator?.Guid.Full ?? 0) * 2654435761u ^ profile.Id * 40503u;
return (hash % 1000) / 1000.0 < keep ? max : 0;
}
/// Patches: the queue trim, before ProcessQueue turns the queue into monsters.
public static void Trim(GeneratorProfile profile)
{
if (profile.SpawnQueue.Count == 0 || ReclaimedZoneOf(profile.Generator) is null)
return;
if (profile.Biota is null || !IsMonster(profile.Biota.WeenieClassId))
return;
var room = Math.Max(0, Share(profile) - profile.Spawned.Count);
while (profile.SpawnQueue.Count > room)
{
profile.SpawnQueue.RemoveAt(profile.SpawnQueue.Count - 1);
Held++;
}
}
///
/// A zone just reclaimed: remove every monster over its generator's share in the zone's
/// open landblocks, if it is not fighting. World thread.
///
public static void Cull(ZoneSetting zone)
{
var removed = 0;
try
{
foreach (var lb in Zones.LandblocksOf(zone))
{
if (Island.Find(lb) is not { } block)
continue;
Island.WithAmbient(block, () =>
{
foreach (var generator in block.GetAllWorldObjectsForDiagnostics().Where(o => o.IsGenerator && o.GeneratorProfiles is not null).ToList())
{
foreach (var profile in generator.GeneratorProfiles.ToList())
{
if (profile.Biota is null || !IsMonster(profile.Biota.WeenieClassId))
continue;
var surplus = profile.Spawned.Count - Share(profile);
foreach (var info in profile.Spawned.Values.ToList())
{
if (surplus <= 0)
break;
if (info.TryGetWorldObject() is not Creature c || c.IsDead || c.AttackTarget is not null)
continue;
c.Destroy();
surplus--;
removed++;
}
}
}
});
}
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] thinning {zone.Name} failed: {ex.Message}", ModManager.LogLevel.Warn);
}
Culled += removed;
ModManager.Log($"[{Mod.Name}] {zone.Name} reclaimed: {removed} monster(s) thinned from its open landblocks");
}
}