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, 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. On Bedrock the item shows the correct 2D sprite in inventory and in hand, exactly like Java renders it.
  • 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 × 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.

A unique Bedrock identifier is generated per (model × base item × predicate shape) mapping, so a single sword model registered against multiple base items or predicate branches doesn't collide on the Geyser side. Generated file names are short content hashes rather than readable names, because full namespace+path names routinely exceeded Geyser's 80-character pack-path limit.

Legacy pre-1.21.4 packs

Packs that still use the old assets/minecraft/models/item/*.json + overrides[].predicate.custom_model_data format are also picked up and synthesized into the modern range-dispatch form. This is best-effort: the console notes how many items used the legacy format, since they often do not render correctly on Bedrock. Migrating the source pack to the 1.21.4+ items-definition format is the real fix.

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 copies those files verbatim into the generated Bedrock pack. Only entity-related directories are accepted (entity, models/entity, animations, animation_controllers, render_controllers, materials, textures/entity) so a contributing plugin can't shadow the pack manifest or the icon atlas. Over-long paths are shortened automatically, with JSON cross-references rewritten to match, so geometry and texture references keep resolving. Two namespaces writing different bytes to the same destination is a hard error rather than a silent overwrite.

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.

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

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 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 the converter finds nothing to publish (no convertible item mappings and no allowed entity-bundle files), 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. Entity-only packs are still published.

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)

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: -60.0 # pitch (tipping toward/away from camera)
firstPersonBaseRotationY: 123.0 # yaw (spinning around vertical line)
firstPersonBaseRotationZ: 170.0 # roll (around camera-forward axis)
firstPersonBasePositionX: -8.0 # vertical on screen (positive = up)
firstPersonBasePositionY: 7.5 # depth (positive = further into the scene)
firstPersonBasePositionZ: -5.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: 6.0 # vertical (positive = raises the model)
thirdPersonBasePositionZ: -10.0 # depth relative to holder (positive = forward)

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

Change the value, run /rspm reload, and reconnect the Bedrock test client so its next join receives the rebuilt pack. Iterate until the held item 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. 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.
  • A malformed item definition is a different case and is now fatal to the cycle: unparseable JSON aborts the whole conversion with Bedrock conversion failed: ... rather than quietly producing a partial pack. A pack that used to convert "mostly" now tells you it didn't.
  • 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: a /rspm reload or a shutdown partway through aborts cleanly instead of leaving half-written output. The Geyser mappings file is only published after the pack zip succeeds, so you never get a new pack paired with stale mappings.
  • When the merged pack contains neither convertible item mappings nor allowed entity-bundle files, no Bedrock pack is emitted and any previous-run output is deleted. An entity-only pack is still valid and is emitted even when there are 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.