namespace Aeshnidae.Expedition;
///
/// The island's monster density (Settings.SpawnMultiplier; Tom, 2026-10-01: "I think we need to
/// double the spawns"). Every generator that comes into the island copy has its monster profiles'
/// InitCreate and MaxCreate, and its own InitGeneratedObjects and MaxGeneratedObjects, multiplied -
/// so ACE's own generator logic spawns and respawns that many more, and nothing else about a spawn
/// changes. The real Aerlinthe, and every other copy, keep retail numbers.
///
/// Why it is done this way: a generator object does NOT own its profile data - ACE builds world
/// objects with the cached weenie's PropertiesGenerator list by reference (WeenieConverter,
/// referenceWeenieCollectionsForCommonProperties), so writing MaxCreate there would double the
/// same generator everywhere, the real island included. Each island profile is instead pointed at
/// its own Clone() with the multiplied counts. The generator's own counts are integer properties,
/// which ARE per object. Patching the getters instead was ruled out: they are one-line properties
/// the JIT inlines, and a patch on an inlined method never runs (Healing, 2026-09-30).
///
/// Applied when a generator is added to a landblock of the island copy (Landblock's
/// AddWorldObjectInternal, before its first spawn), and to the generators of every open island
/// landblock when the mod loads or its settings are reloaded. PropertyInt 9048 on the generator
/// records the multiplier applied (x100), so a reload of this mod never multiplies twice. Lowering
/// the multiplier takes effect as each landblock loads again (the island copy closes two minutes
/// after its last player leaves). Treasure profiles (chests' contents) and anything not a monster
/// are left alone. Thinning still holds a reclaimed shore to ThinTo of the (multiplied) profile.
///
public static class Density
{
public const PropertyInt Marker = (PropertyInt)9048;
public static long Generators, Profiles;
private static double M => Math.Max(1, Mod.Settings.SpawnMultiplier);
/// A generator in the island copy: its monster profiles and caps multiplied, once.
public static void Apply(WorldObject generator)
{
var m = M;
if (m <= 1 || generator?.GeneratorProfiles is not { Count: > 0 } profiles || generator.GetProperty(Marker) is not null)
return;
var any = false;
foreach (var profile in profiles)
{
if (profile.IsPlaceholder || profile.Biota is null || profile.RegenLocationType.HasFlag(RegenLocationType.Treasure))
continue;
if (!Thinning.IsMonster(profile.Biota.WeenieClassId))
continue;
var own = profile.Biota.Clone();
if (own.MaxCreate > 0)
own.MaxCreate = (int)Math.Ceiling(own.MaxCreate * m);
if (own.InitCreate > 0)
own.InitCreate = (int)Math.Ceiling(own.InitCreate * m);
profile.Biota = own;
any = true;
Profiles++;
}
if (!any)
return;
if (generator.MaxGeneratedObjects > 0)
generator.MaxGeneratedObjects = (int)Math.Ceiling(generator.MaxGeneratedObjects * m);
if (generator.InitGeneratedObjects > 0)
generator.InitGeneratedObjects = (int)Math.Ceiling(generator.InitGeneratedObjects * m);
generator.SetProperty(Marker, (int)Math.Round(m * 100));
Generators++;
}
/// Patches: an object added to a landblock - a generator of the island copy gets the island's density.
public static void OnAdded(Landblock block, WorldObject wo)
{
if (!Mod.Settings.Enabled || M <= 1 || wo is null || !wo.IsGenerator)
return;
if (!Island.Landblocks.Contains((ushort)block.Id.Landblock) || !Island.IsIsland(block))
return;
Apply(wo);
}
///
/// For the console: each open island landblock - monsters standing in it, and the multiplied
/// and retail sums of its monster profiles' MaxCreate. World thread.
///
public static string Report()
{
var sb = new StringBuilder();
foreach (var block in Island.OpenBlocks().OrderBy(b => b.Id.Landblock))
{
var objects = block.GetAllWorldObjectsForDiagnostics();
var standing = objects.Count(o => o is Creature c && o is not Player && !c.IsDead && Thinning.IsMonster(o.WeenieClassId));
int now = 0, retail = 0, gens = 0, marked = 0;
foreach (var g in objects.Where(o => o.IsGenerator && o.GeneratorProfiles is not null))
{
gens++;
if (g.GetProperty(Marker) is not null)
marked++;
foreach (var p in g.GeneratorProfiles.Where(p => !p.IsPlaceholder && p.Biota is not null && Thinning.IsMonster(p.Biota.WeenieClassId)))
{
now += Math.Max(0, p.MaxCreate);
var weenie = DatabaseManager.World.GetCachedWeenie(g.WeenieClassId);
var original = weenie?.PropertiesGenerator?.FirstOrDefault(x => x.WeenieClassId == p.Biota.WeenieClassId && x.Probability == p.Biota.Probability);
retail += Math.Max(0, original?.MaxCreate ?? p.MaxCreate);
}
}
sb.AppendLine($" {block.Id.Landblock:X4}: {standing} monster(s) standing; {gens} generator(s), {marked} multiplied; monster profile caps {now} (retail {retail})");
}
return sb.Length == 0 ? " no island landblock is open" : sb.ToString();
}
/// Every generator already standing in the open island landblocks (mod load, settings reload). World thread.
public static int ApplyToOpen()
{
var before = Generators;
foreach (var block in Island.OpenBlocks())
{
foreach (var wo in block.GetAllWorldObjectsForDiagnostics().Where(o => o.IsGenerator).ToList())
{
try { Apply(wo); }
catch (Exception ex) { ModManager.Log($"[{Mod.Name}] density: {wo.Name} failed: {ex.Message}", ModManager.LogLevel.Warn); }
}
}
return (int)(Generators - before);
}
}