Skip to main content

Java-to-Bedrock Conversion

ResourcePackManager can convert the merged Java resource pack into a Bedrock resource pack so GeyserMC clients see the same custom content as Java clients. 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 / Waterfall proxy. The backend produces its Bedrock pack and exposes it on a small HTTP server so the proxy plugin can fetch it.

If none of these conditions hold the converter is pure overhead and is skipped silently. /rspm status explains exactly why when the pack hasn't 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. For each leaf model it either:

  • emits a flat Bedrock inventory icon (for plain minecraft:item/generated and similar flat-builtin parents), or
  • emits Bedrock geometry, an attachable, an animation file, and a software-rendered 2D inventory icon (for true 3D models).

A unique Bedrock identifier is generated per (model × base item) pair, so a single sword model registered against multiple base items doesn't collide on the Geyser side.

Custom armor sets are detected when a sibling assets/<namespace>/equipment/<material>.json exists. The converter wires an armor attachable that combines the vanilla armor geometry with the Java texture as the visible layer, so Bedrock players see the right armor texture when wearing the item.

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 pack name itself is used only for header.name, not for 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)

When Geyser-Spigot is detected on the same backend, RSPM registers a SessionLoadResourcePacksEvent subscriber. Every Bedrock player who joins after a fresh mix gets the latest Bedrock pack served straight from disk — no server restart required for texture or model edits to existing items.

Geyser's custom-item mappings (the JSON in custom_mappings/) are still boot-frozen, so adding new custom items or removing existing ones requires a server restart before Bedrock clients see those changes. RSPM pre-deploys the previous run's mapping file early in startup so Geyser's boot-time custom-items registration picks it up automatically.

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 + output/rspm_geyser_mappings.json like normal.
  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 via If-Modified-Since, 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. On the proxy side, conversion (well, merging) is fully automatic; there's no proxy-side config for the Bedrock pipeline.

Output Files

After a successful mix, the Bedrock files live in:

plugins/ResourcePackManager/output/ResourcePackManager_Bedrock.zip
plugins/ResourcePackManager/output/rspm_geyser_mappings.json

If auto-deploy is on and Geyser was detected on the same JVM, 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. RSPM also removes any legacy copy that older plugin versions left in that folder so Geyser doesn't end up serving the pack twice (duplicate UUIDs would confuse the Bedrock client).

When the converter finds nothing to convert (no items definitions in the merged pack), RSPM deletes any stale Bedrock outputs from a previous run rather than shipping an empty pack. The backend's /bedrock.zip route then returns 404 cleanly, which is the right signal to the proxy that this backend has no Bedrock content to contribute.

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.
# Can be absolute or relative to the plugins/ directory.
bedrockGeyserFolder: ""

# 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 (treated as absolute path first, then as a path relative to plugins/).
  2. plugins/Geyser-Spigot/
  3. plugins/Geyser-*/ (any variant)
  4. config/Geyser-*/ (for Fabric/NeoForge setups)

Tuning Held-Item Display: bedrock_display_offsets.yml

Bedrock renders the held item through a parent bone whose rest pose differs from Java's first-person and third-person transforms, so the algorithmic conversion has to apply a base offset on top of whatever the Java model's display transform specifies. The default offsets work for typical right-handed Java models, but odd cases can need tuning.

First-person and third-person are two completely separate Bedrock render passes (different parent bones, different rest poses), so each gets its own independent set of six knobs. Tuning one does not affect the other.

# ===== First-person (right hand, seen by the holder) =====
firstPersonBaseRotationX: -90.0 # pitch (tipping toward/away from camera). Default cancels parent rotation.
firstPersonBaseRotationY: 0.0 # yaw (spinning around vertical line)
firstPersonBaseRotationZ: 0.0 # roll (around camera-forward axis)
firstPersonBasePositionX: 0.0 # vertical on screen (positive = up)
firstPersonBasePositionY: 12.5 # depth (positive = further into the scene)
firstPersonBasePositionZ: 0.0 # horizontal on screen (positive = right)

# ===== Third-person (right hand, seen by other players / F5) =====
thirdPersonBaseRotationX: 90.0 # pitch as observers see it
thirdPersonBaseRotationY: 0.0 # yaw
thirdPersonBaseRotationZ: 0.0 # roll around the item's long axis
thirdPersonBasePositionX: 0.0 # horizontal across the holder's body (positive = outward)
thirdPersonBasePositionY: 12.5 # vertical (positive = raises the model)
thirdPersonBasePositionZ: 0.0 # depth relative to holder (positive = forward)

Position values are in pixels, where 1 pixel = 1/16 of a block. Rotations are in degrees.

The Bedrock client live-reloads attachable JSON without a relaunch. If a Bedrock test client is logged in, change the value, run /rspm reload, and the next Bedrock-join (or in some cases the same connected player after a small in-world action that re-binds the attachable) shows the new offset. Iterate until it looks right.

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.
  • 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 a [BedrockConverter] warning is logged (or a debug line if bedrockConverterDebug is on); the rest of the pipeline continues.
  • When the merged pack contains zero convertible items, no Bedrock pack is emitted at all and any previous-run output is deleted. RSPM does not ship a useless ~22 KB empty pack with a prompt.