Pixelmon Legendary Timer Persistence¶
Keeps Pixelmon's legendary and mega boss spawn countdowns from resetting when the server restarts
or runs /reload. Without it, every restart starts a fresh full wait.
What it does¶
- Once a second it notes how much time is left on Pixelmon's
legendaryandmegabossspawn timers. - Every 30 seconds, and on a clean shutdown, it saves that to a small file in the world folder.
- When Pixelmon rebuilds its spawners (at boot and on every
/reload), it puts the remaining time back. - Server downtime does not count by default: a timer with 5 minutes left still has 5 minutes left after the server comes back.
- It never changes spawn rates, chances, or which Pokemon can spawn, and it never makes a spawn happen early.
On main, Legendary Tracker's daily schedule holds Pixelmon's own legendary roll off, so this mod
mostly matters for the mega boss timer there, and for any server with the schedule off.
Server-side only.
How players use it¶
Players do not interact with this mod.
Staff guide¶
/legendarytimer needs pixelmonlegendarytimerpersist.command (ops level 2 have it).
| Command | What it does |
|---|---|
/legendarytimer status |
Time left on each timer, the cycle length, whether it is saved, and the save file path |
/legendarytimer set <legendary\|megaboss> <minutes> |
Force a countdown. Handy for testing a restart without waiting |
/legendarytimer save |
Save now |
Players without the node do not see the command at all. A permission change only reaches a player's command list after they relog.
The boss timer is called megaboss, not boss.
Configuration¶
config/pixelmonlegendarytimerpersist-common.toml (in config/, not the world's serverconfig/).
Restart after changes.
| Key | Default | Meaning |
|---|---|---|
spawners.persistLegendary |
true |
Save the legendary timer |
spawners.persistMegaBoss |
true |
Save the mega boss timer |
restore.countDowntimeTowardTimer |
false |
Let offline time count down the timer. With true, a long outage can leave the timer already expired, so a legendary may spawn seconds after the first player joins |
restore.maxCarryoverMinutes |
720 |
A saved time larger than this is cut down (protects against a bad file) |
saving.saveIntervalSeconds |
30 |
How often to save. The most a hard crash can lose |
saving.debugLogging |
false |
Log every save and restore |
Data and storage¶
<world>/data/pixelmon_spawn_timers.json. It belongs to the world, so swapping worlds does not carry
an old countdown over. It can be edited by hand. A broken file logs a warning and the timers start
fresh. Writes go to a .tmp file first and are then moved into place.
{
"version": 1,
"savedAtEpochMs": 1785000000000,
"savedAt": "2026-08-03T18:00:00Z",
"spawners": {
"legendary": { "remainingMs": 431000, "remaining": "7m 11s", "spawnFrequency": 0.85 },
"megaboss": { "remainingMs": 122000, "remaining": "2m 2s", "spawnFrequency": 1.2 }
}
}
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| Timer reset after a restart | Mod not loaded, or the timer is turned off in config | Check the boot lines below and /legendarytimer status |
/legendarytimer is not available |
Missing the node, or the player has not relogged since getting it | Grant pixelmonlegendarytimerpersist.command, then relog |
| Legendary spawned right after a long outage | countDowntimeTowardTimer=true |
Set it to false |
Boot lines that show it loaded:
Registered permission node pixelmonlegendarytimerpersist.command
Loaded saved spawn timers from <world>/data/pixelmon_spawn_timers.json (written <time> ago)
On a first boot with no file you see No saved spawn timers at ... this boot starts them from
Pixelmon's defaults instead.
Maintainer notes¶
The bug¶
Pixelmon keeps the countdown in two plain fields on TickingSpawner:
public long lastCycleTime = 0L;
public long nextSpawnTime = 0L; // absolute System.currentTimeMillis() deadline
public float spawnFrequency = ...; // spawns per minute
Nothing writes them anywhere. PixelmonSpawning.initialize() builds brand-new LegendarySpawner
objects, and the builder's apply() ends with onSpawnEnded():
this.lastCycleTime = System.currentTimeMillis();
this.nextSpawnTime = this.lastCycleTime + (long)(60000.0F / this.spawnFrequency);
- a full fresh interval. So every boot restarts the countdown, and the 50 minutes players spent waiting are gone.
It is worse than "on restart". initialize() is registered as a datapack-loader task
(PixelmonLoaders → .task(PixelmonSpawning::initialize)), so every /reload resets it too.
How the fix works¶
- Once a second, copy each spawner's remaining time (
nextSpawnTime - now) into memory. - Every
saveIntervalSeconds, and again on a clean shutdown, write that to<world>/data/pixelmon_spawn_timers.json. - Whenever the live spawner object is not the one we have been sampling, write the remaining time back into it.
Step 3 is the whole trick. Pixelmon signals "I rebuilt the spawners" by replacing the object, not by mutating it, so a reference-identity check is an exact, cheap detector, and it makes one code path cover both cases:
| Case | What identity says | Where the remaining time comes from |
|---|---|---|
| Cold boot | first sight of this object | the saved file |
/reload |
object changed mid-run | memory (no downtime to reason about) |
| Anything else | same object | nothing, just sample it |
Because a rebuilt spawner always starts with a full interval, never shorter than what we are
about to write, restoring a tick or two late can never let something spawn early. That is why the
mod can afford to poll rather than hook initialize() itself.
Restore is driven from ServerStartedEvent, OnDatapackSyncEvent (fires after /reload
completes) and the once-a-second tick sample, all funnelling into the same idempotent sync().
Downtime¶
The saved file stores the remaining time plus the wall clock when it was taken, not Pixelmon's absolute deadline. Storing the deadline would silently make downtime count against the timer.
By default countDowntimeTowardTimer = false: a timer with 5 minutes left still has 5 minutes left
after an hour of downtime. Set it true to honour the original deadline instead, but note that a
long outage then leaves the timer already expired, so a legendary can spawn within seconds of the
first player joining.
What it does not change¶
Spawn rate, spawn chance, which Pokémon are eligible, or anything about how a spawn is chosen. The
only fields it writes are nextSpawnTime, lastCycleTime and spawnFrequency on the two
LegendarySpawner instances, and only when Pixelmon has just thrown the old values away.
lastCycleTime is kept consistent with nextSpawnTime rather than left at the rebuild time.
Pixelmon only reads it for the /spawning diagnose output of tracking spawners, so that is
cosmetic, but a mismatched pair is a trap for whoever reads this next.
Permission node¶
Checked with a Brigadier requires(), so the command is hidden from players without the node. The
node is also mirrored into Bukkit at ServerStartedEvent, only so it tab-completes in /lp. See the
pixelmon-checkspawns-perms README for why NeoForge mod nodes otherwise never show up there.
Compatibility¶
Only TickingSpawner's public fields, PixelmonSpawning's statics and PlayerHelper are used. They
are the same on Pixelmon 9.3.14 and 9.3.16. Tested through a full stop and start on the Youer test
server; the transcript is in manual_uploads/2026-08-03-legendary-timer-persist/README.md in the ops
repo.
Building¶
JAVA_HOME=$(/usr/libexec/java_home -v 21) ./gradlew build
Output: build/libs/pixelmonlegendarytimerpersist-<version>.jar. pixelmon_jar in
gradle.properties points at the local Pixelmon universal jar; it must match the server's version.
Built like the sibling pixelmon-checkspawns-perms mod (NeoGradle, Java 21, Pixelmon as compileOnly).
Copy the jar into the server's mods/ and restart; mod jars do not hot-load.
Full command, permission and config tables: Reference.