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.
How to Install CobblemonOptimizer (Mod)
CobblemonOptimizer by MarcoErCarrozza is verified for Fabric, Neoforge across Minecraft versions 1.21.1 (latest release: 5.0.0, file size: 121 KB). Follow the steps below to install it safely from ModCraft.tv:
- Click the Download button above and select your Minecraft version (1.21.1) and platform (Fabric, Neoforge).
- Make sure you have the compatible loader or environment installed for Fabric, Neoforge.
- Move the downloaded CobblemonOptimizer file into your Minecraft
.minecraft/modsfolder and launch the game.
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 | ||||
| 9mo 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