Build & extend
Both projects are plain Maven builds — one command produces the jar. Below: toolchain, artifacts, project layout and the usual extension points.
Building from source
Toolchain
| Project | JDK | Maven | Compile target |
|---|---|---|---|
| bukkit | JDK 8 or newer | 3.6+ | --release 8 |
| folia | JDK 17 or newer | 3.6+ | --release 17 |
Commands
defaultGoal is already set to clean package, so plain mvn is enough:
# Bukkit build
cd CreeperConfettiPro-bukkit
mvn clean package
# Folia build
cd CreeperConfettiPro-folia
mvn clean package
Artifacts land in each project's target/:
CreeperConfettiPro-4.2.0-bukkit.jar # the plugin, ~850 KB
original-CreeperConfettiPro-4.2.0-bukkit.jar # pre-shade thin jar — do not use
The original- prefix marks the shade plugin's intermediate output. It has no bStats classes and fails on enable with NoClassDefFoundError.
Dependencies & shading
| Dependency | Version | Scope | Notes |
|---|---|---|---|
spigot-api | 1.9.4-R0.1-SNAPSHOT | provided | Bukkit build only; compile-time |
folia-api | 1.19.4-R0.1-SNAPSHOT | provided | Folia build only |
bstats-bukkit | 3.1.0 | compile | Shaded in, package relocated to avoid clashes |
commons-lang3 | 3.18.0 | compile | Same; bStats' own copy is excluded |
annotations | 24.1.0 | provided | JetBrains annotations |
bStats is relocated by maven-shade-plugin, with a different prefix per build:
<!-- bukkit -->
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>me.cookie.creeperconfetti.libs.org.bstats</shadedPattern>
</relocation>
<!-- folia -->
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>CreeperConfetti.example.creeperConfettiPro.libs.org.bstats</shadedPattern>
</relocation>
The build also writes dependency-reduced-pom.xml to the project root. That is normal shade behaviour; usually you ignore it in version control.
Both poms enable resource filtering (<filtering>true</filtering>), so ${...} inside src/main/resources is substituted by Maven. Escape it or avoid the pattern when writing language files.
Project structure
CreeperConfettiPro-bukkit/
├── pom.xml
├── src/main/java/creeperconfetti/
│ ├── CreeperConfettiPro.java # main class / enable flow
│ ├── ConfigMigrator.java # version migration + backups
│ ├── LanguageManager.java # loading, detection, message formatting
│ ├── Metrics.java # bStats wrapper (pluginId 29666)
│ ├── commands/
│ │ ├── CreeperConfettiCommand.java # subcommand dispatcher
│ │ ├── BaseSubCommand.java # subcommand base class
│ │ ├── CommandAliasManager.java # reflective /cc and /confetti
│ │ └── sub/ # the six subcommands
│ ├── effects/
│ │ ├── FireworkStyleManager.java # styles + chances
│ │ ├── FireworkEffectParser.java # colour / effect parsing
│ │ └── FireworkUtil.java # spawning, reading held items
│ ├── events/CreeperExplodeListener.java
│ ├── gui/ # list page, edit page, base
│ └── util/
│ ├── GeoIpResolver.java # parallel country lookup
│ └── Schedulers.java # Folia build only
└── src/main/resources/
├── plugin.yml
├── config.yml
└── languages/ # 18 files
Key classes
| Class | Responsibility | Risk when changing |
|---|---|---|
CreeperConfettiPro | Enable order: save config → migrate → load styles → register command and aliases → init language → register listeners → start bStats | High — order matters |
CreeperExplodeListener | The only event entry point; decides "explode" vs "firework" | High — defines the gameplay |
FireworkStyleManager | Style table, weight normalisation, weighted random, persistence | Medium — keep the 100% invariant |
FireworkEffectParser | Turns YAML / serialised objects / maps into FireworkEffect | Medium — new colour formats go here |
LanguageManager | Copies language files, detects language, formats messages | Medium — singleton, mind thread safety |
ConfigMigrator | Version check, backup, legacy conversion, key merging | Medium — bump the version when the schema changes |
CommandAliasManager | Reflective CommandMap alias registration | Medium — the most version-sensitive part |
GeoIpResolver | Six endpoints in parallel, first valid answer wins | Low |
Schedulers (Folia) | Every scheduling entry point | High — a mistake crashes the region thread |
Extension points
Adding a subcommand
- Extend
BaseSubCommandincommands/sub/and implementgetName(),getPermission(),getDescriptionKey()andexecute(); - call
register(new YourCommand())in theCreeperConfettiCommandconstructor; - add the permission node to
plugin.ymland attach it to thecreeperconfetti.commandchildren; - add
help.sub.xxxplus your message keys to all 18 language files (missing keys fall back to English).
Control which blocks break
The listener cancels EntityExplodeEvent, which means its block list is never applied. If you would rather keep the explosion and decide yourself which blocks survive, drop setCancelled and edit event.blockList():
@EventHandler
public void onCreeperExplode(EntityExplodeEvent event) {
if (event.getEntity().getType() != EntityType.CREEPER) return;
Creeper creeper = (Creeper) event.getEntity();
Location location = creeper.getLocation().add(0, 1, 0);
// keep the explosion, but protect containers
event.blockList().removeIf(b -> b.getState() instanceof InventoryHolder);
FireworkUtil.spawnAndDetonate(plugin, location,
plugin.getFireworkStyleManager().getRandomEffects(), 0);
}
In this variant the creeper is not removed, so the firework appears alongside the explosion. To remove it too, call creeper.remove() after the event — and on the Folia build wrap it in Schedulers.runOnEntity.
Restrict to specific worlds
The world name is right there in the event; one guard is enough (there is no built-in switch for this):
if (!creeper.getWorld().getName().equals("world")) return;
Supporting another colour format
Parsing lives in FireworkEffectParser.parseColor(), which dispatches by type: Color, Number, String, ConfigurationSection, Map. A new format is one extra branch; nothing else has to change.
If you change the config schema, increment ConfigMigrator.CURRENT_VERSION and add the conversion in migrate() — otherwise existing users end up missing fields after an update.