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

ProjectJDKMavenCompile target
bukkitJDK 8 or newer3.6+--release 8
foliaJDK 17 or newer3.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

DependencyVersionScopeNotes
spigot-api1.9.4-R0.1-SNAPSHOTprovidedBukkit build only; compile-time
folia-api1.19.4-R0.1-SNAPSHOTprovidedFolia build only
bstats-bukkit3.1.0compileShaded in, package relocated to avoid clashes
commons-lang33.18.0compileSame; bStats' own copy is excluded
annotations24.1.0providedJetBrains 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

ClassResponsibilityRisk when changing
CreeperConfettiProEnable order: save config → migrate → load styles → register command and aliases → init language → register listeners → start bStatsHigh — order matters
CreeperExplodeListenerThe only event entry point; decides "explode" vs "firework"High — defines the gameplay
FireworkStyleManagerStyle table, weight normalisation, weighted random, persistenceMedium — keep the 100% invariant
FireworkEffectParserTurns YAML / serialised objects / maps into FireworkEffectMedium — new colour formats go here
LanguageManagerCopies language files, detects language, formats messagesMedium — singleton, mind thread safety
ConfigMigratorVersion check, backup, legacy conversion, key mergingMedium — bump the version when the schema changes
CommandAliasManagerReflective CommandMap alias registrationMedium — the most version-sensitive part
GeoIpResolverSix endpoints in parallel, first valid answer winsLow
Schedulers (Folia)Every scheduling entry pointHigh — a mistake crashes the region thread

Extension points

Adding a subcommand

  1. Extend BaseSubCommand in commands/sub/ and implement getName(), getPermission(), getDescriptionKey() and execute();
  2. call register(new YourCommand()) in the CreeperConfettiCommand constructor;
  3. add the permission node to plugin.yml and attach it to the creeperconfetti.command children;
  4. add help.sub.xxx plus 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);
}
i

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.

v

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.

构建与二次开发

两个项目都是标准 Maven 工程,一个命令就能出包。下面说明构建环境、产物、项目结构与常用扩展点。

从源码构建

环境准备

项目JDKMaven编译目标
bukkitJDK 8 及以上3.6+--release 8
foliaJDK 17 及以上3.6+--release 17

构建命令

pom.xml 里已把 defaultGoal 设为 clean package,所以直接 mvn 即可:

# Bukkit 分支
cd CreeperConfettiPro-bukkit
mvn clean package

# Folia 分支
cd CreeperConfettiPro-folia
mvn clean package

产物在各自项目的 target/ 下:

CreeperConfettiPro-4.2.0-bukkit.jar          # 最终插件,约 850 KB
original-CreeperConfettiPro-4.2.0-bukkit.jar # shade 之前的瘦包,不要用

带 original- 前缀的是 shade 插件的中间产物,缺少 bStats 依赖,装进服务端会在启用时报 NoClassDefFoundError。

依赖与打包方式

依赖版本scope说明
spigot-api1.9.4-R0.1-SNAPSHOTprovided仅 Bukkit 分支;编译期使用,不打包
folia-api1.19.4-R0.1-SNAPSHOTprovided仅 Folia 分支
bstats-bukkit3.1.0compile被打进 jar,并重定位包名避免冲突
commons-lang33.18.0compile同上;已排除 bStats 自带的旧版本
annotations24.1.0providedJetBrains 注解,编译期使用

bStats 通过 maven-shade-plugin 重定位,两个分支的重定位前缀不同:

<!-- 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>

构建时 Maven 还会在项目根目录生成 dependency-reduced-pom.xml,这是 shade 插件的正常行为,提交代码时通常忽略即可。

!

两个 pom 都开启了资源过滤(<filtering>true</filtering>)。这意味着 src/main/resources 下的文件里出现 ${...} 会被 Maven 尝试替换。写语言文件时如果要用到美元符号加大括号,需要转义或改用其它写法。

项目结构

CreeperConfettiPro-bukkit/
├── pom.xml
├── src/main/java/creeperconfetti/
│   ├── CreeperConfettiPro.java          # 主类 / 启用流程
│   ├── ConfigMigrator.java              # 配置版本迁移与备份
│   ├── LanguageManager.java             # 语言加载、检测、消息格式化
│   ├── Metrics.java                     # bStats 封装(pluginId 29666)
│   ├── commands/
│   │   ├── CreeperConfettiCommand.java  # 子命令分发器
│   │   ├── BaseSubCommand.java          # 子命令基类
│   │   ├── CommandAliasManager.java     # 反射注册 /cc 与 /confetti
│   │   └── sub/                         # 六个子命令实现
│   ├── effects/
│   │   ├── FireworkStyleManager.java    # 样式与概率管理
│   │   ├── FireworkEffectParser.java    # 颜色 / 效果解析
│   │   └── FireworkUtil.java            # 生成烟花、读手持物品
│   ├── events/CreeperExplodeListener.java
│   ├── gui/                             # 列表页 / 编辑页 / 基类
│   └── util/
│       ├── GeoIpResolver.java           # 并行查询国家代码
│       └── Schedulers.java              # 仅 Folia 分支
└── src/main/resources/
    ├── plugin.yml
    ├── config.yml
    └── languages/                       # 18 个语言文件

关键类说明

类职责改动风险
CreeperConfettiPro启用顺序:存配置 → 迁移 → 载样式 → 注册指令与别名 → 初始化语言 → 注册监听器 → 启动 bStats高,注意顺序依赖
CreeperExplodeListener唯一的事件入口,决定「炸」还是「放烟花」高,改动直接影响玩法
FireworkStyleManager样式表、权重归一化、加权随机、持久化中,注意保持总和 100% 的不变量
FireworkEffectParser把 YAML / 序列化对象 / Map 解析成 FireworkEffect中,新增颜色写法在这里加
LanguageManager复制语言文件、检测语言、消息查表与占位符替换中,单例,注意线程安全
ConfigMigrator版本比对、备份、旧格式转换、补齐新键中,改配置结构时必须同步版本号
CommandAliasManager通过反射拿 CommandMap 动态注册别名中,跨版本兼容最脆弱的地方
GeoIpResolver并发查询 6 个接口,第一个有效结果胜出低
Schedulers(Folia)全部调度入口高,改错会直接崩区域线程

扩展点

新增一个子命令

  1. 在 commands/sub/ 下继承 BaseSubCommand,实现 getName()、getPermission()、getDescriptionKey()、execute();
  2. 在 CreeperConfettiCommand 构造函数里 register(new YourCommand());
  3. 在 plugin.yml 的 permissions 下加权限节点,并挂到 creeperconfetti.command 的 children;
  4. 在 18 个语言文件里补 help.sub.xxx 与提示文案(缺了会回退英文)。

控制哪些方块被破坏

监听器默认取消 EntityExplodeEvent,因此它的方块列表不会生效。如果你想保留爆炸、自己决定哪些方块留下,去掉 setCancelled,改为编辑 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);

    // 保留爆炸,但保护容器类方块
    event.blockList().removeIf(b -> b.getState() instanceof InventoryHolder);

    FireworkUtil.spawnAndDetonate(plugin, location,
            plugin.getFireworkStyleManager().getRandomEffects(), 0);
}
i

这种方式下苦力怕不会被移除,烟花会和爆炸同时出现。想彻底不留苦力怕,仍然需要 creeper.remove(),但要放在事件处理之后——Folia 分支请用 Schedulers.runOnEntity 包一层。

只对指定世界生效

事件里能直接拿到世界名,加一行判断即可(当前版本没有内置这个开关):

if (!creeper.getWorld().getName().equals("world")) return;

支持新的颜色写法

颜色解析集中在 FireworkEffectParser.parseColor(),它按类型分派:Color、Number、String、ConfigurationSection、Map。新增一种写法只需要在这个方法里加一个分支,其它代码不用动。

v

改完配置结构记得把 ConfigMigrator.CURRENT_VERSION 加 1,并在 migrate() 里补上转换逻辑,否则老用户升级后会缺字段。