Brings PackMerger onto the 2026 Minecraft/Paper line, adds automatic discovery of packs generated by other plugins, and closes the biggest tooling gaps.
Changed
Built against Paper 26.1.2 (Minecraft's new year-based versioning) on Java 25. Mojang switched from the 1.21.x scheme to <year>.<drop>.<patch> after 1.21.11; the build now targets io.papermc.paper: paper-api:26.1.2.build.+, the Gradle toolchain is Java 25, and plugin.yml api-version is 26.1.
Pack-format registry brought current. Added the late 1.21 line (1.21.7–1.21.8 → 64, 1.21.9–1.21.10 → 69, 1.21.11 → 75) and the new 26.x scheme (26.1–26.1.2 → 84, 26.2 → 88). The registry had stopped at 1.21.6, so the pack_format drift check was silently returning UNKNOWN (a no-op) on every server newer than that. Startup now logs the registry coverage vs. the running server version at debug.
Pack-format guardrail understands 26.1's min_format/max_format. Since 1.21.9 the pack.mcmeta format moved from pack_format / supported_formats to min_format / max_format (plain ints, or [major, minor]). The validator now reads both schemas, so the drift check actually runs on 26.1 packs — previously it saw no pack_format and silently skipped. A merged pack still declaring a stale format for an older version is now surfaced instead of shipping silently.
Added
Automatic plugin-pack discovery. PackMerger now detects installed pack-generating plugins — Oraxen, Nexo, ItemsAdder, ModelEngine, EliteMobs, FreeMinecraftModels — stages each one's current generated pack into packs/.plugin-packs/<alias>.zip before every merge, and deep-merges them through the format-aware strategies (not just whole-file overwrite). Reference an alias in priority: to control precedence; override a plugin's source path or disable it under plugin-packs.sources.<alias>. Re-staged on every merge, so <plugin> reload + /pm merge always picks up fresh output. Config: plugin-packs (enabled, merge-delay-seconds, sources).
Missing-integration advisory. When a pack-generating plugin is installed but not being merged (disabled, or no generated pack found), admins are warned on the console at startup and on join.
Auto self-host port. upload.self-host.port: -1 derives the HTTP port from the Minecraft server port + 1, so operators don't have to pick a free port manually.
Bedrock / Geyser conversion (opt-in, experimental). New packmerger-bedrock module converts the merged Java pack's custom items into a Bedrock pack + Geyser custom-item mappings so Bedrock/Floodgate players see the same custom items. Targets the 1.21.4+ item-definition subset (custom_model_data → model → layer0 texture, i.e. 2D item icons; 3D geometry is not yet converted). Writes a .mcpack
<name>.geyser.json to output/bedrock/ and can auto-deploy them into Geyser's packs/ and custom_mappings/ folders. Config: bedrock (enabled — off by default, auto-deploy-to-geyser, geyser-folder, debug); new PackBedrockConvertedEvent. Verify against a live Geyser/Bedrock client before use.
Velocity proxy module (network-wide distribution). New packmerger-velocity plugin offers the merged pack to every player from the proxy, so distribution is network-wide instead of per-backend. The proxy reuses the backend's existing hosting — give it the pack URL in its config.properties, and (optionally) set distribution.proxy-notify: true on backends to push live URL/hash updates over the packmergerpack plugin channel. Shares a new packmerger-common module (the PackInfo + message codec) with the backend.
CI workflow (.github/workflows/ci.yml) running ./gradlew build (full test suite + shaded jar) on every push and PR. Until now nothing ran the tests in CI.
Dependabot (.github/dependabot.yml) for the Gradle and GitHub-Actions ecosystems, keeping dependencies and Action pins current via CI-gated PRs.
Gradle version catalog (gradle/libs.versions.toml) as the single source of dependency versions; de-duplicates the MinIO coordinate that was declared twice.
docs/TESTING.md documenting the per-release Paper + Folia smoke test and the integration-test roadmap for the (currently Bukkit-coupled) I/O layer.
config-version field in config.yml. PackMerger logs a one-line notice when a config predates the current schema, so silently-defaulted new keys are visible.
bStats metrics (plugin id 32086), shaded + relocated. Anonymous usage stats only; opt out globally in plugins/bStats/config.yml. Init is guarded so a scheduler incompatibility never blocks enable.
Build / dependencies
Monorepo. Restructured into a Gradle multi-module build under a uniform packmerger-* naming scheme: packmerger-plugin (the backend jar, packmerger-plugin-<version>.jar), packmerger-velocity (the proxy jar, packmerger-velocity-<version>.jar), and the shaded-in libraries packmerger-bedrock and packmerger-common. Dependency versions live in the shared root gradle/libs.versions.toml.
Gradle wrapper 8.12 → 9.6.0 (required for Java 25); pinned with a checksum.
Shadow plugin 8.3.6 → 9.4.2 (required for Gradle 9).
gson 2.11.0 → 2.14.0, commons-compress 1.27.1 → 1.28.0, minio 8.5.10 → 9.0.3, JUnit 5.11.3 → 6.1.0. MinIO 9.x is a major API rewrite — S3UploadProvider was migrated (io.minio.http.Method → io.minio.Http.Method); the runtime S3 upload path isn't CI-verified.
The release workflow now runs ./gradlew build (tests gate the release) on Java 25 and publishes both jars.
Fixed
PolymathUploadProvider no longer echoes an unbounded upstream response body into the console on a failed upload; error messages are collapsed to a single, length-capped snippet.
Update-check manifest URL pointed at the non-existent /main/ branch (default is master), so the check silently 404'd; both the config default and UpdateChecker.DEFAULT_URL now use /master/. The admin-facing "update available" link points to the SpigotMC page.
Operator-experience release. Correctness of the merge engine is settled from
1.0.x; this one focuses on observability, validation, admin workflow, and
external integrations.
Added
Per-file merge provenance log. Every output path now records which pack wrote it, every contributor (in merge order), the merge strategy used, and whether it was a true merge vs single-contributor pass-through. Persisted to output/<merged-pack>.zip.provenance.json so restarts don't blank the state. Exposed via PackMerger.getLastMergeProvenance().
/pm inspect collisions — list of every path touched by 2+ packs
/pm inspect export — writes the full plain-text report to output/last-merge-report.txt
Plugin API + Bukkit events. New sh.pcx.packmerger.api.PackMergerApi interface accessible via plugin.getApi() with accessors for the current pack URL, SHA-1, last merge time, and provenance, plus triggerMerge(). Six new Bukkit events fire at their natural call sites: PackMergeStartedEvent, PackMergedEvent, PackUploadedEvent, PackUploadFailedEvent, PackValidationFailedEvent, PackSentToPlayerEvent. See docs/api-example.java for a sample listener. Tagged @Experimental until 1.2 to allow iteration in 1.1.x.
pack_format vs server-version guardrail. PackValidator now warns when the merged pack's pack_format doesn't match the running Minecraft version. Honors supported_formats ranges (int, array, and {min_inclusive, max_inclusive} object shapes). Config key: validation.pack-format-check (warn | error | off).
Rollback on validation failure. Merges now write to <output>.new.zip first; validation runs against the temp, and the previous output is kept live if validation errors trip. Fires PackValidationFailedEvent with rolledBack=true so listeners can page someone. Config: validation.rollback-on-errors (default true) and validation.fail-on-warnings (default false).
Orphan asset detection. New OrphanDetector scans the merged output for unreferenced .png and .ogg files and reports them as warnings. Reference discovery covers models, item definitions, blockstates, atlases (with directory glob expansion), font providers, and sounds.json. Config: validation.detect-orphans (default true) and validation.orphan-report-limit (default 20).
/pm priority in-game reordering. No more YAML edit + reload: /pm priority list|up|down|top|bottom|set <pack> <n>. Persists via plugin.saveConfig() and triggers an immediate re-merge. Note: Bukkit's config writer doesn't preserve comments on save — documented tradeoff.
Profiles / presets. New profiles: + active-profile: config keys let operators flip between whole pack compositions atomically via /pm profile switch <name>. When profiles aren't in use (or the section is absent), PackMerger falls back to root-level keys — fully backwards- compatible with 1.0.x configs.
Remote pack sources (HTTP). New remote-packs: config section lets admins declare pack aliases whose contents come from an HTTP(S) URL. Packs are downloaded into packs/.remote-cache/<alias>.zip and recognized in the merge pipeline by their alias. ETag / Last-Modified caching, env-var substitution for secrets, bearer + basic auth, HTTPS- required-by-default. New /pm fetch [alias] command. Zero new runtime deps — uses the JDK's HttpClient.
S3-compatible upload provider. New provider: "s3" setting with support for AWS S3, Cloudflare R2, and Backblaze B2 (all via the S3 API). Bundled MinIO SDK, fully shaded + relocated. Supports content-addressed or stable key strategies, public-read or presigned (private) ACLs, and a retention policy for content-addressed buckets. Jar grows from ~270 KB to ~13 MB as a result; document in release notes.
Two new PluginLogger categories: validation() (light purple) and remote() (blue).
Runtime dependency loader. New PackMergerLoader downloads MinIO and its transitive closure from Maven Central on first enable, verifies each artifact against a build-time SHA-256, and loads them through an isolated classloader. Cached in plugins/PackMerger/libraries/ for subsequent starts. Drops the shipped jar from ~13 MB to ~360 KB.
Update check. On enable, the plugin polls versions.json in the repo to see if a newer release is available and surfaces it in the console + as a chat notice to admins on join. Advisory only — no auto-download. Config: update-check.enabled / update-check.url.
Actual Folia support. Swapped every BukkitScheduler call to the right Folia scheduler (AsyncScheduler for periodic async work, GlobalRegionScheduler / entity scheduler where a specific thread matters). PackDistributor.sendPack now self-schedules on the player's region so callers don't have to know about threading. plugin.yml declares folia-supported: true. Paper behavior is unchanged — the scheduler APIs we use exist on both.
Changed
PackMergeEngine.merge() now accepts an optional target-file override (merge(File)), used by PackMerger for the write-to-temp-then-validate flow. Backwards-compatible: merge() keeps writing to plugin.getOutputFile().
Merge provenance moved from a single fixed-name file (output/.merge-provenance.json) to a sidecar keyed by output name (output/<merged-pack>.zip.provenance.json). Lets rollback rename the zip and sidecar together.
FileWatcher now explicitly ignores dot-prefixed entries so the .remote-cache/ subdirectory can't trigger cascading merges.
Dependencies
MinIO Java SDK 8.5.10 + its transitive closure (OkHttp, Okio, Kotlin stdlib, Jackson, BouncyCastle, Guava, Xerial Snappy) is now downloaded at runtime by the loader rather than shaded into the plugin jar. Manifest and SHA-256 digests live in RuntimeDependencies.java (auto-generated at build time from the runtimeDownload Gradle configuration).