How we built it

How the in-house packs are built

Inside the Maven build pipeline for AoW SMP's 6 consolidated plugin packs — Java 21, paper-api, and modular modules.

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.

6in-house packs
~86modules total
1.0.0version of each pack
21JDK compile + runtime
Maven3.8.7
0external compile dependencies

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.

JDK roles
PurposeVersionWhere it lives
Plugin compile target (--release)Java 21Maven maven-compiler-plugin config
Server JVM at runtimeTemurin/OpenJDK 21 (LTS)systemctl status minecraft
Maven itselfMaven 3.8.7/usr/bin/mvn
Server softwarePaperMC 1.21.8/opt/mcserver/paper.jar
Why Java 21? The 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.
Source layout
/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.

Canonical pom.xml
<?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>
One dependency. The only compile-time dependency is 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.
Build command
# 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.

src/main/resources/plugin.yml
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
src/main/java/com/aowmc/mythic/AoWMythic.java (simplified)
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());
    }
}
Ship a config.yml if you call 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.

paper-api only
No third-party plugin imports. Cross-plugin work goes through Bukkit.dispatchCommand.
Adventure API
All text goes through net.kyori.adventure.text.Component — no ChatColor string concatenation.
Vanilla items only
No custom models, no resource packs. Renames + lore + enchantments on real Minecraft items only — keeps Bedrock parity for free.
Server-side
Zero client mods. A Bedrock player on a phone sees the same gameplay as a Java player on a desktop.
ADD-only behavior
Loot tables, generation, and mob spawning are extended, never replaced.
Module toggles
Every module reads a modules.<id> flag from the pack's config. Disable one feature without rebuilding.
Why "vanilla items only" matters. The moment a custom pack emits a custom model data id, you owe every player a resource pack — and Bedrock-via-Geyser handles those very differently from Java. Sticking to enchanted, renamed, lored vanilla items means the same 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.

Dispatching a console 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.

Bedrock-safe naming. When dispatching commands that target a player, we use 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.

Build all 6 packs
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
Deploy
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
What success looks like in the log
[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"
Hot-reload is not used. We don't /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.

GotchaSymptomFix
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.
Watch for renamed Bukkit/Paper enum fields between major versions. A lot of API churn lands as enum or static-field renames that compile cleanly against the wrong version. The primary validation step is 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.

Small surface, total ownership
A developer working on AoWMythic doesn't have to read AoWWorldGen. Packs don't depend on each other at compile time.
Compile is the test
No unit tests; mvn package + a server restart + a 30-second smoke check on the live world is the loop. Fast enough to iterate on.
Vanilla items + Adventure
Sticking to vanilla items and Adventure components means the same code emits text and items correctly on Java and Bedrock with zero special-casing.
ADD-only is forgiving
Because every module is additive, disabling one just removes its content — nothing else breaks. config.yml toggles make this safe to do live.
Console command IPC
"Talk to other plugins through the console" is a stable, version-proof IPC layer. LuckPerms can rewrite its API; lp user X parent add Y still works.
No external infra
JDK + Maven + bash + one VPS. No CI, no registry, no artifact server. The build pipeline is whatever directory you happen to be in.
The full loop, end-to-end. Spec hits the pack → modules are added → write pom + plugin.yml + main class + modules → 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.