This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ExtraMobs is a BentoBox addon (Paper plugin) that re-skins certain natural mob spawns inside GameMode-managed worlds: Zombified Piglins / Piglins → Blaze/Wither Skeleton in the Nether, Endermen → Shulkers in the End, and Fish → Guardians in deep-ocean overworld biomes. The addon does not alter Minecraft's spawn rules — it listens for natural spawns and conditionally cancels + re-spawns a different entity. As of 1.15.0 it also ships a Pladdon entry point so it can be loaded directly by Paper as well as via BentoBox.
mvn clean package— default goal; producestarget/ExtraMobs-<version>-LOCAL.jar.mvn test— runs the test suite.mvn test -Dtest=MobsSpawnListenerTest— single test class.mvn test -Dtest=MobsSpawnListenerTest#methodName— single test method.- Java 21 (
<java.version>21</java.version>in pom.xml). Targets Paper 1.21.11 and BentoBox 3.14.0-SNAPSHOT. - Build versioning is driven by Maven profiles:
-LOCALby default,-b<BUILD_NUMBER>under Jenkins CI, and a clean release version whenGIT_BRANCH=origin/master. Don't hand-edit version strings — changebuild.versioninpom.xml.
Tiny codebase, four production classes:
ExtraMobsAddon(src/main/java/world/bentobox/extramobs/) — extendsworld.bentobox.bentobox.api.addons.Addon. InonLoad()it loadsSettingsvia BentoBox'sConfig<>(auto-createsconfig.ymlfromsrc/main/resources/). InonEnable()it iteratesgetAddonsManager().getGameModeAddons(), setshooked=trueif any GameMode is not indisabledGameModes, and registersMobsSpawnListener. If nothing hooks, the addon disables itself.ExtraMobsPladdon— extendsPladdon. Lets Paper load the addon directly as a plugin (paired withsrc/main/resources/plugin.yml); returns a freshExtraMobsAddonfromgetAddon().config.Settings—ConfigObjectwith@StoreAt(filename="config.yml", path="addons/ExtraMobs"). Field annotations (@ConfigEntry,@ConfigComment) drive both YAML parsing and the on-disk comment block; getters/setters are mandatory for the BentoBox config framework to bind values. Thegamemode-settingsfield is stored asMap<String, Object>because BentoBox's config layer doesn't model the nested-list-of-maps shape;getReplacements(gameMode, env)parses the raw structure intoMobSpawnReplacementrecords on read.config.MobSpawnReplacement— POJO for one per-gamemode rule (oldmob,newmob,chance).resolveOldEntityType()/resolveNewEntityType()upper-case viaLocale.ROOTthen callEntityType.valueOfdefensively.listeners.MobsSpawnListener— single@EventHandler(priority=HIGHEST, ignoreCancelled=true)onCreatureSpawnEvent. OnlySpawnReason.NATURALevents are considered. Flow:resolveActiveGameMode(world)returns the GameMode name (ornullif absent / disabled), thenonEntitySpawndispatches by entity type + environment tohandleNetherSpawn/handleEndSpawn/handleOverworldSpawn. Each handler first checks a "suitable block" predicate (nether brick / purpur / prismarine, with slab+stairs variants), then callsapplyGameModeReplacementsfor per-gamemode rules, and only falls back to the globalnether-chances/end-chances/overworld-chancevalues if no per-gamemode rule fires for that event. The four block sets are pre-computedSet<Material>statics on the class.
The "suitable location" helpers encode the design rule that drives the addon: replacement is gated on the player having built a themed structure. Changes to spawn rules almost always live in these predicates plus the dispatch branches in onEntitySpawn.
Per-gamemode rules supplement the globals rather than suppress them — a per-gamemode rule with chance: 0.05 for ZOMBIFIED_PIGLIN→WITHER_SKELETON falls through to the global wither/blaze chances on a miss, and PIGLIN (different entity) always falls through. The tests testPerGameModeNetherChanceZeroFallsBackToGlobal and testPerGameModeNetherReplacementEntityMismatchFallsBackToGlobal enshrine this behaviour — keep it in mind before changing the listener.
- JUnit 5 + Mockito 5 + MockBukkit (
org.mockbukkit.mockbukkit:mockbukkit-v1.21). Test classes extendCommonTestSetupwhich callsMockBukkit.mock()in@BeforeEachand tears down in@AfterEach; it also injects theBentoBoxsingleton viaWhiteBox.setInternalState(BentoBox.class, "instance", plugin)and statically stubsBukkit+Util. The surefire plugin's long--add-opensargLine plus the leading@{argLine}(which late-binds the Jacoco prepare-agent javaagent) are required; don't strip either. - Jacoco excludes
**/*Names*andorg/bukkit/Material*to avoid synthetic-field / "Material too large to mock" failures. Coverage is reported to SonarCloud via the GitHub Actions workflow.
src/main/resources/addon.yml declares the addon to BentoBox (main, softdepend GameModes, icon). config.yml is filtered (${version} substitution), while locales/*.yml and blueprints/*.{blu,json} are copied unfiltered into the jar root under ./locales and ./blueprints — keep new resource directories consistent with this layout in pom.xml so BentoBox finds them at runtime.