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