Sueta

PackMerger

1.3.0

Merge, Host, and Distribute Multiple Resource Packs on Paper 1.21+

·Plugin
Tested MC versions
1.21, 26.1, 26.2

Merge, Host, and Distribute Multiple Resource Packs on Paper 26.1+

━━━━━━━━━━ Downloads ━━━━━━━━━━

━━━━━━━━━━ 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.
  • In-Game Priority Reordering - /pm priority up|down|top|bottom|set <pack> edits config and re-merges live.
  • 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 ━━━━━━━━━━

  1. Download the jar from the Downloads section above
  2. Place it in your server's plugins/ folder
  3. Start the server — PackMerger generates config.yml, messages_en.yml, and creates the packs/, output/, and cache/ directories under plugins/PackMerger/
  4. Drop your resource pack .zip files (or unzipped pack folders) into plugins/PackMerger/packs/
  5. Edit plugins/PackMerger/config.yml to set priority order, upload provider, and distribution settings
  6. Run /packmerger reload or restart the server

━━━━━━━━━━ Commands ━━━━━━━━━━

Click to view all 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 ━━━━━━━━━━

config.yml Example
# 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"

# Per-server pack composition for multi-server networks.
server-packs:
  lobby:
   additional:
     - "lobby-textures.zip"
   exclude: []
  survival:
   additional:
     - "survival-textures.zip"
   exclude:
     - "ui-pack.zip"

merge:
  auto-merge-on-startup: true
  optimization:
   strip-junk-files: true       # Remove .DS_Store, Thumbs.db, __MACOSX, .git, etc.
   compression-level: 6         # ZIP compression (0-9)
   size-warning-mb: 100         # Warn if output exceeds this size (0 = disabled)
  hot-reload:
   enabled: true
   debounce-seconds: 5

upload:
  auto-upload: true
  provider: "self-host"          # "self-host" or "polymath"
  self-host:
   port: 8080
   public-url: ""               # Auto-detected from server.properties
   rate-limit: 50               # Max concurrent downloads (0 = unlimited)
  polymath:
   server: "https://your-polymath-host:5000"
   secret: "your-custom-secret"
   id: "my-server"

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

  1. Discover - Scans the packs folder for .zip files and directories containing pack.mcmeta or assets/
  2. Order - Builds a merge order from the priority config, per-server include/exclude rules, and any unlisted packs (sorted alphabetically at lowest priority)
  3. Merge - Iterates packs from lowest to highest priority and combines files in memory, applying format-specific merge strategies for JSON files
  4. Override - Applies any custom pack.mcmeta / pack.png placed directly in the packs folder
  5. Validate - Checks pack.mcmeta structure, JSON syntax, and missing texture/model references
  6. Upload - Sends the merged zip to the configured provider (Polymath or self-host)
  7. 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)
  • Everything else (textures, audio, shaders) - Simple overwrite: higher priority wins

━━━━━━━━━━ Upload Providers ━━━━━━━━━━

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

Merged pack not sending to players
  • 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
Self-host port already in use
  • 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
Pack validation warnings
  • "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
Players re-downloading every join (cache issue)
  • 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
Pack too large / download timeouts
  • 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
QualityArmory / CustomModelData missing after merge

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

Similar projects

Plugin →