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