All 6 consolidated in-house packs on AoW SMP are written, compiled, and shipped from the same box that runs the
server. There is no external CI, no IDE, no GitHub Actions — just a JDK, Maven, a directory of
sources at /opt/aow-plugins/, and a modular architecture where each pack contains focused modules.
This page documents how the build pipeline actually works, from environment up.
1Build environment
The compiler and runtime both use Java 21 LTS. Paper 1.21.8 declares Java 21 as its release floor, so we compile and run on the same LTS line. There is no version mismatch to worry about.
| Purpose | Version | Where it lives |
|---|---|---|
Plugin compile target (--release) | Java 21 | Maven maven-compiler-plugin config |
| Server JVM at runtime | Temurin/OpenJDK 21 (LTS) | systemctl status minecraft |
| Maven itself | Maven 3.8.7 | /usr/bin/mvn |
| Server software | PaperMC 1.21.8 | /opt/mcserver/paper.jar |
paper-api artifact we depend on is
1.21.8-R0.1-SNAPSHOT, which declares release 21. We compile and run on Java 21 so a clean
mvn package never trips a "class file has wrong version" check.
/opt/aow-plugins/
├── AoWMythic/
│ ├── pom.xml
│ ├── src/main/java/com/aowmc/mythic/
│ │ ├── AoWMythic.java # plugin main
│ │ ├── core/ # shared pack utilities
│ │ ├── champions/ # champion tier/title logic
│ │ ├── affixes/ # affix handlers
│ │ ├── signatures/ # signature attacks
│ │ ├── bloodmoon/ # Blood Moon event
│ │ ├── codex/ # player codex
│ │ ├── bounties/ # daily bounties
│ │ └── ...
│ └── src/main/resources/
│ ├── plugin.yml
│ └── config.yml
├── AoWWorldGen/
├── AoWMobs/
├── AoWQoL/
├── AoWContent/
└── AoWInfra/
└── (same modular shape)
Each pack is a self-contained Maven project. Inside a pack, every feature lives in its own module sub-package.
Modules are loaded by the pack's main class, each reading its own toggle from the pack's config.yml.
This keeps builds atomic (one jar per pack) while still letting features be enabled or disabled independently.
2Maven setup
Every pack uses the same canonical pom.xml shape. It pins the PaperMC repo, declares
paper-api as provided (the server brings the runtime), and pins
maven-compiler-plugin 3.13.0 with release 21.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.aow</groupId>
<artifactId>AoWMythic</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21.8-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
paper-api.
No Vault, no LuckPerms-API, no Floodgate-API, no PlaceholderAPI. If a pack needs to talk to a third-party
plugin, it does so by dispatching a console command (see §5) rather than linking against its API.
# In any pack directory
cd /opt/aow-plugins/AoWMythic
export JAVA_HOME=/usr/lib/jvm/temurin-21-jdk-amd64
mvn -q -B package
# Output
ls -la target/AoWMythic.jar
3Pack skeleton
A Paper pack is four files: pom.xml (above), plugin.yml, a main class
extending JavaPlugin, and optionally a config.yml. Each module then hooks one hot
event (e.g. CreatureSpawnEvent) and fans out to the modules sorted by priority.
name: AoWMythic
version: 1.0.0
main: com.aowmc.mythic.AoWMythic
api-version: '1.21'
author: AoW
description: Champion ecosystem for AoW SMP.
commands:
champions:
description: Champions admin hub.
usage: /champions
codex:
description: Open your champion codex.
usage: /codex
bounty:
description: Champion bounties.
usage: /bounty
package com.aowmc.mythic;
import org.bukkit.plugin.java.JavaPlugin;
import com.aowmc.mythic.core.Module;
import com.aowmc.mythic.champions.ChampionsModule;
import com.aowmc.mythic.affixes.AffixesModule;
import com.aowmc.mythic.codex.CodexModule;
import com.aowmc.mythic.bounties.BountyModule;
public final class AoWMythic extends JavaPlugin {
private final List<Module> modules = new ArrayList<>();
@Override
public void onEnable() {
saveDefaultConfig();
modules.add(new ChampionsModule(this));
modules.add(new AffixesModule(this));
modules.add(new CodexModule(this));
modules.add(new BountyModule(this));
// ... register one listener per hot event, fan out to modules
getLogger().info("Enabling AoWMythic v" + getDescription().getVersion());
}
}
saveDefaultConfig(). Paper will not invent one for you;
a missing default crashes a pack on enable. Every pack ships a config.yml with modules.<id> toggles.
4The hard rules
Every pack and module is held to the same constraints. These are what let the server stay stable while multiple gameplay systems coexist.
Bukkit.dispatchCommand.net.kyori.adventure.text.Component — no ChatColor string concatenation.modules.<id> flag from the pack's config. Disable one feature without rebuilding.ItemStack looks correct on a Pixel phone and on
a 4K Java client with no extra plumbing.
5Cross-pack actions
Packs never import each other's code. When a pack needs to hand a player a rank, currency, or title, it dispatches a console command. The console runs as OP, so the third-party plugin (LuckPerms, EssentialsX) sees a normal admin command.
import org.bukkit.Bukkit;
import org.bukkit.command.ConsoleCommandSender;
// AoWInfra onboarding — give new player the 'member' rank via LuckPerms
ConsoleCommandSender console = Bukkit.getConsoleSender();
Bukkit.dispatchCommand(console, "lp user " + player.getName() + " parent add member");
// AoWContent daily — pay the player via Essentials/Vault economy
int amount = Math.min(500, 50 + streak * 10);
Bukkit.dispatchCommand(console, "eco give " + player.getName() + " " + amount);
This isolates each pack from version churn in any third-party plugin's API surface. If EssentialsX bumps its economy interface, none of our packs break — because none of them link against it. They just fire strings at the console.
player.getName(), which already includes the Floodgate . prefix for Bedrock players
(e.g. .Steve). Commands like lp user .Steve parent add member just work.
6Build & deploy loop
Once a jar exists in target/, deploy is two commands: copy the jar into the server's
plugins/ directory and restart the systemd unit.
for pack in AoWMythic AoWWorldGen AoWMobs AoWQoL AoWContent AoWInfra; do
cd /opt/aow-plugins/$pack
export JAVA_HOME=/usr/lib/jvm/temurin-21-jdk-amd64
mvn -q -B package || { echo "FAILED $pack"; exit 1; }
done
for pack in AoWMythic AoWWorldGen AoWMobs AoWQoL AoWContent AoWInfra; do
cp /opt/aow-plugins/$pack/target/$pack.jar /opt/mcserver/plugins/
chown minecraft:minecraft /opt/mcserver/plugins/$pack.jar
done
sudo systemctl restart minecraft
# Watch it come up
journalctl -u minecraft -f
[INFO]: [AoWMythic] Loading server plugin AoWMythic v1.0.0
[INFO]: [AoWMythic] Enabling AoWMythic v1.0.0
[INFO]: [AoWWorldGen] Enabling AoWWorldGen v1.0.0
[INFO]: [AoWMobs] Enabling AoWMobs v1.0.0
[INFO]: [AoWQoL] Enabling AoWQoL v1.0.0
[INFO]: [AoWContent] Enabling AoWContent v1.0.0
[INFO]: [AoWInfra] Enabling AoWInfra v1.0.0
...
[INFO]: Done! For help, type "help"
/reload the whole server when shipping pack
changes — Paper rightly hates it. We restart the systemd unit. Players are warned, the world saves, and the
new jars enable cleanly on boot.
7Notable gotchas
These are the things that bit us during the build pipeline. All of them are now part of the pre-build checklist.
| Gotcha | Symptom | Fix |
|---|---|---|
maven-compiler-plugin default |
Old Maven defaults the compiler plugin to 3.1, which defaults source/target to Java 5. Builds fail with cryptic "lambda not supported" errors on perfectly normal Java 21 code. | Pin maven-compiler-plugin to 3.13.0 explicitly and set <release>21</release>. See the canonical pom.xml in §2. |
Missing config.yml |
A pack crashes on enable: saveDefaultConfig() was called, but no config.yml resource existed in the jar, so subsequent config reads return defaults and later code NPEs. |
Any pack that calls saveDefaultConfig() must ship src/main/resources/config.yml. Now part of the build checklist. |
Attribute.GENERIC_MAX_HEALTH |
Compile failure: cannot find symbol: variable GENERIC_MAX_HEALTH (or GENERIC_MOVEMENT_SPEED, etc.). |
Attribute static fields dropped the GENERIC_ prefix. Use Attribute.MAX_HEALTH, Attribute.MOVEMENT_SPEED, Attribute.ATTACK_DAMAGE. |
Biome is not an enum |
switch (biome) or Biome.DESERT does not compile against paper-api 1.21. |
Use the key string: biome.getKey().getKey().toLowerCase().contains("desert"). |
| Effectively final lambdas | Compile error: "variable used in lambda expression should be final or effectively final". | Copy reassigned variables to a final local before the lambda. |
mvn -B package against paper-api 1.21.8-R0.1-SNAPSHOT.
8Why this pipeline works
The architecture is the point. Six packs, each with focused modules, each compiled as a single jar, each owned end-to-end. That's why the build pipeline finishes in minutes and why fixing a bug means rebuilding one pack, not a monolith.
mvn package + a server restart + a 30-second smoke check on the live world is the loop. Fast enough to iterate on.config.yml toggles make this safe to do live.lp user X parent add Y still works.mvn -B package → fix any compile errors → jar drops into
plugins/ → systemctl restart minecraft → log shows
[AoW<Pack>] Enabling AoW<Pack> v1.0.0 → done. Six jars, one clean stack.