From 6abd29b12dfca90f678f3a9ca131bfe0a93e3df0 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 23 Feb 2026 05:27:45 -0500 Subject: [PATCH] Natural Spawns PR3 - Better Runtime Control of Natural Spawns --- src/main/java/noppes/npcs/NPCSpawning.java | 57 ++++++++++++++++--- .../client/gui/SubGuiSpawningOptions.java | 2 +- .../assets/customnpcs/lang/en_US.lang | 22 +++---- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/src/main/java/noppes/npcs/NPCSpawning.java b/src/main/java/noppes/npcs/NPCSpawning.java index 9e9699915..8b97c0307 100644 --- a/src/main/java/noppes/npcs/NPCSpawning.java +++ b/src/main/java/noppes/npcs/NPCSpawning.java @@ -29,7 +29,6 @@ import noppes.npcs.scripted.event.CustomNPCsEvent; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; @@ -39,18 +38,27 @@ public class NPCSpawning { private static Set eligibleChunksForSpawning = Sets.newHashSet(); private static final byte CHUNK_SPAWN_RADIUS = 7; private static final int SPAWN_ATTEMPTS_PER_CHUNK = 3; + private static final long RUNTIME_SPAWN_TICK_INTERVAL = 20L; + private static final int RUNTIME_SPAWN_CYCLE_TICKS = 400; private static final String NATURAL_SPAWN_ID_TAG = "CNPCNaturalSpawnId"; private static final String NATURAL_SPAWN_NAME_TAG = "CNPCNaturalSpawnName"; private static final String NATURAL_SPAWN_TIME_TAG = "CNPCNaturalSpawnTime"; private static final Map SPAWN_COOLDOWNS = new HashMap(); + private static final Map RUNTIME_CHUNK_STATES = new HashMap(); private static boolean animalSpawn; private static boolean monsterSpawn; private static boolean airSpawn; private static boolean liquidSpawn; + private static class RuntimeChunkState { + public ArrayList chunks = new ArrayList(); + public int index = 0; + public int signature = 0; + } + public static void findChunksForSpawning(WorldServer world) { - if (SpawnController.Instance.data.isEmpty() || world.getWorldInfo().getWorldTotalTime() % 400L != 0L) + if (SpawnController.Instance.data.isEmpty() || world.getWorldInfo().getWorldTotalTime() % RUNTIME_SPAWN_TICK_INTERVAL != 0L) return; eligibleChunksForSpawning.clear(); for (int i = 0; i < world.playerEntities.size(); ++i) { @@ -74,15 +82,13 @@ public class NPCSpawning { return; } HashMap spawnEntryCounts = getNaturalSpawnEntryCounts(world); - ArrayList tmp = new ArrayList(eligibleChunksForSpawning); - Collections.shuffle(tmp); - Iterator iterator = tmp.iterator(); + int chunkBudget = getRuntimeChunkBudget(eligibleChunksForSpawning.size()); + List chunksToProcess = getRuntimeChunkBatch(world, chunkBudget); - while (iterator.hasNext()) { + for (ChunkCoordIntPair chunkcoordintpair1 : chunksToProcess) { if (npcCount >= cap) { return; } - ChunkCoordIntPair chunkcoordintpair1 = iterator.next(); ChunkPosition chunkposition = getChunk(world, chunkcoordintpair1.chunkXPos, chunkcoordintpair1.chunkZPos); int j1 = chunkposition.chunkPosX; @@ -112,6 +118,43 @@ public class NPCSpawning { } } + private static int getRuntimeChunkBudget(int eligibleChunkCount) { + if (eligibleChunkCount <= 0) { + return 0; + } + int runsPerCycle = Math.max(1, (int) (RUNTIME_SPAWN_CYCLE_TICKS / RUNTIME_SPAWN_TICK_INTERVAL)); + return Math.max(1, (int) Math.ceil((double) eligibleChunkCount / runsPerCycle)); + } + + private static List getRuntimeChunkBatch(World world, int chunkBudget) { + ArrayList result = new ArrayList(); + if (chunkBudget <= 0 || eligibleChunksForSpawning.isEmpty()) { + return result; + } + + int dimensionId = world.provider.dimensionId; + RuntimeChunkState state = RUNTIME_CHUNK_STATES.get(dimensionId); + if (state == null) { + state = new RuntimeChunkState(); + RUNTIME_CHUNK_STATES.put(dimensionId, state); + } + + int signature = eligibleChunksForSpawning.hashCode(); + if (state.chunks.isEmpty() || state.signature != signature || state.index >= state.chunks.size()) { + state.chunks = new ArrayList(eligibleChunksForSpawning); + Collections.shuffle(state.chunks); + state.index = 0; + state.signature = signature; + } + + int remaining = state.chunks.size() - state.index; + int count = Math.min(chunkBudget, Math.max(0, remaining)); + for (int i = 0; i < count; i++) { + result.add(state.chunks.get(state.index++)); + } + return result; + } + public static int countNPCs(World world) { int count = 0; List list = world.loadedEntityList; diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java index caabe3d11..34b3e7a6e 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java @@ -16,7 +16,7 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield public SubGuiSpawningOptions(SpawnData data) { this.data = data; setBackground("menubg.png"); - xSize = 216; + xSize = 236; ySize = 216; closeOnEsc = true; } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index ee44acef2..3f8053a06 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -445,17 +445,17 @@ tooltip.naturalspawns.main.addEntry=Add an entry slot to this profile.\nBest per tooltip.naturalspawns.main.removeEntry=Remove selected entry slot.\nBest performance: Remove empty/duplicate entries. tooltip.naturalspawns.main.slot=Entry slot index.\nTip: Keep numbering organized for easier debugging. tooltip.naturalspawns.main.selectEntry=Select which clone/NPC this slot spawns.\nBest performance: Avoid very heavy scripted clones in high-frequency profiles. -tooltip.naturalspawns.options.animal=Uses animal-style spawn checks for this entry.\nBest performance: Enable only for passive/ground mobs. -tooltip.naturalspawns.options.monster=Uses monster-style spawn checks for this entry.\nBest performance: Enable only for hostile/night mobs. -tooltip.naturalspawns.options.liquid=Allows water/liquid spawn validation.\nBest performance: Keep disabled unless required. -tooltip.naturalspawns.options.air=Allows spawn attempts in open air blocks.\nBest performance: Keep disabled unless required. -tooltip.naturalspawns.options.minHeight=Minimum Y for this entry.\nBest performance: Use tight vertical ranges to reduce failed attempts. -tooltip.naturalspawns.options.maxHeight=Maximum Y for this entry.\nBest performance: Use tight vertical ranges to reduce failed attempts. -tooltip.naturalspawns.options.maxAlive=Per-entry alive cap (loaded entities).\n0 = unlimited (not recommended).\nBest performance: 20-60. -tooltip.naturalspawns.options.cooldownTicks=Ticks to wait after a successful spawn for this entry.\nBest performance: 200-600. -tooltip.naturalspawns.options.attemptsPerCycle=How many positions this entry tries each spawn cycle.\nBest performance: 1-2. -tooltip.naturalspawns.options.playerMinDistance=No-spawn radius from nearest player for this entry.\nBest performance: 32-48 to reduce pop-in and retests. -tooltip.naturalspawns.options.despawnMode=How spawned CNPC despawn flags are applied.\nBest performance: Force Natural.\nPreserve/Force Persistent can increase long-term entity load. +tooltip.naturalspawns.options.animal=Spawn Rule: Animal\nUses animal-style spawn checks (surface + creature-valid blocks).\nUse for passive/overworld mobs.\nPerformance: Disable if not needed. +tooltip.naturalspawns.options.monster=Spawn Rule: Monster\nUses monster-style spawn checks (surface + monster-valid blocks).\nUse for hostile/night/cave mobs.\nPerformance: Disable if not needed. +tooltip.naturalspawns.options.liquid=Spawn Rule: Liquid\nAllows spawn validation in liquid/water spaces.\nUse only for aquatic entries.\nPerformance: Keep OFF unless required. +tooltip.naturalspawns.options.air=Spawn Rule: Air\nAllows spawn validation in open-air columns.\nUse only for flying/airborne entries.\nPerformance: Keep OFF unless required. +tooltip.naturalspawns.options.minHeight=Minimum Spawn Y\nLower vertical bound for this profile.\nUse with Max Height to narrow valid layers.\nPerformance: Tighter ranges = fewer failed checks. +tooltip.naturalspawns.options.maxHeight=Maximum Spawn Y\nUpper vertical bound for this profile.\nUse with Min Height to target a specific layer.\nPerformance: Tighter ranges = fewer failed checks. +tooltip.naturalspawns.options.maxAlive=Max Alive (Per Profile)\nCounts loaded entities tagged to this natural spawn profile.\n0 = Unlimited (not recommended).\nPerformance baseline: 20-60. +tooltip.naturalspawns.options.cooldownTicks=Cooldown (Ticks)\nWait time after a successful spawn before this profile can spawn again.\n20 ticks = 1 second.\nPerformance baseline: 200-600 (10-30 seconds). +tooltip.naturalspawns.options.attemptsPerCycle=Attempts Per Cycle\nHow many nearby positions this profile tests each cycle.\nHigher values increase spawn pressure and CPU checks.\nPerformance baseline: 1-2. +tooltip.naturalspawns.options.playerMinDistance=Min Player Distance\nBlocks from nearest player where spawning is blocked.\nHigher values reduce pop-in near players.\nPerformance baseline: 32-48. +tooltip.naturalspawns.options.despawnMode=Despawn Mode (CNPC entries)\nForce Natural: sets canDespawn=true on spawn. Recommended for natural ecosystems.\nPreserve Template: keeps clone/template despawn setting.\nForce Persistent: sets canDespawn=false (NPC stays loaded longer).\nPerformance recommendation: Force Natural. role.name=Role role.none=No Role