Skip to content

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

  1. Gates the output. Three LuckPerms nodes per spawner decide whether a player sees the countdown, the per-roll chance, and the per-species percentages.
  2. 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.
  3. Opens /givepixelsprite to 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.legendary and pixelmon.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 default to keep stock behavior.
  • /checkspawns <spawner> me needs 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: givepixelsprite is in the rank3extras group.
  • Also fixed: /givepixelsprite pikachu works (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 = false restores stock area behavior while keeping the nodes. This is the rollback that needs no jar swap.
  • sampling.* keys trade accuracy for server load. Keep samples-per-tick low to protect TPS.
  • Area bounds come from Pixelmon's config/pixelmon/spawning/wild.yml (minimum-distance-from-centre 18, maximum-distance-from-centre 120, slice radii 5 and 25 on main).

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.getPercentages scores 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.