All Releases - Previous versions and prerelease test builds
━━━━━━━━━━ Overview ━━━━━━━━━━
PackMerger is a Paper plugin that merges multiple Minecraft resource packs into a single pack, hosts it via a built-in HTTP server or Polymath, and automatically distributes it to players on join. Designed for servers and networks that need to combine packs from different sources — UI overhauls, mechanics plugins like QualityArmory, custom model packs — into one seamless download.
Instead of forcing players to manually install or merge packs, drop your packs into a folder, configure priority, and PackMerger handles the rest: intelligent JSON merging, SHA-1 hash tracking, per-server composition, and hot reload during development.
━━━━━━━━━━ Features ━━━━━━━━━━
Automatic Plugin-Pack Discovery - Detects packs generated by other installed plugins (Oraxen, Nexo, ItemsAdder, ModelEngine, EliteMobs, FreeMinecraftModels) and pulls each into the merge automatically — no manual exporting or copying. Unlike file-overwrite mergers, these are deep-merged through the format-aware strategies, so overlapping item models and arrays combine instead of clobbering each other.
Priority-Based Merging - Control which pack's files win on conflict via a simple ordered list.
Format-Aware Deep JSON Merging - Dedicated strategies per Minecraft format (models, blockstates, items, equipment, atlases, font, sounds, language, pack.mcmeta) so nothing is silently lost: overrides/sources/providers/overlays.entries/filter.block arrays concat-dedup by identity, lang files key-union, and same-predicate custom_model_data collisions are flagged at merge time (no more "my knife turned into a pistol").
Merge Provenance + /pm inspect - For every output file, records which pack won and every contributor. Surface via /pm inspect (summary, per-pack, collisions, exportable report) and the plugin API.
Validation + Rollback - Checks pack.mcmeta structure, JSON syntax, and missing texture/model references after every merge; if a merge trips errors, the previous pack stays live for players instead of shipping broken output.
pack_format Guardrail - Warns (or errors) when the merged pack's pack_format doesn't match the running server version. Honors supported_formats ranges and tracks the latest Minecraft versions.
Orphan Asset Detection - Finds unreferenced textures/sounds so you can trim bloated packs.
Profiles / Presets - Flip whole pack compositions atomically via /pm profile switch <name> for seasonal events or A/B tests.
Remote Pack Sources - Declare pack URLs in config; PackMerger downloads + caches them with ETag / Last-Modified and bearer/basic auth using ${ENV_VAR} substitution. /pm fetch to refresh.
Flexible Hosting - Built-in HTTP host (no third-party service; optional auto port), a self-hosted Polymath instance, or any S3-compatible store (AWS S3, Cloudflare R2, Backblaze B2) with content-addressed keys + retention and public or presigned URLs.
Bedrock / Geyser Conversion (opt-in, experimental) - Converts the merged pack's custom item icons into a Bedrock pack + Geyser custom-item mappings so Bedrock players see the same custom items, with optional auto-deploy into Geyser's folders. Targets 2D item icons (1.21.4+ item-definition format); 3D geometry not yet converted.
Network-Wide Distribution (optional) - A companion Velocity proxy plugin offers the merged pack to players from the proxy instead of each backend sending its own; backends can push live URL/hash updates to it over a plugin channel.
Auto-Distribution on Join - The pack is sent to players automatically when they connect, with player-cache tracking that skips re-sending when the SHA-1 already matches.
Per-Server Composition - Multi-server networks can include/exclude packs per backend.
Hot Reload - Watches the packs folder and auto-merges on changes with configurable debounce.
Custom Overrides - Drop a pack.mcmeta or pack.png into the packs folder to override the merged metadata/icon.
Plugin API + Bukkit Events - PackMergedEvent, PackUploadedEvent, PackValidationFailedEvent, and more — other plugins react without log-scraping.
Brigadier Commands - Full tab completion via Paper's modern command API; all player-facing text is externalized in messages_en.yml.
Lean Jar - Ships ~360 KB; heavy optional dependencies (the S3 SDK) are downloaded on first enable and verified by SHA-256.
━━━━━━━━━━ Requirements ━━━━━━━━━━
Paper 26.1+ (built against Paper API 26.1.2)
Java 25+
Tested on Paper and Folia-compatible forks
━━━━━━━━━━ Installation ━━━━━━━━━━
Download the jar from the Downloads section above
Place it in your server's plugins/ folder
Start the server — PackMerger generates config.yml, messages_en.yml, and creates the packs/, output/, and cache/ directories under plugins/PackMerger/
Drop your resource pack .zip files (or unzipped pack folders) into plugins/PackMerger/packs/
Edit plugins/PackMerger/config.yml to set priority order, upload provider, and distribution settings
Run /packmerger reload or restart the server
━━━━━━━━━━ Commands ━━━━━━━━━━
•
/pm reload — Reload config, re-init upload provider, re-fetch on-reload remote packs, trigger a merge
•
/pm validate — Run validation on the current merged pack (JSON, missing refs, orphans, pack_format drift)
•
/pm status — Server name, provider, last merge time, pack URL, SHA-1, file size, discovered packs
•
/pm apply [player] — Force-send the current pack to all online players or one (supports
@a selectors)
•
/pm inspect — Summary of the last merge (1.1.0+)
•
/pm inspect <pack> — Per-pack detail: files won + files contributed but lost on (1.1.0+)
•
/pm inspect collisions — List every output path touched by 2+ packs (1.1.0+)
•
/pm inspect export — Write the full merge report to output/last-merge-report.txt (1.1.0+)
•
/pm priority list — Show current priority order (1.1.0+)
•
/pm priority up|down|top|bottom <pack> — Move a pack in priority (1.1.0+)
•
/pm priority set <pack> <n> — Place at absolute position (1.1.0+)
•
/pm profile list — List defined profiles (1.1.0+)
•
/pm profile switch <name> — Activate a profile and re-merge (1.1.0+)
•
/pm fetch [alias] — Re-download remote packs, all or one (1.1.0+)
━━━━━━━━━━ Permissions ━━━━━━━━━━
packmerger.admin - Access to all /packmerger commands (default: op)
packmerger.notify - Receive chat notifications when a new pack is available (default: true)
━━━━━━━━━━ Configuration ━━━━━━━━━━
Code (Text):
# Identifies this server for per-server pack configs. Auto-detected if empty.
server-name: ""
# Merge order — first entry has highest priority.
priority:
- "main-pack.zip"
- "ui-overhaul.zip"
- "custom-models.zip"
distribution:
enabled: true
required: false # Kick players who decline
prompt-message: "" # Custom MiniMessage prompt (empty = Minecraft default)
use-add-resource-pack: false # true = add alongside existing; false = replace
join-delay-ticks: 20
cache:
enabled: true # Skip re-sending to players with matching SHA-1
on-new-pack:
action: "notify" # "none", "notify", or "resend"
notify-message: "<yellow>[PackMerger]</yellow> <gray>A new resource pack is available. Rejoin or use F3+T to reload.</gray>"
log-level: "info" # "debug", "info", "warning", or "error"
━━━━━━━━━━ How It Works ━━━━━━━━━━
Merge → Upload → Distribute Pipeline
Discover - Scans the packs folder for .zip files and directories containing pack.mcmeta or assets/
Order - Builds a merge order from the priority config, per-server include/exclude rules, and any unlisted packs (sorted alphabetically at lowest priority)
Merge - Iterates packs from lowest to highest priority and combines files in memory, applying format-specific merge strategies for JSON files
Override - Applies any custom pack.mcmeta / pack.png placed directly in the packs folder
Upload - Sends the merged zip to the configured provider (Polymath or self-host)
Distribute - If the SHA-1 changed, notifies online players per the on-new-pack action; new joiners receive it automatically (skipped if cached)
━━━━━━━━━━ JSON Merge Strategies ━━━━━━━━━━
PackMerger uses a different merge strategy for each Minecraft resource-pack JSON format, because naive deep-merge silently drops array data:
Models (assets/<ns>/models/*.json) - Deep merge objects; overrides arrays concat and dedup by predicate (this is what makes QualityArmory CustomModelData merges survive). When two packs collide on the same predicate a warning is logged so the silent drop is visible to operators.
Blockstates (assets/<ns>/blockstates/*.json) - Variants merge per selector key; multipart cases concat and dedup by when clause
Atlases (assets/<ns>/atlases/*.json) - sources arrays concat with structural dedup (1.19.3+)
Item Definitions (assets/<ns>/items/*.json) - Key-level deep merge (1.21.4+)
Equipment (assets/<ns>/equipment/*.json) - Key-level deep merge per slot (1.21.2+)
Sounds (assets/<ns>/sounds.json) - Per-event sound arrays concat so all variants coexist
Font (assets/<ns>/font/*.json) - providers arrays concat so custom glyph packs compose
Language (assets/<ns>/lang/*.json) - Translation keys union across packs; higher priority wins on collision so plugins layering localizations into en_us.json no longer lose each other's strings
pack.mcmeta - Deep-merge on the metadata block; overlays.entries[] concat-dedup by directory (v1.0.4) and filter.block[] concat-dedup by namespace|path (v1.0.5)
Self-Host (Default) Runs a built-in HTTP server; players download from
http://<your-ip>:<port>/pack. Make sure the port is open in your firewall. Includes a concurrent download rate limiter.
Polymath Uploads to a self-hosted
Polymath instance (a lightweight Python server originally built for Oraxen). You must self-host your own Polymath if you choose this provider.
S3 / R2 / Backblaze B2 (v1.1.0+) Upload to any S3-compatible object store. One config works for AWS S3, Cloudflare R2, and Backblaze B2; only the endpoint URL changes. Content-addressed keys with retention keep storage bounded while letting client HTTP caches work cleanly. Private-bucket setups get short-lived presigned URLs automatically. Credentials use
${ENV_VAR} substitution so secrets stay out of config.yml.
━━━━━━━━━━ Per-Server Packs ━━━━━━━━━━
On a multi-server network, each backend can have its own merged pack by defining
server-packs entries. Each server's
additional packs are merged at lowest priority below the global list, and
exclude packs are skipped entirely. The output file is named
<server-name>-merged-pack.zip to avoid collisions.
━━━━━━━━━━ Troubleshooting ━━━━━━━━━━
Check that distribution.enabled is true
Verify a merge has completed: run /pm status and check Pack URL is not "N/A"
Check the console for upload errors — if the upload failed, no URL is available
Ensure upload.auto-upload is true
Another process is using the configured port (default 8080)
Change upload.self-host.port to an unused port
Use netstat -tlnp | grep 8080 (Linux) or netstat -an | findstr 8080 (Windows) to find the conflicting process
"Invalid JSON" - a JSON file has syntax errors; open it in a validator
"Missing texture" - a model references a texture that doesn't exist in the merged pack
"Missing model" - a blockstate references a model that doesn't exist
Validation warnings don't prevent the pack from sending — they flag potential visual issues
Ensure distribution.cache.enabled is true
The cache only updates after the client reports SUCCESSFULLY_LOADED
If using use-add-resource-pack: true, Minecraft may cache differently than replace mode
Delete plugins/PackMerger/cache/player-cache.json to reset
Set log-level: "debug" to see per-player send/skip logs
Check output size with /pm status
Increase merge.optimization.compression-level (up to 9)
Remove unnecessary packs or textures
For very large packs (200+ MB) consider a CDN or Polymath for faster downloads
As of v1.0.3 this is fixed — item-model
overrides arrays from multiple packs are now concat-deduped by predicate. If you're still seeing missing textures:
Update to v1.0.3 or newer
Confirm the QualityArmory pack zip actually contains its assets/qualityarmory/ namespace — unzip the merged output and check
Run /pm validate to surface missing-model / missing-texture references
On older versions, workaround: place QualityArmory as highest priority, or avoid letting other packs redefine the same assets/minecraft/models/item/*.json files
Since v1.1.0, validation errors trigger rollback by default: the new merge is discarded and the previous pack stays live. Check the console for
[VALIDATION] entries mentioning rollback, or watch for
PackValidationFailedEvent if you have a custom listener.
Fix the validation errors in the offending pack and /pm reload
To ship despite errors (not recommended), set validation.rollback-on-errors: false
First-run case: if no previous pack exists, the broken pack ships anyway so the initial merge doesn't stall forever
Since v1.0.5, when two packs both define an item-model override with the same
predicate (e.g. both claim
custom_model_data: 1000001 on
iron_sword), PackMerger logs a warning naming the file and the offending predicate. This is informational — the pack still ships. It surfaces silent drops that previously only showed up in-game when the wrong item rendered.
The higher-priority pack's override wins; the lower-priority entry is dropped
The warning lists the file path and predicate so you can reassign one side's CustomModelData to a different number
If you don't want to change the pack, pick the one that should win and make sure it is listed above the other in priority