Skip to main content

Java-to-Bedrock Conversion

ResourcePackManager can convert the merged Java resource pack into a Bedrock resource pack so GeyserMC clients can see supported custom content from the Java pack. This is on by default.

When Conversion Runs​

Conversion is gated on a Bedrock target being present. RSPM considers a target present if any of these is true:

  1. Geyser-Spigot is installed on this backend (Bedrock players hit Geyser locally).
  2. Floodgate is installed on this backend (typical proxy-backend setup — Floodgate runs locally, Geyser is somewhere else).
  3. Network mode is active — RSPM detected it's behind a Velocity / BungeeCord proxy. The backend produces its Bedrock pack and exposes it on a small HTTP server so the proxy plugin can fetch it.

Conversion must also be enabled with bedrockConversionEnabled. If it is disabled or no target is present, RSPM withdraws any previous Bedrock output and skips conversion. /rspm status reports why the pack has not been generated.

What Gets Converted​

The converter is namespace-agnostic. It walks every assets/<namespace>/items/**/*.json file in the 1.21.4+ items-definition format, recursively and including the minecraft namespace.

Flat vs 3D routing​

For each leaf model the converter decides which of two pipelines to use. Models rooted in minecraft:item/generated or minecraft:builtin/generated stay on the flat path. For every other parent chain, a model goes down the 3D pipeline only when the merged model carries a non-empty elements array; a model without geometry is a 2D sprite.

  • Flat — the model's layer0 texture is copied straight to textures/items/<hash>.png and registered as a Geyser icon. Bedrock uses that layer as its 2D inventory and held-item sprite; this does not preserve arbitrary additional Java layers or model branches.
  • 3D — the converter stitches a texture atlas, converts the Java cuboids into Bedrock geometry, generates hold/head animations, software-renders a 64×64 inventory icon, and writes one attachable per (model × base item × item definition × predicate shape) mapping.

Why the geometry check matters: a flat handheld tool (parent minecraft:item/handheld with only a layer0 texture and no elements) is still a 2D sprite. Earlier versions pushed flat handheld items into the 3D pipeline, where they failed at the geometry step and vanished on Bedrock or fell back to the vanilla base-item icon. This particularly affected ItemsAdder packs, which ship many flat handheld items. Because elements is read from the merged parent chain, a non-generated model that inherits geometry from a parent is still routed to 3D.

RSPM derives a Bedrock identifier for each (model × base item × item definition × predicate shape) mapping. This separates a sword model's different base-item and predicate variants, and keeps two item definitions that reuse one model on separate identifiers. Generated filenames use short hashes of source identifiers, not file contents, to avoid Geyser's warning for pack paths of 80 or more characters.

Supported item-model branches​

Conversion supports model, condition, range_dispatch, and select nodes (with or without the minecraft: prefix). Other node types, including composite and special, are skipped. A Java pack rendering correctly does not establish that every branch can be represented on Bedrock.

NodeSupported properties and fallback behavior
conditionbroken, damaged, fishing_rod_cast, has_component, custom_model_data. using_item keeps only on_false as the idle model. Other unsupported conditions skip both branches.
range_dispatchdamage, count, bundle_fullness, custom_model_data. use_duration and other unsupported properties keep only fallback, if present.
selectcharge_type, trim_material, context_dimension, custom_model_data. display_context keeps the gui case, or its fallback if no GUI case exists. Other unsupported properties keep only their fallback.

This means Java bow draw stages and separate per-context models do not automatically survive conversion. Enable bedrockConverterDebug to see skipped node and property messages, and check the resulting item on a Bedrock client.

Legacy pre-1.21.4 packs​

The legacy scanner reads JSON files directly inside assets/minecraft/models/item/ and synthesizes range-dispatch definitions from overrides with a numeric predicate.custom_model_data. It does not recursively scan legacy subfolders or preserve the override's other predicates. An override that also depends on a bow's pull state, for example, loses that condition. The legacy item count appears only with bedrockConverterDebug enabled. Use the 1.21.4+ items-definition format for maintained packs, and check its branches against the support table above.

Base items​

Geyser maps each custom item under one or more Java base items. RSPM chooses them as follows:

  • A legacy override file and a root assets/minecraft/items/<item>.json definition map to that vanilla item.
  • Other definitions are matched by filename. Weapon and tool words map to related vanilla items: sword to the six swords, pickaxe to the six pickaxes, wand or staff to minecraft:stick, spear to minecraft:trident, and so on. Armor names use the namespace's equipment file or slot words such as helmet and boots. Anything else uses a fallback set: paper, stick, name tag, compass and the six swords.
  • Definitions in the freeminecraftmodels namespace are also mapped under minecraft:leather_horse_armor, the item FreeMinecraftModels uses to display model bones.
  • A producer plugin can declare the real base items in assets/<namespace>/rspm_item_bases/<path>.json, where <path> matches items/<path>.json. The file holds {"base_items": ["minecraft:blaze_rod"]}. Only minecraft: IDs are read, and they are added to the filename choice. FreeMinecraftModels writes one for each custom item with the material its config names, so a wand on BLAZE_ROD renders its model on Bedrock.

Java-only items​

A producer plugin can keep items out of the conversion by placing a file at assets/<namespace>/rspm_item_java_only/<path>.json. Only the file's presence matters. It excludes items/<path>.json and every item definition under items/<path>/, so those items get no Geyser mapping, icon or attachable. FreeMinecraftModels uses it for the sprite items behind its particle effects, which only Java item displays use; Bedrock players see the native particle instead. A pack whose items are all Java-only does not trigger the legacy-format warning.

Hand-authored Bedrock entity bundles​

A plugin can ship native Bedrock entity assets directly by placing them under assets/<namespace>/rspm_bedrock_pack/ in its Java pack. RSPM imports the files into the generated Bedrock pack. Only entity-related directories are accepted (entity, models/entity, animations, animation_controllers, render_controllers, materials, particles, textures/entity) so a contributing plugin can't shadow the pack manifest or the icon atlas. Supported paths of 80 or more characters are shortened, and exact JSON path references within that bundle are rewritten to match. Files are otherwise copied as supplied. Two namespaces producing different bytes at the same destination cause a hard error.

This is the mechanism behind true custom Bedrock entities — see Geyser extension & custom entities. A pack containing only entity bundles and zero item mappings still ships.

Worn-armor conversion runs on the flat-item path for an armor or elytra base item. It derives <material> from the item-definition filename by removing a recognized slot suffix, then looks for assets/<namespace>/equipment/<material>.json in the leaf model's namespace. It uses the first texture layer in humanoid, humanoid_leggings for leggings, or wings for an elytra when present. The corresponding PNG must exist in the merged pack. The generated attachable uses vanilla Bedrock armor geometry. An equipment file alone, additional texture layers, or an unrelated equipment asset ID does not establish that the armor will convert.

The Bedrock pack's manifest header/module UUIDs are derived deterministically from the plugin version string (seeds rspm_bedrock_header:<pluginVersion> and rspm_bedrock_module:<pluginVersion>), so they stay stable across rebuilds of the same plugin version and only change when the plugin version changes. The visible header name is the fixed ResourcePackManager Bedrock Pack; it is not part of the UUID. The version triplet is bumped per build from a cache-bust token derived from a SHA-256 content digest of the staged Bedrock pack — identical contents yield the same version (so no-op rebuilds don't churn Geyser's cache), while real content changes invalidate Bedrock's (uuid, version)-keyed pack cache. Build time (System.currentTimeMillis()) is only used as a fallback when the content digest can't be computed.

Per-Session Live Serving (Standalone)​

With conversion enabled and Geyser-Spigot on the same standalone backend, RSPM registers a SessionLoadResourcePacksEvent subscriber. It offers the latest authorized Bedrock pack from disk to each joining player. Texture or geometry edits can reach the next join without a server restart when the registered item mappings stay unchanged. Network-mode backends leave serving to the proxy, even if Geyser-Spigot is also installed locally.

Geyser's custom-item mappings, the JSON in custom_mappings/, are boot-frozen. Adding or removing items, changing a model reference, or changing predicates can require new mappings and therefore a restart. RSPM pre-deploys the previous authorized mapping file early in startup when auto-deploy is enabled. After the first successful conversion, restart Geyser to load newly generated mappings, then reconnect the Bedrock client.

Currently connected Bedrock players keep whatever pack they received at their own join time — that's a Bedrock protocol constraint, not something the plugin can override mid-session.

How It Works in Proxy Mode​

In a proxy network, the backend itself does not register a SessionLoadResourcePacks subscriber (there's no local Geyser to subscribe to). Instead:

  1. The backend produces output/ResourcePackManager_Bedrock.zip and, when item mappings exist, output/rspm_geyser_mappings.json.
  2. The backend starts a small HTTP server (auto-derived port, mcPort + 1 by default) exposing /bedrock.zip and /mappings.json routes that read the file fresh on every request, and announces the exact port it bound to the proxy.
  3. The proxy plugin polls every 5 seconds, preferring strong ETags through If-None-Match and retaining If-Modified-Since compatibility for older backends. It downloads each backend's outputs when they change and waits for the inbox to stabilize.
  4. The proxy plugin merges every backend's Bedrock pack into a single network-wide pack and serves it to Bedrock clients through the proxy's Geyser.
  5. If the proxy can't reach a backend's HTTP port directly (common on shared/managed hosting where adjacent ports are firewalled), the backend pushes its files to a magmaguy.com relay endpoint and the proxy fetches them through there.

See Proxy networks for setup. Proxy-side merging is automatic and has no enable toggle. The proxy config has just two settings: network-http-offset-v2, the fallback port offset used before a backend endpoint announcement is available, and geyser-extension-auto-install, the proxy-side equivalent of geyserExtensionAutoInstall.

Output Files​

After a successful mix, the Bedrock files live in:

plugins/ResourcePackManager/output/ResourcePackManager_Bedrock.zip
plugins/ResourcePackManager/output/rspm_geyser_mappings.json # only when item mappings exist

If auto-deploy is on and a local Geyser data folder was detected, the mappings file is also copied to:

<geyser-folder>/custom_mappings/rspm_geyser_mappings.json

The Bedrock pack zip is not copied into <geyser-folder>/packs/ — it's served live per-session instead. If an older RSPM release left ResourcePackManager_Bedrock.zip in Geyser's pack directory, the current plugin does not delete it while Geyser is running: Geyser has already scanned that path and may still hold it in memory. RSPM skips live-provider registration for that boot and prints the exact legacy path. Fully stop the server, delete only that legacy file, then start the server again. A /reload is not sufficient.

When there are no convertible item mappings or allowed entity-bundle files, RSPM withdraws publication and attempts to delete previous local outputs and its deployed Geyser mapping. A locked leftover file does not remain authorized for serving. The backend's /bedrock.zip route returns 404 to indicate that this backend has no Bedrock content to contribute. Entity-only packs are still published. Plain Java block or vanilla texture replacements do not produce a converted pack; those need a separately authored Bedrock pack.

Disabling bedrockAutoDeployToGeyser removes RSPM's previously deployed mapping during reconciliation. Changing bedrockGeyserFolder also cleans the previous owned destination. Neither action changes the mappings already loaded into a running Geyser instance; restart Geyser to apply the new mapping set.

config.yml Settings​

# Toggles Java-to-Bedrock conversion altogether.
bedrockConversionEnabled: true

# Copies the Geyser custom mappings file into the detected Geyser folder's
# custom_mappings/ directory on each mix.
bedrockAutoDeployToGeyser: true

# Manual override for the Geyser data folder. Empty = auto-detect.
# Relative values are tried from the server working directory first, then
# relative to plugins/. Absolute paths work directly.
bedrockGeyserFolder: ""

# Installs and updates the universal ResourcePackManager.jar in Geyser's
# extensions/ folder, which is what makes custom Bedrock ENTITIES render
# instead of armor stands. Separate from pack conversion above — items,
# textures and models convert and serve normally either way.
# Setting this false stops future installation and update staging; it does NOT
# remove an extension jar that is already installed. Delete that by hand while
# Geyser is stopped. See the Geyser extension page.
geyserExtensionAutoInstall: true

# Verbose per-item / per-bone progress logging from the Bedrock pipeline.
# Default false — a clean run emits a single "Bedrock conversion complete: N
# mappings" summary instead of dozens to hundreds of per-item lines. Flip on
# when debugging a specific conversion issue.
bedrockConverterDebug: false

Auto-detection of the Geyser folder looks at, in order:

  1. bedrockGeyserFolder if set (used as written first, so relative paths resolve from the server working directory; if that does not exist, it is tried relative to plugins/). Absolute paths also work.
  2. plugins/Geyser-Spigot/
  3. plugins/Geyser-*/ (any variant)
  4. config/Geyser-*/ (for Fabric/NeoForge setups)

Held-Item Poses​

Each 3D attachable gets first-person, third-person and head animations. The held poses come from the Java model's display.firstperson_righthand and display.thirdperson_righthand transforms. RSPM maps each transform through the vanilla Bedrock player rig, so the held model lands where Java draws it, with the same position, orientation and per-axis scale. A slot the model does not define uses Java's identity transform. The head animation comes from display.head.

There is nothing to tune. To change how an item sits in the hand, change the Java model's display transforms; Java and Bedrock then follow the same values. The rig uses the wide (Steve) arm, so with slim-armed skins Bedrock can differ from Java by half a pixel.

Earlier versions added hand-tuned offsets from plugins/ResourcePackManager/bedrock_display_offsets.yml. That file and its firstPerson* and thirdPerson* settings are no longer read, and RSPM regenerates cached Bedrock packs after the update. An old copy of the file can be deleted.

Held-item particles​

A producer plugin can attach particles to a held 3D item with a sidecar at assets/<namespace>/rspm_item_particles/<model path>.json, where <model path> is the held Java model's path (display/wand for freeminecraftmodels:display/wand). The sidecar maps effect names to particle identifiers the plugin ships in its entity bundle, lists locators in Java model pixels, and holds a looping particle timeline. RSPM adds the locators to the item geometry and plays the timeline as an extra attachable animation, so Bedrock clients draw the particles at the hand in first and third person. FreeMinecraftModels writes these sidecars for items with particle effects. An unreadable sidecar is skipped with a warning.

Debug Logging​

Two debug surfaces are available:

  • Backend: bedrockConverterDebug: true in config.yml turns on per-item, per-attachable, per-mapping log lines from the converter. Useful when you need to know why a specific item didn't make it into the Bedrock pack. This is separate from verboseLogging, which covers pack merging and hosting rather than the Bedrock pipeline — for a conversion problem you want bedrockConverterDebug.
  • Proxy (Velocity and BungeeCord): /rspm debug bedrock on toggles the [RSPM-BedrockDebug] log stream emitted by the proxy's GeyserBinder. Useful when Bedrock players join the proxy but don't see the pack. The setting resets to off on proxy restart so it can't accidentally be left on.

Limitations and Known Behaviour​

  • 3D inventory icons are software-rendered from the Java model's display.gui transform. The render is reasonable but not pixel-perfect; if the icon looks wrong, the most common cause is a missing/misnamed texture file referenced by the model.
  • Flipbook textures used as item icons are cropped to frame 0 — Bedrock's item_texture.json doesn't support animated icons, only animated block/terrain textures via flipbook_textures.json.
  • Attachable geometry format version is fixed at 1.21.0; update Geyser if your installation can't parse it.
  • Legacy vanilla item-model override files (anything under assets/minecraft/models/item/, including shield.json and crossbow.json) are merged across packs rather than letting one pack win outright: only the overrides arrays are combined (deduplicated by override key), while non-override fields stay higher-priority-wins. No file is singled out for special treatment.
  • If the converter cannot resolve a referenced texture or model file, that leaf is skipped and the rest of the pipeline continues. Enable bedrockConverterDebug for detailed resolution traces. A normal warning is emitted when an item ends up with no valid custom textures at all or a referenced model JSON cannot be parsed.
  • Unparseable item-definition JSON aborts conversion with Bedrock conversion failed: .... Valid JSON with a missing model or unsupported node can instead be skipped with debug logging. Inspect the error and resulting mapping count rather than treating every skipped definition as a fatal parse error.
  • Banner and redstone base items are skipped. Custom items whose base is one of the 16 dyed banners or minecraft:redstone do not get a Geyser custom-item mapping, because Geyser derives an invalid block-placer from those bases. Bedrock shows the vanilla icon for them. The console names the affected items when this happens.
  • Conversion is cooperatively cancellable. RSPM stages the ZIP and mappings, then replaces the publication as a set. A failure or cancellation during replacement attempts to restore the previous set; a conversion failure before replacement withdraws the old output. Published hashes and artifact-set metadata let consumers reject stale or mixed generations. A ZIP existing on disk is not proof that it is currently authorized or that a Bedrock client rendered it.
  • When the merged pack contains neither convertible item mappings nor allowed entity-bundle files, RSPM withdraws any previous publication and attempts to delete its outputs. An entity-only pack is still emitted with zero item mappings.

Pack Size Optimization​

Before publishing a Bedrock pack, RSPM deduplicates byte-identical texture files that use the same extension and rewrites exact JSON texture references to the retained file. It deliberately keeps aliases when a reference is ambiguous, embedded in a larger string, or found in malformed/opaque JSON, so optimization does not silently break resource references.