Skip to content

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 legendary and megaboss spawn 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

  1. Once a second, copy each spawner's remaining time (nextSpawnTime - now) into memory.
  2. Every saveIntervalSeconds, and again on a clean shutdown, write that to <world>/data/pixelmon_spawn_timers.json.
  3. 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.