CobbleOptimizer
A server-side Fabric mod for Minecraft 1.21.1 that optimizes Cobblemon (the Pokemon mod) entity performance based on server load (TPS). It has 4 main
features:
- Tick Throttle — Reduces how often distant/unimportant Pokemon entities are ticked. When the server TPS drops, Pokemon far from players get their tick rate reduced, saving CPU cycles.
- AI Freeze — Completely freezes the AI of Pokemon entities that are far from any player. If no one is nearby to observe them, there's no need to run their pathfinding and behavior logic.
- Adaptive Spawn — Dynamically adjusts Pokemon spawn rates based on current server TPS. When the server is struggling, fewer Pokemon spawn; when it's healthy, spawns return to normal.
- Batch Despawn — Removes excess Pokemon entities in bulk when the server is overloaded, prioritizing removal of unimportant ones (non-shiny, non-legendary, unowned, not in battle).
Shared Infrastructure
- TPS Monitor — Continuously tracks server TPS to inform all features.
- Player Tracker — Caches player-to-entity distances so multiple features can cheaply check proximity.
- Config — TOML-based configuration with in-game commands (/cobbleoptimizer) to tweak thresholds on the fly.
In short
It's a performance optimization mod that keeps Cobblemon servers running smoothly by intelligently scaling back Pokemon entity processing when the server is under load, while protecting important Pokemon (shinies, legendaries, battling, owned) from aggressive optimization.
All Versions
+ Add Version| Name | Game version | Platforms | Published | Downloads | Actions |
|---|---|---|---|---|---|
| 2mo ago | 1 | ||||
| 2mo ago | 1 | ||||
| 2mo ago | 1 | ||||
| 2mo ago | 1 | ||||
| 2mo ago | 1 | ||||
| 2mo ago | 1 | ||||
| 5mo ago | 1 | ||||
| 5mo ago | 1 | ||||
| 6mo ago | 1 | ||||
| 6mo ago | 1 | ||||
| 6mo ago | 1 | ||||
| 6mo ago | 1 | ||||
| 6mo ago | 1 | ||||
| 6mo ago | 1 | ||||
| 7mo ago | 1 | ||||
| 7mo ago | 1 | ||||
| 7mo ago | 1 | ||||
| 7mo ago | 1 | ||||
| 7mo ago | 1 | ||||
| 7mo ago | 1 | ||||
| 7mo ago | 1 | ||||
| 7mo ago | 1 | ||||
| 7mo ago | 1 | ||||
| 7mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 8mo ago | 1 | ||||
| 9mo ago | 1 | ||||
| 9mo ago | 1 |
CobbleOptimizer 5.0.0 — Cobblemon 1.8 update
Minecraft 1.21.1 · Cobblemon 1.8.x · Fabric + NeoForge
▎ ⚠️ Requires Cobblemon 1.8.0 or newer. Won't load on Cobblemon .3, stay on 4.8.0.
✨ New
- Smarter spawn throttling. Adaptive Spawn now skips whole spawn any work: no area scan, no condition checks, no Pokémongeneration. Before, it cancelled spawns only at the end, after the server had already paid the full cost.
- Same spawn-rate curve as before (healthy_tps / critical_tps
- Spawns slow down smoothly instead of arriving in bursts
- Fishing and PokeSnack spawns aren't affected
- Emergency mode still blocks all spawns
- /cobbleoptimizer status now shows how many spawn passes were
- On startup the server checks that the spawn hook attached, and logs a warning if it didn't
- New option: adaptive_spawn.throttle_spawn_passes (default trold behaviour.
- New thread-safety patch for Cobblemon 1.8's passenger position riding packet. It gets the same main-thread protection as the existing riding-state patch. New
🔧 Changes
- Updated to Cobblemon 1.8. Every patch was checked against the
- Removed two patches that Cobblemon 1.8 now handles natively:
- Berry ticker optimization
- PokemonStore UUID cache
- Fixed: near_player_spawn_protect_radius couldn't be changed frrks (default is still 16 blocks).
- The Cobblemon dependency is now limited to 1.8.x, so an untested future version can't load and break silently.
Everything else works as before: Tick Throttle, AI Freeze, Batch Despawn, Emergency mode, and all cache and bugfix patches.
CobbleOptimizer 5.0.0 — Cobblemon 1.8 update
Minecraft 1.21.1 · Cobblemon 1.8.x · Fabric + NeoForge
▎ ⚠️ Requires Cobblemon 1.8.0 or newer. Won't load on Cobblemon .3, stay on 4.8.0.
✨ New
- Smarter spawn throttling. Adaptive Spawn now skips whole spawn any work: no area scan, no condition checks, no Pokémongeneration. Before, it cancelled spawns only at the end, after the server had already paid the full cost.
- Same spawn-rate curve as before (healthy_tps / critical_tps
- Spawns slow down smoothly instead of arriving in bursts
- Fishing and PokeSnack spawns aren't affected
- Emergency mode still blocks all spawns
- /cobbleoptimizer status now shows how many spawn passes were
- On startup the server checks that the spawn hook attached, and logs a warning if it didn't
- New option: adaptive_spawn.throttle_spawn_passes (default trold behaviour.
- New thread-safety patch for Cobblemon 1.8's passenger position riding packet. It gets the same main-thread protection as the existing riding-state patch. New
🔧 Changes
- Updated to Cobblemon 1.8. Every patch was checked against the
- Removed two patches that Cobblemon 1.8 now handles natively:
- Berry ticker optimization
- PokemonStore UUID cache
- Fixed: near_player_spawn_protect_radius couldn't be changed frrks (default is still 16 blocks).
- The Cobblemon dependency is now limited to 1.8.x, so an untested future version can't load and break silently.
Everything else works as before: Tick Throttle, AI Freeze, Batch Despawn, Emergency mode, and all cache and bugfix patches.
options automatically on startup. Your existing settings are kept exactly as
they were - only missing keys are added, and comments are refreshed.
No more editing the file by hand or deleting it to force a regeneration.
The server log tells you what happened, for example:
[CobbleOptimizer] Config updated with 3 new option(s):
batch_despawn.spawn_grace_period_ticks, batch_despawn.protect_player_npc,
patches.fix_cobbreeding_semver
If your config fails validation, the file is left untouched so you can see and
correct the offending values.
Note: if you have been running the mod for a while, your config may be missing
options that were added in earlier releases and never appeared in your file.
This update adds them. patches.fix_cobbreeding_semver is a common example.
FIXES
- Settings could silently fail to save.
seconds lost the change. Config writes were asynchronous and were not flushed
on shutdown. They are now written to disk immediately.
- spawn_grace_period_ticks is now actually adjustable.
fishing catch is not swept away mid-reel. It existed and was in use, but was
locked at 600 ticks (30 seconds) with no way to change it and was never saved
to the config file. It is now a real, editable option.
- Removed a dead setting.
had nothing behind it, and returned "Invalid key or value" if you tried to
set it. It has been removed.
- Wild Pokemon could accumulate without ever being cleaned up.
saccharine trees, were incorrectly treated as permanent. Because Cobblemon
does not despawn those on its own either, they could pile up over time and
cost TPS. Only deliberately persistent Pokemon are protected now.
NEW CONFIG OPTIONS
[batch_despawn]
# Protect player-made NPC Pokemon from despawn (Pokemon whose vanilla
# PersistenceRequired flag was set via /data modify, e.g. pokemon center
# receptionists)
protect_player_npc = true
# Newly spawned Pokemon near a player are protected for this many ticks,
# so fishing/spawn results are not swept away
# (600 = 30 seconds, 0 = disabled)
spawn_grace_period_ticks = 600
Both are also settable in game:
/cobbleoptimizer set batch_despawn.protect_player_npc false
/cobbleoptimizer set batch_despawn.spawn_grace_period_ticks 1200
"/cobbleoptimizer status" now lists player_npc among the active protections.
Drop in a new version of the mod and cobbleoptimizer.toml now picks up any new
options automatically on startup. Your existing settings are kept exactly as
they were - only missing keys are added, and comments are refreshed.
No more editing the file by hand or deleting it to force a regeneration.
The server log tells you what happened, for example:
[CobbleOptimizer] Config updated with 3 new option(s):
batch_despawn.spawn_grace_period_ticks, batch_despawn.protect_player_npc,
patches.fix_cobbreeding_semver
If your config fails validation, the file is left untouched so you can see and
correct the offending values.
Note: if you have been running the mod for a while, your config may be missing
options that were added in earlier releases and never appeared in your file.
This update adds them. patches.fix_cobbreeding_semver is a common example.
FIXES
- Settings could silently fail to save.
seconds lost the change. Config writes were asynchronous and were not flushed
on shutdown. They are now written to disk immediately.
- spawn_grace_period_ticks is now actually adjustable.
fishing catch is not swept away mid-reel. It existed and was in use, but was
locked at 600 ticks (30 seconds) with no way to change it and was never saved
to the config file. It is now a real, editable option.
- Removed a dead setting.
had nothing behind it, and returned "Invalid key or value" if you tried to
set it. It has been removed.
- Wild Pokemon could accumulate without ever being cleaned up.
saccharine trees, were incorrectly treated as permanent. Because Cobblemon
does not despawn those on its own either, they could pile up over time and
cost TPS. Only deliberately persistent Pokemon are protected now.
NEW CONFIG OPTIONS
[batch_despawn]
# Protect player-made NPC Pokemon from despawn (Pokemon whose vanilla
# PersistenceRequired flag was set via /data modify, e.g. pokemon center
# receptionists)
protect_player_npc = true
# Newly spawned Pokemon near a player are protected for this many ticks,
# so fishing/spawn results are not swept away
# (600 = 30 seconds, 0 = disabled)
spawn_grace_period_ticks = 600
Both are also settable in game:
/cobbleoptimizer set batch_despawn.protect_player_npc false
/cobbleoptimizer set batch_despawn.spawn_grace_period_ticks 1200
"/cobbleoptimizer status" now lists player_npc among the active protections.
Got tired of your Pokémon Center receptionist or NPC Pokémon vanishing when the server gets busy? That's fixed.
- NPC Pokémon are now protected from despawning. If you've set up a Pokémon as an NPC using the classic /data modify entity ... PersistenceRequired set value 1b trick, CobbleOptimizer will now recognize it and never remove it during a despawn cleanup — no matter how far it is from players or how long it's been standing there.
- This is on by default, no config changes needed. Server admins can still turn it off with protect_player_npc = false in cobbleoptimizer.toml if they don't want the exception.
- Everything else about your setup (NoAI, Unbattleable, Invulnerable, uncatchable) keeps working exactly as before — this change only stops the mod from despawning it.
TL;DR: build your Pokémon Center, your gym, your NPC trainers
Got tired of your Pokémon Center receptionist or NPC Pokémon vanishing when the server gets busy? That's fixed.
- NPC Pokémon are now protected from despawning. If you've set up a Pokémon as an NPC using the classic /data modify entity ... PersistenceRequired set value 1b trick, CobbleOptimizer will now recognize it and never remove it during a despawn cleanup — no matter how far it is from players or how long it's been standing there.
- This is on by default, no config changes needed. Server admins can still turn it off with protect_player_npc = false in cobbleoptimizer.toml if they don't want the exception.
- Everything else about your setup (NoAI, Unbattleable, Invulnerable, uncatchable) keeps working exactly as before — this change only stops the mod from despawning it.
TL;DR: build your Pokémon Center, your gym, your NPC trainers
● CobbleOptimizer 4.6.0 — Changelog
New: CobBreeding compatibility patch
Added a @Pseudo mixin targeting ludichat.cobbreeding.utils.SemVerKt::toSemVer to address two distinct issues in CobBreeding (Ludichat/Cobbreeding) versions ≤ 2.2.1.
Performance fix (always active, zero risk)
CobBreeding's toSemVer constructed a new kotlin.text.Regex on every invocation, triggering Pattern.compile (1–10 µs per call). On servers with many eggs, spark profiler flagged this as a measurable hotspot during egg
deserialization.
The mixin uses @Redirect on the new kotlin/text/Regex(String) instruction inside toSemVer to return one of two process-wide cached Regex singletons. Pattern compilation now happens twice total at class load instead of once per
call.
Correctness fix (toggleable, off by default)
The original regex ^\d(\.\d(\.\d([.-_].+)?)?)?$ has two bugs:
1. Single \d per component. Multi-digit version components (e.g. 1.21.1, 2.2.10) fail to match, so toSemVer returns SemVer(emptyList()) with major=minor=patch=0. Downstream, PokemonEgg.verifyComponentsAfterLoad() misidentifies
the egg as pre-2.0.0 and runs the wrong migration, crashing the server with NumberFormatException when an egg is moved/cloned from a pasture block.
2. [.-_] as a character class. This is a regex range from . (0x2E) to _ (0x5F) instead of three literal characters. It accidentally matches digits, uppercase letters, and various punctuation.
When the toggle is enabled, the cached regex is the corrected version: ^\d+(\.\d+(\.\d+([._-].+)?)?)?$.
New config option
[patches]
fix_cobbreeding_semver = false # default off, opt-in
Also reachable via the existing /cobbleoptimizer config patches.fix_cobbreeding_semver <true|false> command. The mixin re-reads the config on every call to toSemVer, so the toggle takes effect immediately without a server
restart.
When the toggle is false, the cache still returns a singleton with the original (buggy) regex — semantics are bit-identical to vanilla CobBreeding, but the per-call Pattern.compile cost is eliminated. When the toggle is true,
the cache returns the corrected regex.
Safety properties
- No compile-time dependency on CobBreeding. The mixin uses @Pseudo + string target, so CobbleOptimizer builds and runs identically whether CobBreeding is installed or not.
- Silent no-op when CobBreeding is absent. Registered in cobbleoptimizer-patches.mixins.json with defaultRequire: 0, so missing target classes don't propagate as errors.
- Forward compatibility. If a future CobBreeding release changes the bytecode shape of toSemVer (e.g. caches the regex upstream, renames the method), the @Redirect no longer matches and the mixin becomes a silent no-op.
● CobbleOptimizer 4.6.0 — Changelog
New: CobBreeding compatibility patch
Added a @Pseudo mixin targeting ludichat.cobbreeding.utils.SemVerKt::toSemVer to address two distinct issues in CobBreeding (Ludichat/Cobbreeding) versions ≤ 2.2.1.
Performance fix (always active, zero risk)
CobBreeding's toSemVer constructed a new kotlin.text.Regex on every invocation, triggering Pattern.compile (1–10 µs per call). On servers with many eggs, spark profiler flagged this as a measurable hotspot during egg
deserialization.
The mixin uses @Redirect on the new kotlin/text/Regex(String) instruction inside toSemVer to return one of two process-wide cached Regex singletons. Pattern compilation now happens twice total at class load instead of once per
call.
Correctness fix (toggleable, off by default)
The original regex ^\d(\.\d(\.\d([.-_].+)?)?)?$ has two bugs:
1. Single \d per component. Multi-digit version components (e.g. 1.21.1, 2.2.10) fail to match, so toSemVer returns SemVer(emptyList()) with major=minor=patch=0. Downstream, PokemonEgg.verifyComponentsAfterLoad() misidentifies
the egg as pre-2.0.0 and runs the wrong migration, crashing the server with NumberFormatException when an egg is moved/cloned from a pasture block.
2. [.-_] as a character class. This is a regex range from . (0x2E) to _ (0x5F) instead of three literal characters. It accidentally matches digits, uppercase letters, and various punctuation.
When the toggle is enabled, the cached regex is the corrected version: ^\d+(\.\d+(\.\d+([._-].+)?)?)?$.
New config option
[patches]
fix_cobbreeding_semver = false # default off, opt-in
Also reachable via the existing /cobbleoptimizer config patches.fix_cobbreeding_semver <true|false> command. The mixin re-reads the config on every call to toSemVer, so the toggle takes effect immediately without a server
restart.
When the toggle is false, the cache still returns a singleton with the original (buggy) regex — semantics are bit-identical to vanilla CobBreeding, but the per-call Pattern.compile cost is eliminated. When the toggle is true,
the cache returns the corrected regex.
Safety properties
- No compile-time dependency on CobBreeding. The mixin uses @Pseudo + string target, so CobbleOptimizer builds and runs identically whether CobBreeding is installed or not.
- Silent no-op when CobBreeding is absent. Registered in cobbleoptimizer-patches.mixins.json with defaultRequire: 0, so missing target classes don't propagate as errors.
- Forward compatibility. If a future CobBreeding release changes the bytecode shape of toSemVer (e.g. caches the regex upstream, renames the method), the @Redirect no longer matches and the mixin becomes a silent no-op.
New optimization: Berry ticker backport (Cobblemon 1.8 → 1.7.3)
Added a new patch that eliminates the per-tick BlockEntityTicker overhead from BerryBlock, ported from how Cobblemon 1.8 handles berry growth.
What it does
- Cancels the original BerryBlock.getTicker() registration (no more per-tick callback on every loaded berry tree).
- Adds hasRandomTicks / randomTick / scheduledTick overrides that drive berry growth via vanilla random ticking instead, with delta-time compensation so
- Mature berries (age = 5) and rooted berries don't tick at all → zero per-tick cost.
Performance
- Before: every loaded BerryBlockEntity ran a callback 20×/s → linear cost scaling with the number of loaded berry trees.
- After: each berry block is visited on average ~once per 68 s (at randomTickSpeed=3), with the elapsed game-time subtracted in one batch from stageTimer.
- On servers with large berry farms, this removes a constant per-tick overhead proportional to the farm size.
Configuration
- Config key: patches.berry_ticker_optimization
- Default: true (enabled out of the box for new installs; existing config files preserve their previous values).
- To disable: edit cobbleoptimizer.toml or run /cobbleoptimizer set patches.berry_ticker_optimization false and reload.
New optimization: Berry ticker backport (Cobblemon 1.8 → 1.7.3)
Added a new patch that eliminates the per-tick BlockEntityTicker overhead from BerryBlock, ported from how Cobblemon 1.8 handles berry growth.
What it does
- Cancels the original BerryBlock.getTicker() registration (no more per-tick callback on every loaded berry tree).
- Adds hasRandomTicks / randomTick / scheduledTick overrides that drive berry growth via vanilla random ticking instead, with delta-time compensation so
- Mature berries (age = 5) and rooted berries don't tick at all → zero per-tick cost.
Performance
- Before: every loaded BerryBlockEntity ran a callback 20×/s → linear cost scaling with the number of loaded berry trees.
- After: each berry block is visited on average ~once per 68 s (at randomTickSpeed=3), with the elapsed game-time subtracted in one batch from stageTimer.
- On servers with large berry farms, this removes a constant per-tick overhead proportional to the farm size.
Configuration
- Config key: patches.berry_ticker_optimization
- Default: true (enabled out of the box for new installs; existing config files preserve their previous values).
- To disable: edit cobbleoptimizer.toml or run /cobbleoptimizer set patches.berry_ticker_optimization false and reload.
1. Removed console log spam
The Batch despawn: removed X (Y aged out) [Skitty x6, Bramblin x4, ...] log line was being printed on every sweep cycle (every ~10 seconds when entities were
despawned). It's now gone. Internal counters (lastDespawnCount, lastAgeDespawnCount) are still updated and remain visible via the /cobbleoptimizer status
command — only the log noise was removed.
2. TPS-gated sweep
Previously the batch despawn ran on a fixed timer (every check_interval_ticks, default 200 ticks = 10s) regardless of server health. Now it only runs when TPS
drops below a configurable threshold.
New config field: batch_despawn.tps_threshold (default 18.0).
- TPS ≥ 18.0 → no despawn sweep (server is healthy, no need)
- TPS < 18.0 → sweep runs at the normal interval
This brings batch despawn in line with the other features (tick_throttle, ai_freeze), which already use the same TPS-gating pattern.
3. Emergency override preserved
The forceNextSweep() flag (used by the Emergency Manager when TPS drops below emergency.tps_threshold, default 12.0) bypasses both the interval check and the
new TPS gate. So even if you set batch_despawn.tps_threshold to something low like 14.0, an emergency at TPS 11 will still force an immediate sweep.
Files changed (mirrored on Fabric + NeoForge)
- CobbleOptimizerConfig.kt — added tpsThreshold: Double = 18.0 to BatchDespawnConfig
- BatchDespawnManager.kt — added TPS gate, removed log line, removed dead species-counting code
- ConfigManager.kt — TOML parse/write, key list, and setValue support for the new field
- ConfigValidator.kt — validates tps_threshold in range 1.0..20.0
- gradle.properties — version bumped 4.3.2 → 4.3.3
How to tune
/cobbleoptimizer config set batch_despawn.tps_threshold 17.0 # stricter (only at 17 TPS)
/cobbleoptimizer config set batch_despawn.tps_threshold 19.5 # more aggressive
/cobbleoptimizer config set batch_despawn.tps_threshold 20.0 # effectively "always on" (old behavior)
The change is backwards-compatible: existing cobbleoptimizer.toml files without the new key fall back to the default 18.0 and the key is written on the next
save.
1. Removed console log spam
The Batch despawn: removed X (Y aged out) [Skitty x6, Bramblin x4, ...] log line was being printed on every sweep cycle (every ~10 seconds when entities were
despawned). It's now gone. Internal counters (lastDespawnCount, lastAgeDespawnCount) are still updated and remain visible via the /cobbleoptimizer status
command — only the log noise was removed.
2. TPS-gated sweep
Previously the batch despawn ran on a fixed timer (every check_interval_ticks, default 200 ticks = 10s) regardless of server health. Now it only runs when TPS
drops below a configurable threshold.
New config field: batch_despawn.tps_threshold (default 18.0).
- TPS ≥ 18.0 → no despawn sweep (server is healthy, no need)
- TPS < 18.0 → sweep runs at the normal interval
This brings batch despawn in line with the other features (tick_throttle, ai_freeze), which already use the same TPS-gating pattern.
3. Emergency override preserved
The forceNextSweep() flag (used by the Emergency Manager when TPS drops below emergency.tps_threshold, default 12.0) bypasses both the interval check and the
new TPS gate. So even if you set batch_despawn.tps_threshold to something low like 14.0, an emergency at TPS 11 will still force an immediate sweep.
Files changed (mirrored on Fabric + NeoForge)
- CobbleOptimizerConfig.kt — added tpsThreshold: Double = 18.0 to BatchDespawnConfig
- BatchDespawnManager.kt — added TPS gate, removed log line, removed dead species-counting code
- ConfigManager.kt — TOML parse/write, key list, and setValue support for the new field
- ConfigValidator.kt — validates tps_threshold in range 1.0..20.0
- gradle.properties — version bumped 4.3.2 → 4.3.3
How to tune
/cobbleoptimizer config set batch_despawn.tps_threshold 17.0 # stricter (only at 17 TPS)
/cobbleoptimizer config set batch_despawn.tps_threshold 19.5 # more aggressive
/cobbleoptimizer config set batch_despawn.tps_threshold 20.0 # effectively "always on" (old behavior)
The change is backwards-compatible: existing cobbleoptimizer.toml files without the new key fall back to the default 18.0 and the key is written on the next
save.
Fixed: ghost Pokémon after batch despawn
Sometimes when the mod cleaned up extra wild Pokémon to keep TPS healthy, you'd still see them standing around on your screen — they couldn't be interacted
with, didn't move, and only disappeared after relogging or moving far away.
This happened because the server removed the Pokémon, but in some cases forgot to tell your client about it. Now the server sends an explicit "remove this
entity" message to every player who could see the Pokémon, right at the moment it gets despawned. No more invisible-to-the-server ghosts cluttering your view.
Fixed: ghost Pokémon after batch despawn
Sometimes when the mod cleaned up extra wild Pokémon to keep TPS healthy, you'd still see them standing around on your screen — they couldn't be interacted
with, didn't move, and only disappeared after relogging or moving far away.
This happened because the server removed the Pokémon, but in some cases forgot to tell your client about it. Now the server sends an explicit "remove this
entity" message to every player who could see the Pokémon, right at the moment it gets despawned. No more invisible-to-the-server ghosts cluttering your view.
We identified three causes of ghost (phantom) Pokemon entities:
1. Redundant entity.discard() in AdaptiveSpawnManager — When blocking a spawn, the code called both event.cancel() and entity.discard(). The cancel alone is
sufficient; the extra discard created a race condition where the entity tracker could send spawn packets to clients before the entity was removed server-side,
leaving a visible but non-interactive ghost on the client.
2. Missing velocity sync in MobEntityAiMixin — When AIFreeze zeroed a Pokemon's horizontal velocity, it never flagged the entity for network sync. The server
had the Pokemon stopped, but the client kept rendering it with the old velocity, causing visual drift/desync. Fixed by setting velocityDirty = true (Fabric) /
hasImpulse = true (NeoForge) after zeroing velocity, which forces the entity tracker to send the updated velocity to all clients.
3. Silent exception swallowing in GhostEntityHandler — The early delegate.tick() calls that prevent ghost entities were catching all exceptions silently. Now
they log at DEBUG level with species name and position, making it possible to diagnose any remaining ghost issues.
ServerCore Compatibility
ServerCore and CobbleOptimizer both inject into the same method (tickNewAi / customServerAiStep) to skip entity AI ticks. When both mods are loaded, they
independently cancel the AI tick — causing Pokemon to never get their AI processed, leading to unresponsive ghost-like entities.
Solution: Created a ServerCoreCompat module that:
- Auto-detects ServerCore at startup via the mod loader API
- When detected, suppresses TickThrottle and AIFreeze (the two features that conflict with ServerCore's entity activation ranges)
- Keeps AdaptiveSpawn, BatchDespawn, and Emergency active (these use Cobblemon events and periodic sweeps, not AI tick injection)
- Logs a clear warning at startup explaining what was disabled and why
- Adds a config option compat.auto_disable_with_servercore (default true) so users can override the behavior if needed, reloadable at runtime
We identified three causes of ghost (phantom) Pokemon entities:
1. Redundant entity.discard() in AdaptiveSpawnManager — When blocking a spawn, the code called both event.cancel() and entity.discard(). The cancel alone is
sufficient; the extra discard created a race condition where the entity tracker could send spawn packets to clients before the entity was removed server-side,
leaving a visible but non-interactive ghost on the client.
2. Missing velocity sync in MobEntityAiMixin — When AIFreeze zeroed a Pokemon's horizontal velocity, it never flagged the entity for network sync. The server
had the Pokemon stopped, but the client kept rendering it with the old velocity, causing visual drift/desync. Fixed by setting velocityDirty = true (Fabric) /
hasImpulse = true (NeoForge) after zeroing velocity, which forces the entity tracker to send the updated velocity to all clients.
3. Silent exception swallowing in GhostEntityHandler — The early delegate.tick() calls that prevent ghost entities were catching all exceptions silently. Now
they log at DEBUG level with species name and position, making it possible to diagnose any remaining ghost issues.
ServerCore Compatibility
ServerCore and CobbleOptimizer both inject into the same method (tickNewAi / customServerAiStep) to skip entity AI ticks. When both mods are loaded, they
independently cancel the AI tick — causing Pokemon to never get their AI processed, leading to unresponsive ghost-like entities.
Solution: Created a ServerCoreCompat module that:
- Auto-detects ServerCore at startup via the mod loader API
- When detected, suppresses TickThrottle and AIFreeze (the two features that conflict with ServerCore's entity activation ranges)
- Keeps AdaptiveSpawn, BatchDespawn, and Emergency active (these use Cobblemon events and periodic sweeps, not AI tick injection)
- Logs a clear warning at startup explaining what was disabled and why
- Adds a config option compat.auto_disable_with_servercore (default true) so users can override the behavior if needed, reloadable at runtime
The mod was indiscriminately blocking/despawning Pokémon spawned by fishing because it treated all spawns identically — no distinction between wild spawns and
player-initiated ones (fishing, etc.).
What was wrong
- AdaptiveSpawn cancelled fishing spawns during low TPS or emergency mode
- BatchDespawn targeted freshly-fished Pokémon first (new entities scored as most expendable)
- No spawn source differentiation anywhere in the codebase
What was fixed
AdaptiveSpawnManager — Added a near-player proximity check before any blocking logic. If a Pokémon spawns within nearPlayerSpawnProtectRadius (default: 16
blocks) of any player, it bypasses all TPS-based, emergency, and density spawn blocking. Fishing bobbers are always near the player, so fishing spawns are
always protected.
DespawnPriority — Added a spawn grace period. Pokémon younger than spawnGracePeriodTicks (default: 600 ticks / 30 seconds) that are within minDespawnRadius
(32 blocks) of a player are marked as protected and excluded from both age-based and count-based despawn sweeps.
TickThrottle / AIFreeze — No changes needed. Both already exempt Pokémon within 32 blocks of a player.
The mod was indiscriminately blocking/despawning Pokémon spawned by fishing because it treated all spawns identically — no distinction between wild spawns and
player-initiated ones (fishing, etc.).
What was wrong
- AdaptiveSpawn cancelled fishing spawns during low TPS or emergency mode
- BatchDespawn targeted freshly-fished Pokémon first (new entities scored as most expendable)
- No spawn source differentiation anywhere in the codebase
What was fixed
AdaptiveSpawnManager — Added a near-player proximity check before any blocking logic. If a Pokémon spawns within nearPlayerSpawnProtectRadius (default: 16
blocks) of any player, it bypasses all TPS-based, emergency, and density spawn blocking. Fishing bobbers are always near the player, so fishing spawns are
always protected.
DespawnPriority — Added a spawn grace period. Pokémon younger than spawnGracePeriodTicks (default: 600 ticks / 30 seconds) that are within minDespawnRadius
(32 blocks) of a player are marked as protected and excluded from both age-based and count-based despawn sweeps.
TickThrottle / AIFreeze — No changes needed. Both already exempt Pokémon within 32 blocks of a player.
event.cancel() only sets a flag on the event object. If Cobblemon adds the entity to the world before firing the event (or doesn't properly check the
cancellation flag), the entity remains in the world as a ghost: it exists server-side but was never fully initialized by Cobblemon's spawn pipeline. This
means:
- No model/animation data synced to clients — the entity is invisible
- No Cobblemon behavior state initialized — the entity is stuck, can't be battled
- Never cleaned up — ghost entities accumulate over time
The fix adds event.entity.discard() after every event.cancel() call. discard() marks the entity for removal from the world. It's safe in both scenarios:
- If the entity is already in the world → it gets properly removed, destroy packet sent to clients
- If the entity was never added to the world → it's a no-op (just sets a flag on a garbage-collectible object)
event.cancel() only sets a flag on the event object. If Cobblemon adds the entity to the world before firing the event (or doesn't properly check the
cancellation flag), the entity remains in the world as a ghost: it exists server-side but was never fully initialized by Cobblemon's spawn pipeline. This
means:
- No model/animation data synced to clients — the entity is invisible
- No Cobblemon behavior state initialized — the entity is stuck, can't be battled
- Never cleaned up — ghost entities accumulate over time
The fix adds event.entity.discard() after every event.cancel() call. discard() marks the entity for removal from the world. It's safe in both scenarios:
- If the entity is already in the world → it gets properly removed, destroy packet sent to clients
- If the entity was never added to the world → it's a no-op (just sets a flag on a garbage-collectible object)
world.
At that point, the entity tracker hasn't finished registering the entity yet. It hasn't determined which players are in range, and hasn't sent spawn or
destroy packets to anyone. Discarding the entity in this transitional state could cause the tracker to partially register an already-removed entity — some
clients would receive a spawn packet but never get the matching destroy packet, leaving a ghost entity stuck on their screen that other players can't see.
Fix: Instead of discarding immediately during chunk load, entities are now added to a pendingChunkLoadDiscards list. This list is drained at END_SERVER_TICK,
after the entity tracker has had a full tick to sync with all clients. At that point, discard() goes through the normal removal flow and every client gets the
destroy packet properly.
world.
At that point, the entity tracker hasn't finished registering the entity yet. It hasn't determined which players are in range, and hasn't sent spawn or
destroy packets to anyone. Discarding the entity in this transitional state could cause the tracker to partially register an already-removed entity — some
clients would receive a spawn packet but never get the matching destroy packet, leaving a ghost entity stuck on their screen that other players can't see.
Fix: Instead of discarding immediately during chunk load, entities are now added to a pendingChunkLoadDiscards list. This list is drained at END_SERVER_TICK,
after the entity tracker has had a full tick to sync with all clients. At that point, discard() goes through the normal removal flow and every client gets the
destroy packet properly.
1. Removed aggressive tick cancellation
- Deleted PokemonEntityTickMixin entirely (both Fabric + NeoForge)
- Tick throttle now only skips AI computation (tickNewAi / customServerAiStep), not the full entity tick
- Cobblemon's state machine, physics, and interaction handling continue running normally even when throttled
2. Fixed hitbox desync on AI freeze
- When AI freeze stops navigation, horizontal velocity is now zeroed out (setVelocity(0, vel.y, 0))
- Previously, residual velocity caused server-side entity position to drift from client-side, making entities unclickable
3. Fixed AI freeze radius misconfiguration
- Default aiFreeze.freezeRadius reduced from 48 to 32 to match tickThrottle.nearPlayerRadius
- Previously, Pokemon between 32-48 blocks would tick normally but have completely frozen AI — appearing alive but unresponsive
- Added a config validation warning if freezeRadius > nearPlayerRadius
4. Frozen entity cleanup on despawn
- AiFreezeManager.removeEntity() is now called before every entity.discard() in BatchDespawnManager
- Prevents stale entity IDs in the frozen set from incorrectly freezing newly spawned entities with recycled IDs
1. Removed aggressive tick cancellation
- Deleted PokemonEntityTickMixin entirely (both Fabric + NeoForge)
- Tick throttle now only skips AI computation (tickNewAi / customServerAiStep), not the full entity tick
- Cobblemon's state machine, physics, and interaction handling continue running normally even when throttled
2. Fixed hitbox desync on AI freeze
- When AI freeze stops navigation, horizontal velocity is now zeroed out (setVelocity(0, vel.y, 0))
- Previously, residual velocity caused server-side entity position to drift from client-side, making entities unclickable
3. Fixed AI freeze radius misconfiguration
- Default aiFreeze.freezeRadius reduced from 48 to 32 to match tickThrottle.nearPlayerRadius
- Previously, Pokemon between 32-48 blocks would tick normally but have completely frozen AI — appearing alive but unresponsive
- Added a config validation warning if freezeRadius > nearPlayerRadius
4. Frozen entity cleanup on despawn
- AiFreezeManager.removeEntity() is now called before every entity.discard() in BatchDespawnManager
- Prevents stale entity IDs in the frozen set from incorrectly freezing newly spawned entities with recycled IDs
All Cobblemon patches are now disabled by default. The patches introduced in 4.0.0 (thread safety fixes, bugfixes, caches) were causing ghost Pokemon entities
for some users. They are now fully configurable and off by default.
If you were experiencing ghost Pokemon, simply updating to this version will fix it — no config changes needed.
Changes:
- All 13 Cobblemon patches (thread safety, bugfixes, caches) are now disabled by default
- Each patch can be individually toggled in the [patches] section of cobbleoptimizer.toml
- The core optimization features (tick throttle, AI freeze, adaptive spawn, batch despawn, emergency mode) are unaffected and remain enabled by default
Note: If you're upgrading from 4.1.1 or earlier, delete your old cobbleoptimizer.toml so the new defaults are generated. Otherwise your existing config will
keep the old true values.
When a Pokémon is recalled after a battle, the recall can happen on a background thread. mod was intercepting this and moving the entity
removal to the main thread — but it was only removing the entity, without telling Cobblemon that the Pokémon was actually recalled. So Cobblemon still
thought the Pokémon was "sent out" and could try to recreate it in a broken state — resulting in those frozen, non-interactable ghost Pokémon near players.
20 times. The problem was that setStageTimer isn't a per-tick call — it's only called at key moments (planting, stage transitions, post-harvest), so
throttling it meant the growth timer was never properly set, causing instant growth and repeated harvesting. The fix removes the throttle entirely while
keeping the berry lookup cache
Smarter throttling — Tick skipping now scales proportionally with TPS severity instead of being a fixed rate. The worse the TPS drops, the more aggressively
distant Pokemon are throttled. This means smoother performance recovery instead of a single on/off switch.
Spawn density control — Pokemon can no longer cluster in one area. Spawns are blocked if too many Pokemon are already nearby (default: 15 within 48 blocks).
This prevents lag spikes from having dozens of entities stacked in the same spot. Configurable via adaptive_spawn.max_local_density and
adaptive_spawn.density_check_radius.
Better spawn defaults — Spawn rates now reach 100% at 19.5 TPS instead of requiring a perfect 20.0, which was almost never achieved in practice. Servers
should see slightly more consistent spawning during normal operation.
Improved status command — /cobbleoptimizer status now shows:
- Tick skip rate (how many ticks were actually saved)
- Density-blocked spawns count
- Pokemon count per world
Bug fixes & config validation
- Fixed battle cache incorrectly clearing during PvP when one player disconnects
- Config now warns if emergency thresholds are set higher than throttle/freeze thresholds (misconfigured escalation)
Problem: NoSuchElementException crash in Targetable.getOppositeOpponent() during battle start (Cobblemon 1.7.3 only).
Root cause: The mixin cached PokemonBattle.getActivePokemon() as a static snapshot (List.copyOf()). In Cobblemon 1.7.3, this method is a lazy iterable that evaluates on each call. During SwitchInstruction processing, the
cached snapshot became stale — the opponent's slot was in transition (old pokemon cleared, new one not yet assigned), so getOppositeOpponent() found no match and threw.
Fix: Removed the getActivePokemon() cache. Cobblemon's lazy iterable now evaluates in real-time, always reflecting current battle state. Other caches (sides, actors, players, isPvP/isPvN/isPvW) are unaffected — they don't
change during instruction processing.
Why it only affects 1.7.3: Cobblemon 1.7.1 likely returned direct list references from getActivePokemon(), making the snapshot safe. 1.7.3 changed to a lazily computed iterable, breaking the snapshot approach.
evolution data before the Pokémon species registry is ready. Instead of crashing, CobbleOptimizer now safely skips the evolution display and lets you connect normally.