Pixelmon CheckSpawns Perms¶
Controls how much of /checkspawns legendary and /checkspawns megaboss each rank can see, makes
the spawn list match what the spawner really does, and lets a rank use /givepixelsprite without op.
What it does¶
- Gates the output. Three LuckPerms nodes per spawner decide whether a player sees the countdown, the per-roll chance, and the per-species percentages.
- Makes the spawn list accurate (v1.4.0). Stock Pixelmon checks the wrong area. This mod
samples the area the spawner really uses, and adds
/checkspawns <spawner> me. - Opens
/givepixelspriteto a rank (v1.5.0). Players can give themselves a cosmetic sprite hat for any species they have caught.
How players use it¶
| Command | What the player sees |
|---|---|
/checkspawns legendary |
Server-wide list of legendaries that could spawn, plus the countdown and chance if their rank allows |
/checkspawns megaboss |
Same, for mega bosses. (The spawner is megaboss; /checkspawns boss does not exist.) |
/checkspawns legendary me |
What could spawn if the spawner picks you ("Possible spawns near you:"). Also self, mine, personal, here. |
/checkspawns legendary me articuno |
Same, filtered to one species |
/givepixelsprite <species> |
Gives yourself a sprite hat for a species you have caught (rank perk) |
Example output, with what each node hides:
Time until next spawn attempt: 3 minutes -> hidden without .time
Chance of spawning: .63% -> hidden without .chance
Possible spawns:
Articuno: 12.5%, Zapdos: 8.33%, Moltres: ... -> without .percentages: "Articuno, Zapdos, Moltres"
"Chance of spawning" is per roll, not per hour. On main it shows .63% (one 3-minute attempt),
while the server produces a legendary about every 8 hours. That is why it has its own node.
Staff guide¶
Permission nodes for /checkspawns¶
<spawner> is legendary or megaboss, so there are six nodes.
| Node | Shows |
|---|---|
pixelmon.checkspawns.<spawner>.time |
The countdown line |
pixelmon.checkspawns.<spawner>.chance |
The per-roll chance line |
pixelmon.checkspawns.<spawner>.percentages |
The per-species percentages |
- Grant any combination. They sit under Pixelmon's own
pixelmon.checkspawns.legendaryandpixelmon.checkspawns.megaboss, which still decide whether the command runs at all. - Installing this hides the countdown from everyone but ops until you grant the nodes. Grant them
to
defaultto keep stock behavior. /checkspawns <spawner> meneeds no extra node and no CommandWhitelist entry.- Example:
lp group default permission set pixelmon.checkspawns.legendary.time true
/givepixelsprite¶
| Action | Node |
|---|---|
| Give yourself a sprite | pixelmon.command.givepixelsprite.use (added by this mod) |
| Give another player a sprite | pixelmon.command.givepixelsprite.other (Pixelmon's own) |
- A non-op may only ask for a species they have caught (any form counts). Turn this off with
sprite.require-caught = false. - Ops and console skip both nodes and the caught check.
- On this network CommandWhitelist controls who sees the command:
givepixelspriteis in therank3extrasgroup. - Also fixed:
/givepixelsprite pikachuworks (stock needed your own name first), and a full inventory drops the sprite at your feet instead of deleting it.
Testing¶
Always test as a non-op. Ops skip both the nodes and CommandWhitelist, so an op test proves
nothing. For the area change: stand at a biome border, compare /checkspawns legendary me with
/checkspawns legendary, then walk 200 blocks into the other biome and run it again. The personal
list should follow the surrounding biomes.
Configuration¶
config/pixelmoncheckspawnsperms-common.toml, written on first boot. Restart to apply.
accuracy.accurate-area = falserestores stock area behavior while keeping the nodes. This is the rollback that needs no jar swap.sampling.*keys trade accuracy for server load. Keepsamples-per-ticklow to protect TPS.- Area bounds come from Pixelmon's
config/pixelmon/spawning/wild.yml(minimum-distance-from-centre18,maximum-distance-from-centre120, slice radii 5 and 25 onmain).
See the Reference section for every key and default.
Data and storage¶
None. The mod stores nothing.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| Non-op sees no countdown | .time node not granted |
Grant pixelmon.checkspawns.<spawner>.time |
/checkspawns legendary over RCON returns nothing |
With players online, the answer arrives a few ticks later, after RCON has replied | Run it in game or from the console |
| Order of the species list still hints at rarity | Pixelmon sorts by percentage before this mod sees the lines | Known limit; needs a mixin on generateMessages |
/givepixelsprite says "No such command." |
Player is not in a CommandWhitelist group that allows it | Check the rank3extras group |
| Caught check says try again | Player's Pokedex not loaded yet | Retry after a moment (fails closed on purpose) |
Nodes do not tab-complete in /lp |
NeoForge mods have no plugin.yml |
The mod registers them with Bukkit at startup; grant by exact name if missing |
With apply-wildcards: true, registering these nodes with Bukkit may also put them under an existing
pixelmon.* grant. Check with /lp group <g> permission check <node>.
Maintainer notes¶
How the output gating works¶
Pixelmon's LegendaryCheckSpawns.checkSpawns posts a LegendaryCheckSpawnsEvent with
shouldShowTime and shouldShowChance flags. The event does not say which spawner it came from, and
both spawners share one class. So the mod wraps each spawner's public checkSpawns field with a
GatedCheckSpawns that knows its spawner, resolves the caller's nodes into ThreadLocals, then
delegates. The event fires synchronously at the top of checkSpawns, so the listener reads the right
verdicts.
The per-species list has no flag and is built later inside server.execute(...), after the
thread-locals are cleared. What survives is the CommandSourceStack, so the mod passes
sender.withSource(new PercentageStrippingSource(...)) and strips percentages from each line as it
is sent. Only PlainTextContents lines are touched (every other line is translatable), so unrelated
messages with a % are never changed. The wrap is skipped for callers who hold the node. The text
logic is in SpawnPercentageFormat, free of Minecraft types so it can be unit-tested.
/reload rebuilds the spawners (it is a datapack-loader task) and drops the wrappers, so the mod
reinstalls them on ServerStartedEvent and OnDatapackSyncEvent. Nodes are registered through
PlayerHelper.registerPermission at FMLCommonSetupEvent, and mirrored into Bukkit (by reflection)
at ServerStartedEvent so LuckPerms can tab-complete them.
Accurate area (v1.4.0)¶
Stock checks the 11x51x11 box around the caller. The spawner actually picks a random player cluster,
a random member, a random bearing, and a distance between 18 and 120 blocks, then checks the slice
there. There is no hook on that path, so AreaSurveyService computes it:
- Players are clustered and filtered by dimension exactly like
forcefullySpawn(PlayerClustering). - A fixed sample budget is split evenly across clusters, then members (
SampleBudget). - Each sample is one point from the real ring, stratified by angle and radius (
AnnulusSampler). - Pixelmon's own
selectionAlgorithm.getPercentagesscores each sample, and results are averaged, never pooled (PercentageAggregator). The legendary spawner counts a species once per slice, so pooling would over-weight species that fit only one far-off spot.
Cost is bounded by the budget (40 players sample the same 64 areas as 5), samples-per-tick, and a
shared cache of cache-seconds. Snapshots run on the server thread; the rest runs on Pixelmon's
spawner thread. Known small error: rarity is taken from the first location a species fits, where
Pixelmon's spawner averages.
/givepixelsprite¶
Pixelmon builds the command with requires(src -> src.hasPermission(2)) and no node, so LuckPerms
and CommandWhitelist cannot open it. The mod replaces the Brigadier node (BrigadierNodeSurgery,
same approach as pixelmon-endbattle-perms) with a permissive requires and checks nodes in the
body. The caught check walks every form, gender, and palette (CaughtSpeciesLookup) because a spec
with no form picks a random one. It reads getStorageNow, never getStorage, so it never loads a
Pokedex from disk.
Tests¶
67 JUnit 5 tests cover the pure classes (SpawnPercentageFormat, PlayerClustering, SampleBudget,
AnnulusSampler, PercentageAggregator, ScopeArguments, SpriteTarget). Cases worth knowing:
percentages can be in scientific notation (1.0E-4); the color code also leaks magnitude so it is
stripped too; formatting codes are digits, so checks run on rendered text; clustering quirks are
asserted, not fixed, because the goal is to predict the spawner.
Deploying¶
A version bump is a jar swap: two jars with the same mod id will not boot, and swapping under a
running server risks NoClassDefFoundError. Prefer a stopped window.
Building¶
./gradlew test build
./gradlew build needs network access. ./gradlew jar test --offline is enough to build and test.
The Pixelmon compile-only jar is set by pixelmon_jar in gradle.properties; the build fails fast
if the path is wrong. Output: build/libs/pixelmoncheckspawnsperms-<version>.jar.
Full command, permission and config tables: Reference.