diff --git a/AGENTS.md b/AGENTS.md index f80e3095..89d542d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,8 @@ This repository stores patch files and the tools that apply them. The generated - Keep changes separate when they are independently reviewable, have different upstream owners, or need different prerequisites. - After folding a patch into an earlier one, remove its patch file and `series.json` entry. Replay and test the full stack. - Keep each upstreamable patch's purpose in its commit body. Before creating or updating its upstream PR, add a non-empty `.pr.md` file beside the patch with the same base filename for PR-specific context and testing. -- Keep tests, fixtures, test dependencies, and test-only build configuration in the local patch stack. Exclude them from every upstream PR. +- For CubeConverter, include the owning patch's regression tests and required API migrations in upstream PRs. Publish the full feature commit instead of its production-only projection. Keep setup excluded. +- For other projects, keep tests, fixtures, test dependencies, and test-only build configuration in the local patch stack. Exclude them from upstream PRs. - Use the filtered PR checkout for upstream builds and review. Run local regression tests through the full stack separately. - Mark prerequisites that supply only test configuration with `testOnly: true`. They must not block a production-only PR. - After upstream merges a feature, remove its local tests when retiring the owning patch. Do not submit a separate test PR. diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 2b6d4b56..2a3a71eb 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -25,7 +25,7 @@ No complete area has verified Bedrock parity. | Protocol 2225 | Updated packet layouts, explicit enum values, stonecutter notifications, cursor drags, skin identity, and sound unions. | Block seating, signed content playback, matchmaking UI, and environment layers remain partial or unsupported. | | World resources | World base-game version selects vanilla metadata, textures, actor graphs, and audio before server overrides. The context survives ViaProxy. Server legacy biome bindings retain unqualified names across versions. | Native current/older/current cat, sound, and remote biome-name controls cover selected paths. Broader pose and audio behavior need comparisons. | | Held block placement | Retained stationary build paths, next-cell ray selection, independent fresh block/air admission, frame retries after rejection, use timing, trapdoor orientation, cancellation, and prediction. | Native, direct, and proxy controls cover selected stationary paths, first rejection, last-stack placement, activation, re-aiming, and release. Moving placement, air/entity dispatch, build-height limits, and broader input scheduling remain open. | -| Fonts and UI | Bitmap metadata, extended mapping, same-pack ownership, legacy layouts, authored scenes, and native GUI sampling. | At 1280 × 720, current font fixtures match all 16 labels at native UI scales 1 and 2; legacy scale-2 fixtures match 16/16. Both routes pass Java GUI scale changes and reloads. [Complete authored-screen parity](../patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md), broader viewports, and TrueType comparisons remain open. | +| Fonts and UI | Native bitmap metrics and italic geometry, authored scenes, TrueType cache and atlas sampling, and primary font aliases. | Selected bitmap and TrueType controls match at 1280 × 720. [Validation and limits](../patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md). Complete screens, bold and shadowed text, other font families, mixed or scaled aliases, and broader viewports remain open. | | Player inventory and crafting | Opening ownership, recipe catalogs, Creative catalog actions, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | | Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Short input edges during active Java use, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md). | | Cooking | Snapshot-bound recipe actions, transfer batches, preferences, search, and progress. | Default cooking screens retain ordinary controls until authored bindings are complete. See the [cooking reference](native-cooking-actions.md). | diff --git a/docs/bedrock-development-sources.md b/docs/bedrock-development-sources.md index 391edb3f..178f908d 100644 --- a/docs/bedrock-development-sources.md +++ b/docs/bedrock-development-sources.md @@ -4,7 +4,7 @@ Use this reference when changing ViaBedrock, the ViaFabricPlus Bedrock add-on, o ## Choose the protocol version first -The current StackAnvil ViaBedrock checkout targets Bedrock **1.26.60**, network protocol **2225**. The [first upstreamable patch](../patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.patch) contains the update. Check `BEDROCK_VERSION_NAME` and `BEDROCK_PROTOCOL_VERSION` in `.worktrees/viabedrock` after syncing the stack. +The current StackAnvil ViaBedrock checkout targets Bedrock **1.26.60**, network protocol **2225**. The [protocol compatibility patch](../patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.patch) contains the update. Check `BEDROCK_VERSION_NAME` and `BEDROCK_PROTOCOL_VERSION` in `.worktrees/viabedrock` after syncing the stack. [Mojang's 1.26.60 release metadata](https://github.com/Mojang/bedrock-protocol-docs/releases/tag/v1.26.60) identifies protocol 2225. Its schema version label is `1.26.60-beta.0`. Use the network protocol number to match evidence. A preview for another protocol provides leads but requires wire checks. diff --git a/patches/cubeconverter/series.json b/patches/cubeconverter/series.json index 0cc75c50..c045fba9 100644 --- a/patches/cubeconverter/series.json +++ b/patches/cubeconverter/series.json @@ -2,19 +2,6 @@ "setup": [ "0001-stackanvil-branding.patch" ], - "upstreamable": [ - { - "file": "0001-preserve-invisible-bedrock-cubes.patch", - "title": "fix(models): preserve optional geometry declarations" - }, - { - "file": "0002-preserve-actor-lighting-controls.patch", - "title": "feat(controllers): preserve actor rendering controls" - }, - { - "file": "0003-preserve-authored-face-uv-rotations.patch", - "title": "fix(models): preserve authored face UV rotations" - } - ], + "upstreamable": [], "deferred": [] } diff --git a/patches/cubeconverter/upstreamable/0001-preserve-invisible-bedrock-cubes.patch b/patches/cubeconverter/upstreamable/0001-preserve-invisible-bedrock-cubes.patch deleted file mode 100644 index 87caa5d9..00000000 --- a/patches/cubeconverter/upstreamable/0001-preserve-invisible-bedrock-cubes.patch +++ /dev/null @@ -1,140 +0,0 @@ -From 9ce4b774f6d06c458e4c3c537518dbd45b7bea24 Mon Sep 17 00:00:00 2001 -From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> -Date: Thu, 1 Oct 2026 15:54:14 +0200 -Subject: [PATCH] fix(models): preserve optional geometry declarations - -Retain modern and legacy geometry declarations that omit texture dimensions -or the bones array. Use the native 64 by 64 texture defaults independently -for each absent or null dimension, and keep a missing bones array as an -empty model without mutating the source JSON. - -Upstream PR #2 now owns invisible-cube export, missing UV defaults, -locator-only fitting and source-preserving UV normalization. Keep those -changes and their tests in the upstream base; this remaining patch changes -only the optional-field parser and its targeted regression. -https://github.com/oryxel1/CubeConverter/pull/2 - -The matching Bedrock 1.26.51.1/protocol 2193 compiler caller supplies width -and height 64. Twelve original bounded texture-dimension controls cover -absent/null fields and independent overrides with two caller defaults. -That reference does not establish schema dispatch, inherited/cached model -dimensions, complete bone loading or native rendering parity. ---- - .../geometry/BedrockGeometryParser.java | 23 +++++------ - .../converter/FormatConverterTest.java | 40 +++++++++++++++++++ - 2 files changed, 50 insertions(+), 13 deletions(-) - -diff --git a/src/main/java/org/cube/converter/parser/bedrock/geometry/BedrockGeometryParser.java b/src/main/java/org/cube/converter/parser/bedrock/geometry/BedrockGeometryParser.java -index b692f94..46ca26f 100644 ---- a/src/main/java/org/cube/converter/parser/bedrock/geometry/BedrockGeometryParser.java -+++ b/src/main/java/org/cube/converter/parser/bedrock/geometry/BedrockGeometryParser.java -@@ -16,6 +16,8 @@ import java.util.List; - - // I am NOT cleaning this up lmao... - public class BedrockGeometryParser { -+ private static final int DEFAULT_TEXTURE_SIZE = 64; -+ - public static List parse(String json) { - return parse(GsonUtil.getGson().fromJson(json.trim(), JsonObject.class)); - } -@@ -50,7 +52,7 @@ public class BedrockGeometryParser { - continue; - } - JsonObject object = element.getAsJsonObject(); -- if (!object.has("texturewidth")) { -+ if (!elementName.startsWith("geometry.")) { - continue; - } - final BedrockGeometryModel geometry = getGeometry(object, elementName, "texturewidth", "textureheight"); -@@ -67,19 +69,14 @@ public class BedrockGeometryParser { - JsonObject description = geometry.has("description") ? geometry.getAsJsonObject("description") : geometry; - String identifier = description.has("identifier") ? description.get("identifier").getAsString() : elementName; - -- if (!description.has(textureHeightName) || !description.has(textureWidthName)) { -- return null; -- } -- -- int textureWidth = description.get(textureWidthName).getAsInt(); -- int textureHeight = description.get(textureHeightName).getAsInt(); -- -- if (!geometry.has("bones")) { -- return null; -- } -+ final int textureWidth = description.has(textureWidthName) && !description.get(textureWidthName).isJsonNull() -+ ? description.get(textureWidthName).getAsInt() : DEFAULT_TEXTURE_SIZE; -+ final int textureHeight = description.has(textureHeightName) && !description.get(textureHeightName).isJsonNull() -+ ? description.get(textureHeightName).getAsInt() : DEFAULT_TEXTURE_SIZE; -+ final JsonArray boneElements = geometry.has("bones") ? geometry.getAsJsonArray("bones") : new JsonArray(); - - final List bones = new ArrayList<>(); -- for (JsonElement boneElement : geometry.getAsJsonArray("bones")) { -+ for (JsonElement boneElement : boneElements) { - final JsonObject boneObject = boneElement.getAsJsonObject(); - final String name = boneObject.get("name").getAsString(); - -@@ -144,4 +141,4 @@ public class BedrockGeometryParser { - geometryModel.getParents().addAll(bones); - return geometryModel; - } --} -\ No newline at end of file -+} -diff --git a/src/test/java/org/cube/converter/converter/FormatConverterTest.java b/src/test/java/org/cube/converter/converter/FormatConverterTest.java -index 3257dca..a890ea9 100644 ---- a/src/test/java/org/cube/converter/converter/FormatConverterTest.java -+++ b/src/test/java/org/cube/converter/converter/FormatConverterTest.java -@@ -1,5 +1,7 @@ - package org.cube.converter.converter; - -+import com.viaversion.viaversion.libs.gson.JsonArray; -+import com.viaversion.viaversion.libs.gson.JsonNull; - import com.viaversion.viaversion.libs.gson.JsonObject; - import org.cube.converter.converter.enums.RotationType; - import org.cube.converter.model.impl.java.JavaItemModel; -@@ -11,6 +13,44 @@ import static org.junit.jupiter.api.Assertions.*; - - class FormatConverterTest { - -+ @Test -+ void preservesOptionalGeometryFieldsAndNativeTextureDefaults() { -+ for (final boolean modern : new boolean[]{false, true}) { -+ for (final boolean bones : new boolean[]{false, true}) { -+ final JsonObject document = modern ? geometryJson("[]") -+ : GsonUtil.getGson().fromJson("{\"geometry.example\":{}}", JsonObject.class); -+ final JsonObject geometry = modern ? document.getAsJsonArray("minecraft:geometry").get(0).getAsJsonObject() -+ : document.getAsJsonObject("geometry.example"); -+ final JsonObject description = modern ? geometry.getAsJsonObject("description") : geometry; -+ final String width = modern ? "texture_width" : "texturewidth"; -+ final String height = modern ? "texture_height" : "textureheight"; -+ description.remove(width); -+ description.remove(height); -+ if (bones) geometry.add("bones", new JsonArray()); -+ else geometry.remove("bones"); -+ for (int overrides = 0; overrides < 4; overrides++) { -+ if ((overrides & 1) != 0) description.addProperty(width, 32); -+ else description.add(width, JsonNull.INSTANCE); -+ if ((overrides & 2) != 0) description.addProperty(height, 16); -+ else description.remove(height); -+ final JsonObject original = document.deepCopy(); -+ final var models = BedrockGeometryParser.parse(document); -+ assertEquals(1, models.size()); -+ final var parsed = models.get(0); -+ assertEquals((overrides & 1) != 0 ? 32 : 64, parsed.getTextureSize().getX()); -+ assertEquals((overrides & 2) != 0 ? 16 : 64, parsed.getTextureSize().getY()); -+ assertTrue(parsed.getParents().isEmpty()); -+ for (final RotationType rotation : RotationType.values()) { -+ final var model = parsed.toJavaItemModel("example:item/actor", rotation); -+ assertEquals(1, model.getScale()); -+ assertTrue(finiteExport(model).getAsJsonArray("elements").isEmpty()); -+ } -+ assertEquals(original, document); -+ } -+ } -+ } -+ } -+ - @Test - void exportsGeometryWithoutBonesAtUnitScaleForEveryRotationMode() { - final var geometry = BedrockGeometryParser.parse(geometryJson("[]")).get(0); --- -2.56.0 diff --git a/patches/cubeconverter/upstreamable/0001-preserve-invisible-bedrock-cubes.pr.md b/patches/cubeconverter/upstreamable/0001-preserve-invisible-bedrock-cubes.pr.md deleted file mode 100644 index 505875b2..00000000 --- a/patches/cubeconverter/upstreamable/0001-preserve-invisible-bedrock-cubes.pr.md +++ /dev/null @@ -1,22 +0,0 @@ -Keep valid modern and legacy geometry declarations when texture dimensions or the bones array are omitted. -Dimensions default to 64 by 64, and absent bones produce an empty model. -Recognize legacy declarations by their geometry key, since texture dimensions are optional there too. -Preserve explicit dimensions and source JSON. - -[Upstream PR #2](https://github.com/oryxel1/CubeConverter/pull/2) now supplies invisible-cube export, missing UV defaults, locator-only fitting and source-preserving UV normalization. -Those changes and their tests remain in the pinned upstream base. -The remaining patch changes only optional-field parsing and its regression test. -Its filename remains stable within the series. - -The matching Bedrock 1.26.51.1/protocol 2193 compiler caller `141c11ba0` supplies width and height 64 to `141c13a30`. -The existing private instruction probe executes the texture-dimension reader at `141c13bfd..141c13c6b`. -It verifies twelve cases with absent/null dimensions, independent numeric overrides and two caller defaults. -The probe executes original native JSON lookup and integer conversion helpers. -Its boundaries provide CRT byte/string operations, allocation, frees and a diagnostic backend. -It excludes schema dispatch, inherited/cached dimensions, complete bone loading, registration and rendering. -The executable SHA-256 is `537c0aee2e79afbdc94b44b28e00f466ae62bc50e2733d953b430db9dbaa9ee7`. - -The parser regression covers modern and legacy declarations, missing and empty bones, defaults, independent dimension overrides and source preservation. -Each result exports with finite metadata through every rotation mode. -Earlier capture and replay evidence remains historical evidence for the same optional-field behavior. -It does not establish complete native image parity. diff --git a/patches/cubeconverter/upstreamable/0002-preserve-actor-lighting-controls.patch b/patches/cubeconverter/upstreamable/0002-preserve-actor-lighting-controls.patch deleted file mode 100644 index e212e8af..00000000 --- a/patches/cubeconverter/upstreamable/0002-preserve-actor-lighting-controls.patch +++ /dev/null @@ -1,584 +0,0 @@ -From b5f4326d767b6cb029f25e5ad4b6844e79fd0265 Mon Sep 17 00:00:00 2001 -From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> -Date: Fri, 2 Oct 2026 02:20:53 +0200 -Subject: [PATCH] feat(controllers): preserve actor rendering controls - -Retain ignore_lighting and the light_color_multiplier expression from Bedrock render controllers so renderers can reproduce their selected lighting. Preserve material override order as it determines which wildcard or bone-specific expression wins. Defaults keep controllers without those fields lit at their original strength. - -Retain immutable render-controller UV offset and scale expressions for -camera-dependent client evaluation. Preserve valid neighboring components -when a UV field is malformed, using native identity defaults for missing -components. Numeric and MoLang expressions remain distinct from the -protocol-side lighting calculation. - -Bedrock 1.26.51.1 (protocol 2193) controller parser 141cabf60 initializes -offset components at controller+200/+210 to zero and scale components at -+220/+230 to one. Installed ENTITY vertex DXIL applies normalized UV times -UVAnimation.zw plus UVAnimation.xy. Its material uniform default is -(0,0,1,1). The licensed executable SHA-256 is -537c0aee2e79afbdc94b44b28e00f466ae62bc50e2733d953b430db9dbaa9ee7. -Saved CubeCraft controllers require camera and lifetime expressions; -carry those expressions without assuming that a protocol worker knows -the rendering camera. Native assets and raw exports remain private. - -Preserve legacy client entity and attachable animation-controller aliases -as immutable ordered declarations, independently of modern script entries. -Keep existing constructors source-compatible and default absent declarations -to an empty list. Parsing does not select a state or impose renderer policy. -Tests verify declaration order, independent scripts, malformed neighbors -and absent declarations. - -Retain legacy description.animation_controllers declarations in order. -The active saved protocol 2193 Hive client entity uses format 1.8.0 with -face/center animation aliases and a declared single-default-state -controller, without scripts.animate. Dropping the declaration prevented -its supported rotation_to_camera bone animation from reaching the draw. -The final saved pack stack has exactly one definition for this identifier. -Its entity entry SHA-256 is -58cf3b66a2ebe76751415ae7f5f9ad81b44464e5ec987f4f22d6e49639cc1f80; -the containing pack SHA-256 is -13facb13e8f0aa5ac233253e5deeca0e8c294512ca5f02bb7c38a8fd5da5d028. -Native Bedrock 1.26.51.1 protocol 2193 renders its labels facing the camera. -Raw assets and captures stay private; generic tests reproduce the schema. - -Preserve ordered render-controller part_visibility rules, including -wildcards and repeated patterns, and attachable parent_setup expressions -in their separate owner scope. Native equipped models depend on these -controls for invisibility, first-person visibility and owner variables. -Keep existing constructors while exposing immutable new declarations. -Semantic parser regressions cover visibility precedence, scalar and array -setup, missing controls and defensive ownership. - -Retain every material assignment as an immutable ordered declaration, -including repeated bone patterns. A map overwrote earlier expressions and -left a repeated wildcard at its first position, so later consumers could -both skip Molang effects and select a superseded bone override. The target -Bedrock controller contract applies declarations in order, with later -matches replacing earlier ones. Keep parsing separate from evaluation. - -BREAKING CHANGE: Replace BedrockRenderController.materialsMap with ordered materialAssignments and update constructor callers to pass MaterialAssignment lists. ---- - .../data/bedrock/BedrockAttachableData.java | 7 ++ - .../data/bedrock/BedrockEntityData.java | 59 +++++++++- - .../controller/BedrockRenderController.java | 30 ++++- - .../controller/BedrockControllerParser.java | 58 ++++++++-- - .../bedrock/data/BedrockDataParser.java | 5 +- - .../BedrockAttachableRenderingTest.java | 108 ++++++++++++++++++ - .../BedrockControllerLightingTest.java | 77 +++++++++++++ - .../BedrockLegacyAnimationControllerTest.java | 38 ++++++ - 8 files changed, 368 insertions(+), 14 deletions(-) - create mode 100644 src/test/java/org/cube/converter/parser/bedrock/controller/BedrockAttachableRenderingTest.java - create mode 100644 src/test/java/org/cube/converter/parser/bedrock/controller/BedrockControllerLightingTest.java - create mode 100644 src/test/java/org/cube/converter/parser/bedrock/controller/BedrockLegacyAnimationControllerTest.java - -diff --git a/src/main/java/org/cube/converter/data/bedrock/BedrockAttachableData.java b/src/main/java/org/cube/converter/data/bedrock/BedrockAttachableData.java -index d66f15d..57afc24 100644 ---- a/src/main/java/org/cube/converter/data/bedrock/BedrockAttachableData.java -+++ b/src/main/java/org/cube/converter/data/bedrock/BedrockAttachableData.java -@@ -7,4 +7,11 @@ public class BedrockAttachableData extends BedrockEntityData { - public BedrockAttachableData(String identifier, Scripts scripts, List controllers, Map materials, Map animations, Map textures, Map geometries) { - super(identifier, scripts, controllers, materials, animations, textures, geometries); - } -+ -+ public BedrockAttachableData(String identifier, Scripts scripts, List controllers, -+ Map materials, Map animations, -+ Map textures, Map geometries, -+ List animationControllers) { -+ super(identifier, scripts, controllers, materials, animations, textures, geometries, animationControllers); -+ } - } -diff --git a/src/main/java/org/cube/converter/data/bedrock/BedrockEntityData.java b/src/main/java/org/cube/converter/data/bedrock/BedrockEntityData.java -index 21f1685..c7e1a5f 100644 ---- a/src/main/java/org/cube/converter/data/bedrock/BedrockEntityData.java -+++ b/src/main/java/org/cube/converter/data/bedrock/BedrockEntityData.java -@@ -24,6 +24,33 @@ public class BedrockEntityData { - private final Map animations; - private final Map textures; - private final Map geometries; -+ private final List animationControllers; -+ -+ public BedrockEntityData(String identifier, Scripts scripts, List controllers, -+ Map materials, Map animations, -+ Map textures, Map geometries) { -+ this(identifier, scripts, controllers, materials, animations, textures, geometries, List.of()); -+ } -+ -+ public record AnimationController(String alias, String identifier) { -+ public static List parse(final JsonArray array) { -+ if (array == null) { -+ return List.of(); -+ } -+ final List controllers = new ArrayList<>(); -+ for (final JsonElement element : array) { -+ if (!element.isJsonObject()) { -+ continue; -+ } -+ for (final var entry : element.getAsJsonObject().entrySet()) { -+ if (entry.getValue().isJsonPrimitive() && entry.getValue().getAsJsonPrimitive().isString()) { -+ controllers.add(new AnimationController(entry.getKey(), entry.getValue().getAsString())); -+ } -+ } -+ } -+ return List.copyOf(controllers); -+ } -+ } - - public record RenderController(String identifier, String condition) { - public static List parse(final JsonArray array) { -@@ -51,7 +78,16 @@ public class BedrockEntityData { - } - } - -- public record Scripts(List initialize, List pre_animation, Scale scale, List animates) { -+ public record Scripts(List initialize, List pre_animation, Scale scale, List animates, -+ List parentSetup) { -+ public Scripts { -+ parentSetup = List.copyOf(parentSetup); -+ } -+ -+ public Scripts(List initialize, List pre_animation, Scale scale, List animates) { -+ this(initialize, pre_animation, scale, animates, List.of()); -+ } -+ - public record Scale(String scale, String scaleX, String scaleY, String scaleZ) { - } - -@@ -86,7 +122,26 @@ public class BedrockEntityData { - final Scale scale = new Scale(getOrDefault(object.get("scale"), "1"), getOrDefault(object.get("scaleX"), "1"), getOrDefault(object.get("scaleY"), "1"), getOrDefault(object.get("scaleZ"), "1")); - final List animates = Animate.parse(object.getAsJsonArray("animate")); - -- return new Scripts(initialize, pre_animation, scale, animates); -+ return new Scripts(initialize, pre_animation, scale, animates, parentSetup(object.get("parent_setup"))); -+ } -+ -+ private static List parentSetup(final JsonElement element) { -+ if (element == null) { -+ return List.of(); -+ } -+ if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { -+ return List.of(element.getAsString()); -+ } -+ if (!element.isJsonArray()) { -+ return List.of(); -+ } -+ final List expressions = new ArrayList<>(); -+ for (final JsonElement expression : element.getAsJsonArray()) { -+ if (expression.isJsonPrimitive() && expression.getAsJsonPrimitive().isString()) { -+ expressions.add(expression.getAsString()); -+ } -+ } -+ return List.copyOf(expressions); - } - - public static Scripts emptyScript() { -diff --git a/src/main/java/org/cube/converter/data/bedrock/controller/BedrockRenderController.java b/src/main/java/org/cube/converter/data/bedrock/controller/BedrockRenderController.java -index e76cc41..4ac4336 100644 ---- a/src/main/java/org/cube/converter/data/bedrock/controller/BedrockRenderController.java -+++ b/src/main/java/org/cube/converter/data/bedrock/controller/BedrockRenderController.java -@@ -6,14 +6,38 @@ import com.viaversion.viaversion.libs.gson.JsonObject; - - import java.util.ArrayList; - import java.util.List; --import java.util.Map; - - import static org.cube.converter.util.GsonUtil.*; - - // TODO: hurt color, overlay color, etc.... --public record BedrockRenderController(String identifier, Map materialsMap, -+public record BedrockRenderController(String identifier, List materialAssignments, - String geometryExpression, List textureExpressions, -- List materials, List textures, List geometries) { -+ List materials, List textures, List geometries, -+ boolean ignoreLighting, String lightColorMultiplierExpression, UvAnimation uvAnimation, -+ List partVisibility) { -+ public BedrockRenderController { -+ materialAssignments = List.copyOf(materialAssignments); -+ partVisibility = List.copyOf(partVisibility); -+ } -+ -+ public BedrockRenderController(String identifier, List materialAssignments, -+ String geometryExpression, List textureExpressions, -+ List materials, List textures, List geometries, -+ boolean ignoreLighting, String lightColorMultiplierExpression, UvAnimation uvAnimation) { -+ this(identifier, materialAssignments, geometryExpression, textureExpressions, materials, textures, geometries, -+ ignoreLighting, lightColorMultiplierExpression, uvAnimation, List.of()); -+ } -+ -+ public record MaterialAssignment(String pattern, String expression) { -+ } -+ -+ public record BoneVisibility(String pattern, String expression) { -+ } -+ -+ public record UvAnimation(String offsetU, String offsetV, String scaleU, String scaleV) { -+ public static final UvAnimation IDENTITY = new UvAnimation("0", "0", "1", "1"); -+ } -+ - public record Array(String name, List values) { - public static List parse(final JsonObject object) { - if (object == null) { -diff --git a/src/main/java/org/cube/converter/parser/bedrock/controller/BedrockControllerParser.java b/src/main/java/org/cube/converter/parser/bedrock/controller/BedrockControllerParser.java -index 6949359..de35231 100644 ---- a/src/main/java/org/cube/converter/parser/bedrock/controller/BedrockControllerParser.java -+++ b/src/main/java/org/cube/converter/parser/bedrock/controller/BedrockControllerParser.java -@@ -7,11 +7,8 @@ import org.cube.converter.data.bedrock.controller.BedrockRenderController; - import org.cube.converter.util.GsonUtil; - - import java.util.ArrayList; --import java.util.HashMap; - import java.util.List; --import java.util.Map; - --import static org.cube.converter.util.GsonUtil.objectToMap; - - public class BedrockControllerParser { - public static List parse(final String json) { -@@ -45,19 +42,25 @@ public class BedrockControllerParser { - } - } - -- final Map materialsMap = new HashMap<>(); -+ final List materialAssignments = new ArrayList<>(); - if (object.has("materials")) { - for (final JsonElement element : object.getAsJsonArray("materials")) { - if (!element.isJsonObject()) { - continue; - } -- materialsMap.putAll(objectToMap(element.getAsJsonObject())); -+ for (final var entry : element.getAsJsonObject().entrySet()) { -+ materialAssignments.add(new BedrockRenderController.MaterialAssignment(entry.getKey(), entry.getValue().getAsString())); -+ } - } - } - -+ final boolean ignoreLighting = object.has("ignore_lighting") && object.get("ignore_lighting").getAsBoolean(); -+ final String lightColorMultiplier = object.has("light_color_multiplier") ? object.get("light_color_multiplier").getAsString() : "1.0"; -+ final BedrockRenderController.UvAnimation uvAnimation = uvAnimation(object.get("uv_anim")); -+ final List partVisibility = partVisibility(object.get("part_visibility")); - final JsonObject arrays = object.getAsJsonObject("arrays"); - if (arrays == null) { -- list.add(new BedrockRenderController(identifier, materialsMap, geometryExpression, textureExpressions, List.of(), List.of(), List.of())); -+ list.add(new BedrockRenderController(identifier, materialAssignments, geometryExpression, textureExpressions, List.of(), List.of(), List.of(), ignoreLighting, lightColorMultiplier, uvAnimation, partVisibility)); - continue; - } - -@@ -65,12 +68,53 @@ public class BedrockControllerParser { - final List geometries = BedrockRenderController.Array.parse(arrays.getAsJsonObject("geometries")); - final List materials = BedrockRenderController.Array.parse(arrays.getAsJsonObject("materials")); - -- list.add(new BedrockRenderController(identifier, materialsMap, geometryExpression, textureExpressions, materials, textures, geometries)); -+ list.add(new BedrockRenderController(identifier, materialAssignments, geometryExpression, textureExpressions, materials, textures, geometries, ignoreLighting, lightColorMultiplier, uvAnimation, partVisibility)); - } - - return list; - } - -+ /** Parses ordered visibility declarations without selecting a renderer or variable scope. */ -+ public static List partVisibility(final JsonElement element) { -+ if (element == null || !element.isJsonArray()) { -+ return List.of(); -+ } -+ final List rules = new ArrayList<>(); -+ for (final JsonElement rule : element.getAsJsonArray()) { -+ if (!rule.isJsonObject()) { -+ continue; -+ } -+ for (final var entry : rule.getAsJsonObject().entrySet()) { -+ if (entry.getValue().isJsonPrimitive()) { -+ rules.add(new BedrockRenderController.BoneVisibility(entry.getKey(), entry.getValue().getAsString())); -+ } -+ } -+ } -+ return List.copyOf(rules); -+ } -+ -+ private static BedrockRenderController.UvAnimation uvAnimation(final JsonElement element) { -+ if (element == null || !element.isJsonObject()) { -+ return BedrockRenderController.UvAnimation.IDENTITY; -+ } -+ final JsonObject fields = element.getAsJsonObject(); -+ return new BedrockRenderController.UvAnimation( -+ uvComponent(fields.get("offset"), 0, "0"), uvComponent(fields.get("offset"), 1, "0"), -+ uvComponent(fields.get("scale"), 0, "1"), uvComponent(fields.get("scale"), 1, "1")); -+ } -+ -+ private static String uvComponent(final JsonElement element, final int axis, final String fallback) { -+ if (element == null || !element.isJsonArray() || element.getAsJsonArray().size() != 2) { -+ return fallback; -+ } -+ final JsonElement component = element.getAsJsonArray().get(axis); -+ if (!component.isJsonPrimitive()) { -+ return fallback; -+ } -+ final var value = component.getAsJsonPrimitive(); -+ return value.isString() || value.isNumber() ? value.getAsString() : fallback; -+ } -+ - private static List arrayToList(JsonArray array) { - List list = new ArrayList<>(); - -diff --git a/src/main/java/org/cube/converter/parser/bedrock/data/BedrockDataParser.java b/src/main/java/org/cube/converter/parser/bedrock/data/BedrockDataParser.java -index c782b38..06e925f 100644 ---- a/src/main/java/org/cube/converter/parser/bedrock/data/BedrockDataParser.java -+++ b/src/main/java/org/cube/converter/parser/bedrock/data/BedrockDataParser.java -@@ -50,6 +50,7 @@ public class BedrockDataParser { - final Map geometries = objectToMap(description.getAsJsonObject("geometry")); - final Map animations = objectToMap(description.getAsJsonObject("animations")); - final List controllers = BedrockEntityData.RenderController.parse(description.getAsJsonArray("render_controllers")); -+ final List animationControllers = BedrockEntityData.AnimationController.parse(description.getAsJsonArray("animation_controllers")); - final BedrockEntityData.Scripts scripts; - if (description.has("scripts")) { - scripts = BedrockEntityData.Scripts.parse(description.getAsJsonObject("scripts")); -@@ -57,7 +58,7 @@ public class BedrockDataParser { - scripts = BedrockEntityData.Scripts.emptyScript(); - } - -- return attachable ? new BedrockAttachableData(identifier, scripts, controllers, materials, animations, textures, geometries) : -- new BedrockEntityData(identifier, scripts, controllers, materials, animations, textures, geometries); -+ return attachable ? new BedrockAttachableData(identifier, scripts, controllers, materials, animations, textures, geometries, animationControllers) : -+ new BedrockEntityData(identifier, scripts, controllers, materials, animations, textures, geometries, animationControllers); - } - } -diff --git a/src/test/java/org/cube/converter/parser/bedrock/controller/BedrockAttachableRenderingTest.java b/src/test/java/org/cube/converter/parser/bedrock/controller/BedrockAttachableRenderingTest.java -new file mode 100644 -index 0000000..b8f540e ---- /dev/null -+++ b/src/test/java/org/cube/converter/parser/bedrock/controller/BedrockAttachableRenderingTest.java -@@ -0,0 +1,108 @@ -+package org.cube.converter.parser.bedrock.controller; -+ -+import org.cube.converter.data.bedrock.BedrockEntityData; -+import org.cube.converter.data.bedrock.controller.BedrockRenderController; -+import org.cube.converter.parser.bedrock.data.impl.BedrockAttachableParser; -+import org.junit.jupiter.api.Test; -+ -+import java.util.ArrayList; -+import java.util.List; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+import static org.junit.jupiter.api.Assertions.assertThrows; -+import static org.junit.jupiter.api.Assertions.assertTrue; -+ -+class BedrockAttachableRenderingTest { -+ @Test -+ void preservesVisibilityOverrideOrderIncludingRepeatedPatterns() { -+ final var controller = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.worn":{ -+ "part_visibility":[{"*":false},{"Wing*":"query.is_alive","body":true}, -+ null,{}, {"invalid":{},"Wing*":0},{"*":"!query.is_invisible"}], -+ "arrays":{"textures":{"Array.texture":["Texture.default"]}} -+ }}} -+ """).get(0); -+ -+ assertEquals(List.of( -+ new BedrockRenderController.BoneVisibility("*", "false"), -+ new BedrockRenderController.BoneVisibility("Wing*", "query.is_alive"), -+ new BedrockRenderController.BoneVisibility("body", "true"), -+ new BedrockRenderController.BoneVisibility("Wing*", "0"), -+ new BedrockRenderController.BoneVisibility("*", "!query.is_invisible") -+ ), controller.partVisibility()); -+ assertEquals(1, controller.textures().size()); -+ assertThrows(UnsupportedOperationException.class, () -> controller.partVisibility().clear()); -+ } -+ -+ @Test -+ void retainsVisibilityWithoutArraysAndDefaultsMalformedContainers() { -+ final var visible = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.worn":{"part_visibility":[{"*":1}]}}} -+ """).get(0); -+ assertEquals(1, visible.partVisibility().size()); -+ final var absent = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.worn":{}}} -+ """).get(0); -+ final var malformed = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.worn":{"part_visibility":{"*":true}}}} -+ """).get(0); -+ assertTrue(absent.partVisibility().isEmpty()); -+ assertTrue(malformed.partVisibility().isEmpty()); -+ } -+ -+ @Test -+ void retainsScalarParentSetupSeparatelyFromOwnerAndAnimationScripts() { -+ final var attachable = BedrockAttachableParser.parse(""" -+ {"minecraft:attachable":{"description":{"identifier":"test:worn", -+ "scripts":{"parent_setup":"variable.hide_cape = 1;", -+ "initialize":["variable.phase = 0;"], -+ "pre_animation":["variable.phase = query.life_time;"],"animate":["idle"]} -+ }}} -+ """); -+ assertEquals(List.of("variable.hide_cape = 1;"), attachable.getScripts().parentSetup()); -+ assertEquals(1, attachable.getScripts().initialize().size()); -+ assertEquals(1, attachable.getScripts().pre_animation().size()); -+ assertEquals(1, attachable.getScripts().animates().size()); -+ } -+ -+ @Test -+ void retainsValidParentSetupNeighborsInOrderAndDefaultsAbsentScripts() { -+ final var attachable = BedrockAttachableParser.parse(""" -+ {"minecraft:attachable":{"description":{"identifier":"test:worn", -+ "scripts":{"parent_setup":["variable.first = 1;",null,4,{}, -+ "variable.second = variable.first;"]} -+ }}} -+ """); -+ assertEquals(List.of("variable.first = 1;", "variable.second = variable.first;"), -+ attachable.getScripts().parentSetup()); -+ assertThrows(UnsupportedOperationException.class, () -> attachable.getScripts().parentSetup().clear()); -+ final var absent = BedrockAttachableParser.parse(""" -+ {"minecraft:attachable":{"description":{"identifier":"test:worn"}}} -+ """); -+ final var malformed = BedrockAttachableParser.parse(""" -+ {"minecraft:attachable":{"description":{"identifier":"test:worn", "scripts":{"parent_setup":false}}}} -+ """); -+ assertTrue(absent.getScripts().parentSetup().isEmpty()); -+ assertTrue(malformed.getScripts().parentSetup().isEmpty()); -+ } -+ -+ @Test -+ void recordsDefensivelyCopyNewDeclarationsAndKeepExistingConstructors() { -+ final var visibility = new ArrayList<>(List.of(new BedrockRenderController.BoneVisibility("*", "1"))); -+ final var controller = new BedrockRenderController("controller.render.worn", List.of(), "", List.of(), -+ List.of(), List.of(), List.of(), false, "1", BedrockRenderController.UvAnimation.IDENTITY, visibility); -+ visibility.clear(); -+ assertEquals(1, controller.partVisibility().size()); -+ -+ final var parentSetup = new ArrayList<>(List.of("variable.worn = 1;")); -+ final var scale = new BedrockEntityData.Scripts.Scale("1", "1", "1", "1"); -+ final var scripts = new BedrockEntityData.Scripts(List.of(), List.of(), scale, List.of(), parentSetup); -+ parentSetup.clear(); -+ assertEquals(1, scripts.parentSetup().size()); -+ -+ assertTrue(new BedrockEntityData.Scripts(List.of(), List.of(), scale, List.of()).parentSetup().isEmpty()); -+ assertTrue(new BedrockRenderController("controller.render.worn", List.of(), "", List.of(), -+ List.of(), List.of(), List.of(), false, "1", BedrockRenderController.UvAnimation.IDENTITY) -+ .partVisibility().isEmpty()); -+ } -+} -diff --git a/src/test/java/org/cube/converter/parser/bedrock/controller/BedrockControllerLightingTest.java b/src/test/java/org/cube/converter/parser/bedrock/controller/BedrockControllerLightingTest.java -new file mode 100644 -index 0000000..9553968 ---- /dev/null -+++ b/src/test/java/org/cube/converter/parser/bedrock/controller/BedrockControllerLightingTest.java -@@ -0,0 +1,77 @@ -+package org.cube.converter.parser.bedrock.controller; -+ -+import org.junit.jupiter.api.Test; -+import org.cube.converter.data.bedrock.controller.BedrockRenderController; -+ -+import java.util.List; -+ -+import static org.junit.jupiter.api.Assertions.*; -+ -+class BedrockControllerLightingTest { -+ @Test -+ void retainsLightingExpressionsAndMaterialOverrideOrderWithArrays() { -+ final var controller = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.logo":{ -+ "geometry":"Geometry.default","textures":["Texture.default"], -+ "materials":[{"*":"Material.default"},{"Light*":"Array.materials[query.variant]"}], -+ "ignore_lighting":true,"light_color_multiplier":"query.variant * 0.5", -+ "arrays":{"materials":{"Array.materials":["Material.normal","Material.glow"]}} -+ }}} -+ """).get(0); -+ assertTrue(controller.ignoreLighting()); -+ assertEquals("query.variant * 0.5", controller.lightColorMultiplierExpression()); -+ assertEquals(List.of("*", "Light*"), controller.materialAssignments().stream().map(BedrockRenderController.MaterialAssignment::pattern).toList()); -+ assertEquals(2, controller.materials().get(0).values().size()); -+ } -+ -+ @Test -+ void retainsRepeatedMaterialPatternsInTheirDeclaredPositions() { -+ final var controller = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.repeated":{ -+ "materials":[{"*":"Material.base"},{"Arm*":"Material.glow"},{"*":"Material.final"}] -+ }}} -+ """).get(0); -+ assertEquals(List.of( -+ new BedrockRenderController.MaterialAssignment("*", "Material.base"), -+ new BedrockRenderController.MaterialAssignment("Arm*", "Material.glow"), -+ new BedrockRenderController.MaterialAssignment("*", "Material.final") -+ ), controller.materialAssignments()); -+ assertThrows(UnsupportedOperationException.class, () -> controller.materialAssignments().clear()); -+ } -+ -+ @Test -+ void defaultsAndNumericMultipliersDoNotRequireArrays() { -+ final var defaults = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.default":{}}} -+ """).get(0); -+ assertFalse(defaults.ignoreLighting()); -+ assertEquals(1F, Float.parseFloat(defaults.lightColorMultiplierExpression())); -+ final var dimmed = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.default":{"light_color_multiplier":0.4}}} -+ """).get(0); -+ assertEquals(0.4F, Float.parseFloat(dimmed.lightColorMultiplierExpression())); -+ } -+ @Test -+ void retainsIndependentUvComponentsAndIdentityDefaults() { -+ final var explicit = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.uv":{ -+ "uv_anim":{"offset":[-0.25,{}],"scale":[false,2.5]} -+ }}} -+ """).get(0).uvAnimation(); -+ assertEquals(-0.25F, Float.parseFloat(explicit.offsetU())); -+ assertEquals(0F, Float.parseFloat(explicit.offsetV())); -+ assertEquals(1F, Float.parseFloat(explicit.scaleU())); -+ assertEquals(2.5F, Float.parseFloat(explicit.scaleV())); -+ final var missing = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.default":{}}} -+ """).get(0).uvAnimation(); -+ assertEquals(BedrockRenderController.UvAnimation.IDENTITY, missing); -+ final var malformed = BedrockControllerParser.parse(""" -+ {"render_controllers":{"controller.render.default":{ -+ "uv_anim":{"offset":[0.4],"scale":null} -+ }}} -+ """).get(0).uvAnimation(); -+ assertEquals(BedrockRenderController.UvAnimation.IDENTITY, malformed); -+ } -+ -+} -diff --git a/src/test/java/org/cube/converter/parser/bedrock/controller/BedrockLegacyAnimationControllerTest.java b/src/test/java/org/cube/converter/parser/bedrock/controller/BedrockLegacyAnimationControllerTest.java -new file mode 100644 -index 0000000..eb7a077 ---- /dev/null -+++ b/src/test/java/org/cube/converter/parser/bedrock/controller/BedrockLegacyAnimationControllerTest.java -@@ -0,0 +1,38 @@ -+package org.cube.converter.parser.bedrock.controller; -+ -+import org.cube.converter.data.bedrock.BedrockEntityData; -+import org.cube.converter.parser.bedrock.data.impl.BedrockEntityParser; -+import org.junit.jupiter.api.Test; -+ -+import java.util.List; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+import static org.junit.jupiter.api.Assertions.assertThrows; -+import static org.junit.jupiter.api.Assertions.assertTrue; -+ -+class BedrockLegacyAnimationControllerTest { -+ @Test -+ void preservesLegacyDeclarationsInOrderIndependentlyOfModernScripts() { -+ final var entity = BedrockEntityParser.parse(""" -+ {"format_version":"1.8.0","minecraft:client_entity":{"description":{ -+ "identifier":"test:mob","animations":{"face":"animation.test.face"}, -+ "animation_controllers":[{"first":"controller.animation.test.first"}, -+ null,{"invalid":4,"second":"controller.animation.test.second"}], -+ "scripts":{"animate":["face"]} -+ }}} -+ """); -+ assertEquals(List.of(new BedrockEntityData.AnimationController("first", "controller.animation.test.first"), -+ new BedrockEntityData.AnimationController("second", "controller.animation.test.second")), entity.getAnimationControllers()); -+ assertEquals(1, entity.getScripts().animates().size()); -+ assertThrows(UnsupportedOperationException.class, () -> entity.getAnimationControllers().clear()); -+ } -+ -+ @Test -+ void defaultsWithoutInventingImplicitControllers() { -+ final var entity = BedrockEntityParser.parse(""" -+ {"minecraft:client_entity":{"description":{"identifier":"test:mob"}}} -+ """); -+ assertTrue(entity.getAnimationControllers().isEmpty()); -+ assertTrue(entity.getScripts().animates().isEmpty()); -+ } -+} --- -2.56.0 diff --git a/patches/cubeconverter/upstreamable/0002-preserve-actor-lighting-controls.pr.md b/patches/cubeconverter/upstreamable/0002-preserve-actor-lighting-controls.pr.md deleted file mode 100644 index 27f87838..00000000 --- a/patches/cubeconverter/upstreamable/0002-preserve-actor-lighting-controls.pr.md +++ /dev/null @@ -1,53 +0,0 @@ -Preserve `ignore_lighting` and the Molang `light_color_multiplier` expression from Bedrock render controllers. Retain material override order with an ordered map. Actor renderers need these values to select bone materials and reproduce controller lighting. - -The [render controller reference](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/visualreference/render_controller.v1.8.0?view=minecraft-bedrock-stable) defines both fields. A saved Bedrock 1.26.51.1 Hive controller uses them on its lobby logo. Target protocol is 2193. The parser keeps values without applying renderer policy. - -Validation: targeted parser tests cover explicit values, default values, controllers with arrays, and material override order. The complete CubeConverter test suite and build pass. - -Retain controller UV offset and scale expressions with native identity defaults. Invalid components leave valid neighboring components available. Client code evaluates camera-dependent expressions when it submits the draw. - -Bedrock 1.26.51.1 parser `141cabf60` initializes UV offset to zero and scale to one. Installed ENTITY vertex DXIL applies `UV * scale + offset`. The licensed executable SHA-256 is `537c0aee2e79afbdc94b44b28e00f466ae62bc50e2733d953b430db9dbaa9ee7`; target protocol is 2193. Native assets remain private. - -Targeted tests cover signed and fractional numeric components, invalid neighboring fields, malformed pairs, and identity defaults. This metadata does not add material mask effects or infer blending states. - - -Legacy `description.animation_controllers` declarations retain their alias, -identifier, and order for client entities and attachables. Absent fields -default to an empty list. Existing constructor callers remain compatible. -The parser keeps this metadata independently of modern `scripts.animate`; -client code chooses supported playback semantics. - -The active native Bedrock 1.26.51.1 Hive entity uses format 1.8.0 and declares -a controller without modern animation scripts. The only active saved -entity entry has SHA-256 -`58cf3b66a2ebe76751415ae7f5f9ad81b44464e5ec987f4f22d6e49639cc1f80`. -Tests verify ordered immutable declarations, independent modern scripts, -malformed neighbors, and defaults without implicit controllers. - -Equipped native models also require ordered `part_visibility` declarations and `scripts.parent_setup`. Preserve repeated and wildcard bone rules without collapsing them into a map. Keep setup expressions in their owner scope, separate from initialization and animation scripts. The declarations are immutable; existing constructor forms remain available. - -The recorded Bedrock 1.26.51.1 Hive angel wings use a wildcard visibility expression that excludes first-person and invisible rendering. Their attachable also supplies owner setup. Parser tests cover scalar and array setup, rule ordering, duplicate patterns, missing fields and defensive ownership. All 16 converter tests pass with no failures or skips. The renderer evaluates these declarations; the parser does not choose a binding or rendering policy. - -## Visibility parser API - -Expose the ordered part-visibility declaration parser to client adapters. The parser preserves declared expressions and rule order. Renderer selection, variable scope and client-specific fallback values remain with the consumer. This public helper is a local extension to the pinned upstream API. - - -## Ordered material declarations, October 26, 2026 - -Render controllers now retain an immutable list of material assignments, including repeated bone patterns. -The previous map removed earlier expressions and kept repeated keys at their original positions. -That representation lost both expression effects and later override precedence. - -The public `materialAssignments()` list replaces `materialsMap()`. -Constructor callers now supply `MaterialAssignment` lists. -Core and client consumers change together, and the core patch records this prerequisite. -Parsing does not evaluate expressions or select the final material for a bone. - -The [Molang reference](https://mojang.github.io/bedrock-samples/Molang.html) specifies ordered material expression evaluation. -The [animation reference](https://mojang.github.io/bedrock-samples/Animations.html) specifies later material overrides. -The bundled Bedrock 1.26.51.1 library includes eight controllers with multiple material rules, including axolotl limbs and chicken legs. -These assets establish target support, without a native execution comparison for the authored repeated-pattern cases. - -The parser regression retains wildcard, arm, and repeated wildcard assignments in their declared positions. -The list rejects mutation, and all 20 converter tests pass without skips. diff --git a/patches/cubeconverter/upstreamable/0003-preserve-authored-face-uv-rotations.patch b/patches/cubeconverter/upstreamable/0003-preserve-authored-face-uv-rotations.patch deleted file mode 100644 index b39c3d6b..00000000 --- a/patches/cubeconverter/upstreamable/0003-preserve-authored-face-uv-rotations.patch +++ /dev/null @@ -1,134 +0,0 @@ -From c771e6f9104976ca72b90323144d9859a6ea0842 Mon Sep 17 00:00:00 2001 -From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> -Date: Tue, 27 Oct 2026 00:33:46 +0100 -Subject: [PATCH] fix(models): preserve authored face UV rotations - -Read per-face uv_rotation into the shared geometry model and retain it -through cloning, normalization and Java item export. Compose fitted cube -rotations with the authored angle instead of replacing it. - -Bedrock added clockwise quarter-turn face UV rotation in geometry format -1.21.0, before the target Bedrock 1.26.51.1/protocol 2193. The 1.26.40 -schema retains the four supported angles. This supplies the same parsed -metadata to native actor and skin consumers without changing coordinates. -https://learn.microsoft.com/en-us/minecraft/creator/reference/content/visualreference/geometry.v1.26.40 - -Regression controls cover all six faces and all four angles, signed UV -rectangles, absent faces, independent clones, rotation composition, -normalization and Java item export. The original implementation fails -the regression; the complete converter test suite passes after the fix. ---- - .../cube/converter/util/element/UVMap.java | 28 ++++++------- - .../converter/util/element/UVMapTest.java | 42 +++++++++++++++++++ - 2 files changed, 55 insertions(+), 15 deletions(-) - create mode 100644 src/test/java/org/cube/converter/util/element/UVMapTest.java - -diff --git a/src/main/java/org/cube/converter/util/element/UVMap.java b/src/main/java/org/cube/converter/util/element/UVMap.java -index 9c1e76a..7778e1a 100644 ---- a/src/main/java/org/cube/converter/util/element/UVMap.java -+++ b/src/main/java/org/cube/converter/util/element/UVMap.java -@@ -26,7 +26,8 @@ public final class UVMap { - return; - } - -- float rotation = (roll * 90) % 360; -+ float rotation = (this.uvRotation.getOrDefault(direction, 0F) + roll * 90) % 360; -+ if (rotation < 0) rotation += 360; - this.uvRotation.put(direction, rotation); - } - -@@ -47,6 +48,7 @@ public final class UVMap { - map.getUvMap().put(entry.getKey(), uv); - } - -+ map.getUvRotation().putAll(this.uvRotation); - return map; - } - -@@ -68,20 +70,16 @@ public final class UVMap { - } - - final Position3V size = new Position3V(object.getAsJsonArray("size")); -- putIfExist(Direction.NORTH, uv, size, map.getUvMap()); -- putIfExist(Direction.EAST, uv, size, map.getUvMap()); -- putIfExist(Direction.SOUTH, uv, size, map.getUvMap()); -- putIfExist(Direction.WEST, uv, size, map.getUvMap()); -- putIfExist(Direction.UP, uv, size, map.getUvMap()); -- putIfExist(Direction.DOWN, uv, size, map.getUvMap()); -- return map; -- } -- -- private static void putIfExist(final Direction direction, final JsonObject object, final Position3V size, final Map map) { -- String name = direction.name().toLowerCase(); -- if (object.has(name)) { -- map.put(direction, getUVDirection(object.getAsJsonObject(name), direction, size)); -+ for (final Direction direction : Direction.values()) { -+ final String name = direction.name().toLowerCase(java.util.Locale.ROOT); -+ if (!uv.has(name)) continue; -+ final JsonObject face = uv.getAsJsonObject(name); -+ map.uvMap.put(direction, getUVDirection(face, direction, size)); -+ if (face.has("uv_rotation")) { -+ map.uvRotation.put(direction, face.get("uv_rotation").getAsFloat()); -+ } - } -+ return map; - } - - private static Float[] getUVDirection(final JsonObject object, final Direction direction, final Position3V size) { -@@ -120,4 +118,4 @@ public final class UVMap { - public enum UVType { - PERFACE, BOX - } --} -\ No newline at end of file -+} -diff --git a/src/test/java/org/cube/converter/util/element/UVMapTest.java b/src/test/java/org/cube/converter/util/element/UVMapTest.java -new file mode 100644 -index 0000000..7f66d88 ---- /dev/null -+++ b/src/test/java/org/cube/converter/util/element/UVMapTest.java -@@ -0,0 +1,42 @@ -+package org.cube.converter.util.element; -+ -+import com.viaversion.viaversion.libs.gson.JsonParser; -+import org.cube.converter.converter.enums.RotationType; -+import org.cube.converter.model.impl.bedrock.BedrockGeometryModel; -+import org.junit.jupiter.api.Test; -+ -+import static org.junit.jupiter.api.Assertions.*; -+ -+class UVMapTest { -+ @Test -+ void preservesAuthoredFaceRotationsThroughParsingCloningAndItemExport() { -+ for (final Direction direction : Direction.values()) { -+ for (final int rotation : new int[]{0, 90, 180, 270}) { -+ final var document = JsonParser.parseString(""" -+ {"format_version":"1.21.0","minecraft:geometry":[{ -+ "description":{"identifier":"geometry.rotated","texture_width":64,"texture_height":32}, -+ "bones":[{"name":"root","cubes":[{"origin":[0,0,0],"size":[4,6,8],"uv":{ -+ "%s":{"uv":[12,10],"uv_size":[-4,6],"uv_rotation":%d}}}]}]}]} -+ """.formatted(direction.name().toLowerCase(java.util.Locale.ROOT), rotation)).getAsJsonObject(); -+ final var original = document.deepCopy(); -+ final var geometry = BedrockGeometryModel.fromJson(document).get(0); -+ final var uv = geometry.getParents().get(0).getCubes().get(0).getUvMap(); -+ assertEquals((float) rotation, uv.getUvRotation().getOrDefault(direction, 0F)); -+ final var copy = uv.clone(); -+ assertEquals(uv.getUvRotation(), copy.getUvRotation()); -+ copy.rotate(direction, 1); -+ assertEquals((float) ((rotation + 90) % 360), copy.getUvRotation().get(direction)); -+ assertEquals((float) rotation, uv.getUvRotation().getOrDefault(direction, 0F)); -+ final var normalized = uv.clone().toJavaPerfaceUV(64, 32); -+ assertEquals(uv.getUvRotation(), normalized.getUvRotation()); -+ assertArrayEquals(new Float[]{3F, 5F, 2F, 8F}, normalized.getUvMap().get(direction)); -+ final var exported = geometry.toJavaItemModel("test:actor", RotationType.POST_1_21_11).compile(); -+ final var faces = exported.getAsJsonArray("elements").get(0).getAsJsonObject().getAsJsonObject("faces"); -+ assertEquals(1, faces.size()); -+ final var face = faces.getAsJsonObject(direction.name().toLowerCase(java.util.Locale.ROOT)); -+ assertEquals(rotation, face.has("rotation") ? face.get("rotation").getAsInt() : 0); -+ assertEquals(original, document); -+ } -+ } -+ } -+} --- -2.56.0 diff --git a/patches/cubeconverter/upstreamable/0003-preserve-authored-face-uv-rotations.pr.md b/patches/cubeconverter/upstreamable/0003-preserve-authored-face-uv-rotations.pr.md deleted file mode 100644 index 9a969b49..00000000 --- a/patches/cubeconverter/upstreamable/0003-preserve-authored-face-uv-rotations.pr.md +++ /dev/null @@ -1,15 +0,0 @@ -Preserve per-face `uv_rotation` when parsing Bedrock geometry. -Keep each angle through cloning, UV normalization and Java item export. -When fitting requires another cube rotation, add it to the authored angle. - -Bedrock added entity face rotation in geometry format 1.21.0. -The [1.26.40 geometry reference](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/visualreference/geometry.v1.26.40?view=minecraft-bedrock-stable) -specifies clockwise rotations of 0, 90, 180 and 270 degrees. -Both versions precede the target Bedrock 1.26.51.1, protocol 2193. -This patch preserves valid authored values without adding geometry version dispatch. - -The regression parses all six faces at all four angles. -It checks signed UV rectangles, absent faces, independent clones, added rotations, normalization and exported item faces. -The original implementation fails the regression. -The complete converter test suite passes with the change. -These checks establish parsed and exported coordinates, not native image parity. diff --git a/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch b/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch index f0facc69..b3edc79e 100644 --- a/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch +++ b/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch @@ -1,4 +1,4 @@ -From 3efc13f7266e1826ee69977f548a28bd4711d68e Mon Sep 17 00:00:00 2001 +From ceae6155574f1147d94a6e814a2bf37c40dfe82e Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(entity-events): translate Bedrock actor events to Java @@ -301,10 +301,10 @@ index 00000000..36a2d78e + +} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java b/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java -index b47f5471..88ce252d 100644 +index a4b3ec09..ae173e3f 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java +++ b/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java -@@ -223,14 +223,18 @@ public class Entity { +@@ -224,14 +224,18 @@ public class Entity { } public void playSound(final String soundEvent) { @@ -327,18 +327,18 @@ index b47f5471..88ce252d 100644 levelSoundEvent.send(BedrockProtocol.class, false); } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java -index 4578a73f..7bb6b8f8 100644 +index 6f65acbc..b91f27d8 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java -@@ -40,6 +40,7 @@ import net.raphimc.viabedrock.api.model.NativeActorMessage; - import net.raphimc.viabedrock.api.model.ActorMotionCommand; +@@ -41,6 +41,7 @@ import net.raphimc.viabedrock.api.model.ActorMotionCommand; + import net.raphimc.viabedrock.api.model.RemotePlayerMovement; import net.raphimc.viabedrock.api.model.MovementCorrectionTiming; import net.raphimc.viabedrock.api.model.NativeAnimationMessage; +import net.raphimc.viabedrock.api.model.NativeFishingEvent; import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity; import net.raphimc.viabedrock.api.model.entity.CustomEntity; import net.raphimc.viabedrock.api.model.entity.Entity; -@@ -51,10 +52,12 @@ import net.raphimc.viabedrock.api.util.RegistryUtil; +@@ -52,10 +53,12 @@ import net.raphimc.viabedrock.api.util.RegistryUtil; import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; @@ -351,7 +351,7 @@ index 4578a73f..7bb6b8f8 100644 import net.raphimc.viabedrock.protocol.data.enums.java.Relative; import net.raphimc.viabedrock.protocol.data.enums.java.generated.EquipmentSlot; import net.raphimc.viabedrock.protocol.data.generated.java.EntityDataFields; -@@ -63,6 +66,9 @@ import net.raphimc.viabedrock.protocol.model.*; +@@ -64,6 +67,9 @@ import net.raphimc.viabedrock.protocol.model.*; import net.raphimc.viabedrock.protocol.rewriter.EntityMetadataRewriter; import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; import net.raphimc.viabedrock.protocol.storage.EntityPropertyTracker; @@ -361,7 +361,7 @@ index 4578a73f..7bb6b8f8 100644 import net.raphimc.viabedrock.protocol.storage.ChannelStorage; import net.raphimc.viabedrock.protocol.storage.EntityTracker; import net.raphimc.viabedrock.protocol.storage.PlayerPredictionStorage; -@@ -73,6 +79,7 @@ import net.raphimc.viabedrock.protocol.types.BedrockTypes; +@@ -74,6 +80,7 @@ import net.raphimc.viabedrock.protocol.types.BedrockTypes; import java.util.ArrayList; import java.util.List; @@ -369,7 +369,7 @@ index 4578a73f..7bb6b8f8 100644 import java.util.logging.Level; public final class EntityPackets { -@@ -509,6 +516,43 @@ public final class EntityPackets { +@@ -512,6 +519,43 @@ public final class EntityPackets { hurt.send(BedrockProtocol.class); } switch (event) { @@ -413,7 +413,7 @@ index 4578a73f..7bb6b8f8 100644 case HURT -> { final CompoundTag damageTypeRegistry = gameSession.getJavaRegistries().getCompoundTag(RegistryKeys.DAMAGE_TYPE); final SharedTypes_Legacy_ActorDamageCause damageCause = SharedTypes_Legacy_ActorDamageCause.getByValue(data, SharedTypes_Legacy_ActorDamageCause.Override); -@@ -538,12 +582,17 @@ public final class EntityPackets { +@@ -541,12 +585,17 @@ public final class EntityPackets { } if (entity != entityTracker.getClientPlayer()) { entity.playSound("death"); @@ -434,7 +434,7 @@ index 4578a73f..7bb6b8f8 100644 } } }); -@@ -791,6 +840,34 @@ public final class EntityPackets { +@@ -794,6 +843,34 @@ public final class EntityPackets { return entity; } diff --git a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch index cb72a005..df80002c 100644 --- a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch +++ b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch @@ -1,4 +1,4 @@ -From b27caf00cff9f20385e2186139e821ce84219a2d Mon Sep 17 00:00:00 2001 +From c2118b2d96da7c1906878eba61d978b78244ce4e Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(inventory): translate server-authoritative Bedrock @@ -848,7 +848,7 @@ Keep legacy requests idempotent and publish readiness once per opening. .../protocol/model/recipe/ItemRecipeKey.java | 35 + .../protocol/packet/ClientPlayerPackets.java | 60 +- .../protocol/packet/InteractionPackets.java | 51 +- - .../protocol/packet/InventoryPackets.java | 1525 +++- + .../protocol/packet/InventoryPackets.java | 1517 +++- .../protocol/packet/JoinPackets.java | 2 +- .../protocol/packet/MultiStatePackets.java | 34 +- .../protocol/packet/WorldPackets.java | 12 + @@ -932,7 +932,7 @@ Keep legacy requests idempotent and publish readiness once per opening. .../item/NetworkItemConstructionTest.java | 171 + .../types/item/NetworkItemCountTest.java | 35 +- .../types/item/NetworkItemStackFixtures.java | 69 + - 139 files changed, 29041 insertions(+), 2047 deletions(-) + 139 files changed, 29037 insertions(+), 2043 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenClose.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenReady.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenRequest.java @@ -1030,10 +1030,10 @@ Keep legacy requests idempotent and publish readiness once per opening. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/types/item/NetworkItemStackFixtures.java diff --git a/build.gradle b/build.gradle -index dfec773e..e02da714 100644 +index 5ef41e2a..e2f34a13 100644 --- a/build.gradle +++ b/build.gradle -@@ -162,7 +162,8 @@ dependencies { +@@ -175,7 +175,8 @@ dependencies { testImplementation "com.viaversion:viaversion-common:5.12.0" testImplementation "com.google.guava:guava:33.7.1-jre" @@ -9710,7 +9710,7 @@ index 223db3d9..d8e307df 100644 } chunkTracker.trackBlockPrediction(sequence, position, placedPosition); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index d22d7616..48721470 100644 +index 7b3138e1..6c1434ed 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -17,26 +17,28 @@ @@ -9912,7 +9912,7 @@ index d22d7616..48721470 100644 wrapper.cancel(); } } -@@ -202,347 +286,863 @@ public final class InventoryPackets { +@@ -208,347 +292,863 @@ public final class InventoryPackets { } public static void register(final BedrockProtocol protocol) { @@ -9934,17 +9934,34 @@ index d22d7616..48721470 100644 - ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Unknown ContainerType: " + rawType); - wrapper.cancel(); - return; -- } ++ protocol.registerClientbound(ClientboundBedrockPackets.CREATIVE_CONTENT, null, wrapper -> { ++ wrapper.cancel(); ++ final ItemRewriter itemRewriter = wrapper.user().get(ItemRewriter.class); ++ final int groups = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); ++ for (int i = 0; i < groups; i++) { ++ wrapper.read(Types.UNSIGNED_BYTE); // category ++ wrapper.read(BedrockTypes.STRING); // name ++ readCreativeItem(wrapper, itemRewriter); // icon + } - final BlockPosition position = wrapper.read(BedrockTypes.BLOCK_POSITION); // position - final long actorId = wrapper.read(BedrockTypes.VAR_LONG); // entity unique id - final Entity actor = actorId == -1 ? null : wrapper.user().get(EntityTracker.class).getEntityByUid(actorId); -- + - if (inventoryTracker.isAnyScreenOpen()) { - ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Server tried to open container while another container is open"); - PacketFactory.sendBedrockContainerClose(wrapper.user(), (byte) -1, ContainerType.NONE); - wrapper.cancel(); - return; -- } ++ itemRewriter.clearCreativeItems(); ++ final int entries = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); ++ for (int i = 0; i < entries; i++) { ++ wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // creative net ID ++ final BedrockItem item = readCreativeItem(wrapper, itemRewriter); ++ wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // group index ++ if (!item.isEmpty()) { ++ itemRewriter.addCreativeItem(item); ++ } + } - final BedrockBlockEntity blockEntity = actorId == -1 ? chunkTracker.getBlockEntity(position) : null; - final ContainerTitle openingTitle = containerTitle(wrapper.user(), type, actor, blockEntity, - new TranslationComponent(switch (type) { @@ -9954,7 +9971,40 @@ index d22d7616..48721470 100644 - default -> "container." + blockStateRewriter.tag(chunkTracker.getBlockState(position)); - })); - final TextComponent title = openingTitle.caption(); -- ++ }); ++ protocol.registerClientbound( ++ ClientboundBedrockPackets.CONTAINER_OPEN, ++ ClientboundPackets26_3.OPEN_SCREEN, ++ wrapper -> { ++ final ChunkTracker chunkTracker = wrapper.user().get(ChunkTracker.class); ++ final BlockStateRewriter blockStateRewriter = ++ wrapper.user().get(BlockStateRewriter.class); ++ final InventoryTracker inventoryTracker = ++ wrapper.user().get(InventoryTracker.class); ++ final byte containerId = wrapper.read(Types.BYTE); // container id ++ final byte rawType = wrapper.read(Types.BYTE); // type ++ final ContainerType type = ContainerType.getByValue(rawType); ++ if (type == null) { ++ ViaBedrock.getPlatform() ++ .getLogger() ++ .log(Level.WARNING, "Unknown ContainerType: " + rawType); ++ wrapper.cancel(); ++ return; ++ } ++ final BlockPosition position = ++ wrapper.read(BedrockTypes.BLOCK_POSITION); // position ++ final long entityUniqueId = ++ wrapper.read(BedrockTypes.VAR_LONG); // entity unique id ++ final ContainerOpenContext openContext = new ContainerOpenContext(rawType, position, entityUniqueId); ++ final Entity containerEntity = ++ wrapper.user() ++ .get(EntityTracker.class) ++ .getEntityByUid(entityUniqueId); ++ final ContainerType screenType = ++ vehicleScreenType(type, containerEntity); ++ final BlockPosition containerPosition = ++ screenType != type || entityUniqueId != -1 ? null : position; + - final Container container; - switch (type) { - case INVENTORY -> { @@ -9970,7 +10020,10 @@ index d22d7616..48721470 100644 - wrapper.cancel(); - if (!WorldPackets.openNativeCommandBlock(wrapper.user(), containerId, blockEntity)) { - PacketFactory.sendBedrockContainerClose(wrapper.user(), containerId, ContainerType.NONE); -- } ++ if (screenType == ContainerType.HORSE) { ++ openHorseContainer(wrapper, containerId, containerEntity, null, openContext); ++ return; + } - return; - } - case NONE, CAULDRON, JUKEBOX, ARMOR, HAND, HUD, DECORATED_POT -> { // Bedrock client can't open these containers @@ -9986,7 +10039,7 @@ index d22d7616..48721470 100644 - } - } - inventoryTracker.setCurrentContainer(container); -- + - wrapper.write(Types.VAR_INT, (int) containerId); // container id - final int javaMenuId = BedrockProtocol.MAPPINGS.getBedrockToJavaContainers().get(type); - wrapper.write(Types.VAR_INT, javaMenuId); // type @@ -10001,97 +10054,11 @@ index d22d7616..48721470 100644 - } - }); - protocol.registerClientbound(ClientboundBedrockPackets.INVENTORY_CONTENT, ClientboundPackets26_3.CONTAINER_SET_CONTENT, wrapper -> { -+ protocol.registerClientbound(ClientboundBedrockPackets.CREATIVE_CONTENT, null, wrapper -> { -+ wrapper.cancel(); - final ItemRewriter itemRewriter = wrapper.user().get(ItemRewriter.class); +- final ItemRewriter itemRewriter = wrapper.user().get(ItemRewriter.class); - final int containerId = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // container id - final BedrockItem[] items = wrapper.read(itemRewriter.newItemArrayType()); // items - final FullContainerName containerName = wrapper.read(BedrockTypes.FULL_CONTAINER_NAME); // container name - final BedrockItem storageItem = wrapper.read(itemRewriter.newItemType()); // storage item -- -- final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); -- final Container container = inventoryTracker.getContainerClientbound((byte) containerId, containerName, storageItem); -- if (container != null && container.setItems(items)) { -- PacketFactory.writeJavaContainerSetContent(wrapper, container); -- } else { -- wrapper.cancel(); -+ final int groups = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); -+ for (int i = 0; i < groups; i++) { -+ wrapper.read(Types.UNSIGNED_BYTE); // category -+ wrapper.read(BedrockTypes.STRING); // name -+ readCreativeItem(wrapper, itemRewriter); // icon - } -- }); -- protocol.registerClientbound(ClientboundBedrockPackets.INVENTORY_SLOT, ClientboundPackets26_3.CONTAINER_SET_SLOT, wrapper -> { -- final ItemRewriter itemRewriter = wrapper.user().get(ItemRewriter.class); -- final int containerId = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // container id -- final int slot = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // slot -- final FullContainerName containerName = wrapper.read(BedrockTypes.OPTIONAL_FULL_CONTAINER_NAME); // container name -- final BedrockItem storageItem = wrapper.read(itemRewriter.optionalNewItemType()); // storage item -- final BedrockItem item = wrapper.read(itemRewriter.newItemType()); // item -- -- final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); -- final Container container = inventoryTracker.getContainerClientbound((byte) containerId, containerName, storageItem); -- if (container != null && container.setItem(slot, item)) { -- if (container.type() == ContainerType.HUD && slot == 0) { // cursor item -- wrapper.setPacketType(ClientboundPackets26_3.SET_CURSOR_ITEM); -- } else { -- wrapper.write(Types.VAR_INT, (int) container.javaContainerId()); // container id -- wrapper.write(Types.VAR_INT, 0); // revision -- wrapper.write(Types.SHORT, (short) container.javaSlot(slot)); // slot -+ -+ itemRewriter.clearCreativeItems(); -+ final int entries = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); -+ for (int i = 0; i < entries; i++) { -+ wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // creative net ID -+ final BedrockItem item = readCreativeItem(wrapper, itemRewriter); -+ wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // group index -+ if (!item.isEmpty()) { -+ itemRewriter.addCreativeItem(item); - } -- wrapper.write(VersionedTypes.V26_3.item, container.getJavaItem(slot)); // item -- } else { -- wrapper.cancel(); - } - }); -+ protocol.registerClientbound( -+ ClientboundBedrockPackets.CONTAINER_OPEN, -+ ClientboundPackets26_3.OPEN_SCREEN, -+ wrapper -> { -+ final ChunkTracker chunkTracker = wrapper.user().get(ChunkTracker.class); -+ final BlockStateRewriter blockStateRewriter = -+ wrapper.user().get(BlockStateRewriter.class); -+ final InventoryTracker inventoryTracker = -+ wrapper.user().get(InventoryTracker.class); -+ final byte containerId = wrapper.read(Types.BYTE); // container id -+ final byte rawType = wrapper.read(Types.BYTE); // type -+ final ContainerType type = ContainerType.getByValue(rawType); -+ if (type == null) { -+ ViaBedrock.getPlatform() -+ .getLogger() -+ .log(Level.WARNING, "Unknown ContainerType: " + rawType); -+ wrapper.cancel(); -+ return; -+ } -+ final BlockPosition position = -+ wrapper.read(BedrockTypes.BLOCK_POSITION); // position -+ final long entityUniqueId = -+ wrapper.read(BedrockTypes.VAR_LONG); // entity unique id -+ final ContainerOpenContext openContext = new ContainerOpenContext(rawType, position, entityUniqueId); -+ final Entity containerEntity = -+ wrapper.user() -+ .get(EntityTracker.class) -+ .getEntityByUid(entityUniqueId); -+ final ContainerType screenType = -+ vehicleScreenType(type, containerEntity); -+ final BlockPosition containerPosition = -+ screenType != type || entityUniqueId != -1 ? null : position; -+ -+ if (screenType == ContainerType.HORSE) { -+ openHorseContainer(wrapper, containerId, containerEntity, null, openContext); -+ return; -+ } -+ + if (inventoryTracker.isAnyScreenOpen()) { + ViaBedrock.getPlatform() + .getLogger() @@ -10252,7 +10219,22 @@ index d22d7616..48721470 100644 + } + container.resolveBlockBinding(); + inventoryTracker.setCurrentContainer(container, openContext); -+ + +- final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); +- final Container container = inventoryTracker.getContainerClientbound((byte) containerId, containerName, storageItem); +- if (container != null && container.setItems(items)) { +- PacketFactory.writeJavaContainerSetContent(wrapper, container); +- } else { +- wrapper.cancel(); +- } +- }); +- protocol.registerClientbound(ClientboundBedrockPackets.INVENTORY_SLOT, ClientboundPackets26_3.CONTAINER_SET_SLOT, wrapper -> { +- final ItemRewriter itemRewriter = wrapper.user().get(ItemRewriter.class); +- final int containerId = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // container id +- final int slot = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // slot +- final FullContainerName containerName = wrapper.read(BedrockTypes.OPTIONAL_FULL_CONTAINER_NAME); // container name +- final BedrockItem storageItem = wrapper.read(itemRewriter.optionalNewItemType()); // storage item +- final BedrockItem item = wrapper.read(itemRewriter.newItemType()); // item + wrapper.write(Types.VAR_INT, (int) containerId); // container id + final int javaMenuId = screenType == ContainerType.CONTAINER && size == 54 + ? BedrockProtocol.MAPPINGS.getJavaMenuId( @@ -10292,7 +10274,22 @@ index d22d7616..48721470 100644 + wrapper.read(BedrockTypes.FULL_CONTAINER_NAME); // container name + final BedrockItem storageItem = + wrapper.read(itemRewriter.newItemType()); // storage item -+ + +- final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); +- final Container container = inventoryTracker.getContainerClientbound((byte) containerId, containerName, storageItem); +- if (container != null && container.setItem(slot, item)) { +- if (container.type() == ContainerType.HUD && slot == 0) { // cursor item +- wrapper.setPacketType(ClientboundPackets26_3.SET_CURSOR_ITEM); +- } else { +- wrapper.write(Types.VAR_INT, (int) container.javaContainerId()); // container id +- wrapper.write(Types.VAR_INT, 0); // revision +- wrapper.write(Types.SHORT, (short) container.javaSlot(slot)); // slot +- } +- wrapper.write(VersionedTypes.V26_3.item, container.getJavaItem(slot)); // item +- } else { +- wrapper.cancel(); +- } +- }); + final InventoryTracker inventoryTracker = + wrapper.user().get(InventoryTracker.class); + final Container container = @@ -10461,6 +10458,24 @@ index d22d7616..48721470 100644 - return; - } - final short damage = wrapper.read(BedrockTypes.SHORT_LE); // damage +- +- final BedrockItem item = armorSlot.getValue() < armorContainer.size() ? armorContainer.getItem(armorSlot.getValue()) : BedrockItem.empty(); +- if (item.tag() == null) { +- item.setTag(new CompoundTag()); +- } +- item.tag().putInt("Damage", damage); +- +- final EquipmentSlot equipmentSlot = switch (armorSlot) { +- case Head -> EquipmentSlot.HEAD; +- case Torso -> EquipmentSlot.CHEST; +- case Legs -> EquipmentSlot.LEGS; +- case Feet -> EquipmentSlot.FEET; +- case Body -> EquipmentSlot.BODY; +- }; +- wrapper.write(Types.BYTE, (byte) (equipmentSlot.ordinal() | (i < (size - 1) ? Byte.MIN_VALUE : 0))); // slot +- wrapper.write(VersionedTypes.V26_3.item, wrapper.user().get(ItemRewriter.class).javaItem(item)); // item +- } +- }); + final InventoryTracker inventoryTracker = + wrapper.user().get(InventoryTracker.class); + final FullContainerName[] removedContainers = @@ -10543,23 +10558,13 @@ index d22d7616..48721470 100644 + final Container armorContainer = + wrapper.user().get(InventoryTracker.class).getArmorContainer(); -- final BedrockItem item = armorSlot.getValue() < armorContainer.size() ? armorContainer.getItem(armorSlot.getValue()) : BedrockItem.empty(); -- if (item.tag() == null) { -- item.setTag(new CompoundTag()); -- } -- item.tag().putInt("Damage", damage); -- -- final EquipmentSlot equipmentSlot = switch (armorSlot) { -- case Head -> EquipmentSlot.HEAD; -- case Torso -> EquipmentSlot.CHEST; -- case Legs -> EquipmentSlot.LEGS; -- case Feet -> EquipmentSlot.FEET; -- case Body -> EquipmentSlot.BODY; -- }; -- wrapper.write(Types.BYTE, (byte) (equipmentSlot.ordinal() | (i < (size - 1) ? Byte.MIN_VALUE : 0))); // slot -- wrapper.write(VersionedTypes.V26_3.item, wrapper.user().get(ItemRewriter.class).javaItem(item)); // item -- } -- }); +- protocol.registerServerbound(ServerboundPackets26_3.CONTAINER_CLICK, null, wrapper -> { +- wrapper.cancel(); +- final int containerId = wrapper.read(Types.VAR_INT); // container id +- final int revision = wrapper.read(Types.VAR_INT); // revision +- final short slot = wrapper.read(Types.SHORT); // slot +- final byte button = wrapper.read(Types.BYTE); // button +- final ContainerInput action = ContainerInput.values()[wrapper.read(Types.VAR_INT)]; // action + wrapper.write( + Types.VAR_INT, + wrapper.user() @@ -10584,14 +10589,6 @@ index d22d7616..48721470 100644 + } + final short damage = wrapper.read(BedrockTypes.SHORT_LE); // damage -- protocol.registerServerbound(ServerboundPackets26_3.CONTAINER_CLICK, null, wrapper -> { -- wrapper.cancel(); -- final int containerId = wrapper.read(Types.VAR_INT); // container id -- final int revision = wrapper.read(Types.VAR_INT); // revision -- final short slot = wrapper.read(Types.SHORT); // slot -- final byte button = wrapper.read(Types.BYTE); // button -- final ContainerInput action = ContainerInput.values()[wrapper.read(Types.VAR_INT)]; // action -- - final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); - if (inventoryTracker.getPendingCloseContainer() != null) { - wrapper.cancel(); @@ -10882,7 +10879,14 @@ index d22d7616..48721470 100644 - final String inputKey = String.valueOf(elementIndex); - if (!payload.contains(inputKey)) { - continue; -- } ++ final InventoryTracker inventoryTracker = ++ wrapper.user().get(InventoryTracker.class); ++ final InventoryContainer inventory = inventoryTracker.getInventoryContainer(); ++ if (javaSlot < 5 || javaSlot > 45 ++ || inventoryTracker.getPendingCloseContainer() != null) { ++ PacketFactory.sendJavaContainerSetContent(wrapper.user(), inventory); ++ return; + } - final FormElement element = customForm.getElements()[elementIndex]; - if (element instanceof CheckboxFormElement checkbox) { - checkbox.setChecked(payload.getBoolean(inputKey)); @@ -10894,14 +10898,6 @@ index d22d7616..48721470 100644 - stepSlider.setSelected(Integer.parseInt(payload.getString(inputKey))); - } else if (element instanceof TextFieldFormElement textField) { - textField.setValue(payload.getString(inputKey)); -+ final InventoryTracker inventoryTracker = -+ wrapper.user().get(InventoryTracker.class); -+ final InventoryContainer inventory = inventoryTracker.getInventoryContainer(); -+ if (javaSlot < 5 || javaSlot > 45 -+ || inventoryTracker.getPendingCloseContainer() != null) { -+ PacketFactory.sendJavaContainerSetContent(wrapper.user(), inventory); -+ return; -+ } + final Container target = javaSlot < 9 + ? inventoryTracker.getArmorContainer() + : javaSlot == 45 ? inventoryTracker.getOffhandContainer() : inventory; @@ -11085,7 +11081,7 @@ index d22d7616..48721470 100644 protocol.registerServerbound( ServerboundPackets26_3.EDIT_BOOK, null, -@@ -593,33 +1193,55 @@ public final class InventoryPackets { +@@ -599,33 +1199,55 @@ public final class InventoryPackets { updatedTag.put("pages", updatedPages); book.setTag(updatedTag); }); @@ -11163,7 +11159,7 @@ index d22d7616..48721470 100644 if (type == ContainerType.CONTAINER && actor != null) { final String filtered = actorTitle(actor, ActorDataIds.FILTERED_NAME); final String name = filtered.isEmpty() ? actorTitle(actor, ActorDataIds.NAME) : filtered; -@@ -631,7 +1253,7 @@ public final class InventoryPackets { +@@ -637,7 +1259,7 @@ public final class InventoryPackets { final String raw = customName.getValue(); return new ContainerTitle(TextUtil.stringToTextComponent(user.get(ResourcePackStorage.class).getTexts().translate(raw)), raw, false); } @@ -11172,7 +11168,7 @@ index d22d7616..48721470 100644 } private static String actorTitle(final Entity actor, final ActorDataIds field) { -@@ -657,21 +1279,23 @@ public final class InventoryPackets { +@@ -663,21 +1285,23 @@ public final class InventoryPackets { case HOPPER, MINECART_HOPPER -> size == 5 ? ContainerUi.Kind.HOPPER : null; case DISPENSER -> size == 9 ? ContainerUi.Kind.DISPENSER : null; case DROPPER -> size == 9 ? ContainerUi.Kind.DROPPER : null; @@ -11200,7 +11196,7 @@ index d22d7616..48721470 100644 return; } final var resources = wrapper.user().get(ResourcePackStorage.class); -@@ -680,22 +1304,33 @@ public final class InventoryPackets { +@@ -686,22 +1310,33 @@ public final class InventoryPackets { } final var state = wrapper.user().get(NativeContainerUiState.class); final var ui = resources.getContainerUi(); @@ -11238,7 +11234,7 @@ index d22d7616..48721470 100644 private static Dialog createFormDialog(final UserConnection user, final int id, final Form form, final ServerFormUi.Presentation presentation) { final ResourcePackStorage resourcePackStorage = user.get(ResourcePackStorage.class); -@@ -986,13 +1621,148 @@ public final class InventoryPackets { +@@ -992,13 +1627,148 @@ public final class InventoryPackets { } } @@ -11389,7 +11385,7 @@ index d22d7616..48721470 100644 for (String line : BedrockTextUtils.split(text, "\n")) { final TextInput textInput = new TextInput(TextUtil.stringToTextComponent(line)); textInput.setMaxLength(Integer.MAX_VALUE); -@@ -1000,14 +1770,53 @@ public final class InventoryPackets { +@@ -1006,14 +1776,53 @@ public final class InventoryPackets { dialog.getInputs().add(new Input("dummy", textInput)); } } else { // VB compatibility diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch index 3a109ce3..e170de55 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch @@ -1,4 +1,4 @@ -From 2319a7daacc884a1c0f3132901972bf65d536558 Mon Sep 17 00:00:00 2001 +From 0f7f45ee295492060deca48056bb21f95cd73106 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(viabedrock): retain Bedrock creative catalog entries @@ -4232,7 +4232,7 @@ index 00000000..f6d3290d +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUiTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUiTest.java new file mode 100644 -index 00000000..bb241279 +index 00000000..e8b3e47e --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUiTest.java @@ -0,0 +1,126 @@ @@ -4341,7 +4341,7 @@ index 00000000..bb241279 + final var paragraph = nodes(scene.getAsJsonObject("root")).stream() + .filter(node -> node.has("text") && node.get("text").getAsString().equals("Native §ggrid§f instructions")) + .findFirst().orElseThrow(); -+ assertTrue(paragraph.get("text_component").toString().contains("#ded605")); ++ assertTrue(paragraph.get("text_component").toString().contains("#efce16")); + assertFalse(paragraph.get("text_component").toString().contains("#pre_process_")); + final var labels = nodes(scene.getAsJsonObject("root")).stream().filter(node -> node.has("text")) + .map(node -> node.get("text").getAsString()).toList(); diff --git a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch index 1ecdac3c..48e42b1d 100644 --- a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch +++ b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch @@ -1,4 +1,4 @@ -From 4b34da3257b13a83590bffd65e010560056850d9 Mon Sep 17 00:00:00 2001 +From bd0b9b36579519a25eded8f6682333034d89c132 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(trading): translate Bedrock merchant inventories in core @@ -884,7 +884,7 @@ index 00000000..ea4d9561 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 64053175..f8124d38 100644 +index 9125de3a..207470d4 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -80,6 +80,7 @@ import net.raphimc.viabedrock.api.model.container.NativeStorageContainers; @@ -895,7 +895,7 @@ index 64053175..f8124d38 100644 import net.raphimc.viabedrock.api.model.container.HorseContainer; import net.raphimc.viabedrock.api.model.container.block.*; import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; -@@ -432,6 +433,11 @@ public final class InventoryPackets { +@@ -437,6 +438,11 @@ public final class InventoryPackets { return; } @@ -907,7 +907,7 @@ index 64053175..f8124d38 100644 if (inventoryTracker.isAnyScreenOpen()) { ViaBedrock.getPlatform() .getLogger() -@@ -1294,6 +1300,11 @@ public final class InventoryPackets { +@@ -1299,6 +1305,11 @@ public final class InventoryPackets { inventoryTracker.setCurrentContainerClosed(false); return; } diff --git a/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch b/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch index 82cb3605..fecf70a3 100644 --- a/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch +++ b/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch @@ -1,4 +1,4 @@ -From 85cb93fb6ba0f4f13ae04f535edd169ee8b0c1d8 Mon Sep 17 00:00:00 2001 +From 31a60bc7458a65a9996fa179f44258fc6fcbf723 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(inventory): translate Bedrock loom inventories in core @@ -368,10 +368,10 @@ index a3b933c2..6ef8f93d 100644 + } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index f8124d38..c43f45dd 100644 +index 207470d4..05c17d96 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -@@ -535,6 +535,7 @@ public final class InventoryPackets { +@@ -540,6 +540,7 @@ public final class InventoryPackets { container = new SmithingContainer( wrapper.user(), containerId, title, containerPosition); diff --git a/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch b/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch index 74700771..eea31829 100644 --- a/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch +++ b/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch @@ -1,4 +1,4 @@ -From a87f46e12536e5d3afdef00987a1aaeec15f89b5 Mon Sep 17 00:00:00 2001 +From f4492b364286fa017e56e5f392f5bc3f1fcfbd59 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(inventory): confirm survival offhand transfers from @@ -51,10 +51,10 @@ server acceptance and recovery remain separate controls. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/NativeContainerOffhandGuardTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 176bdfd5..c7461506 100644 +index 1cb31d42..15f46759 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -@@ -619,7 +619,6 @@ public final class InventoryPackets { +@@ -624,7 +624,6 @@ public final class InventoryPackets { wrapper.read(BedrockTypes.FULL_CONTAINER_NAME); // container name final BedrockItem storageItem = wrapper.read(itemRewriter.newItemType()); // storage item @@ -62,7 +62,7 @@ index 176bdfd5..c7461506 100644 final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); final Container container = -@@ -629,6 +628,10 @@ public final class InventoryPackets { +@@ -634,6 +633,10 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeContent(container); @@ -73,7 +73,7 @@ index 176bdfd5..c7461506 100644 if (container.type() == ContainerType.HUD) { inventoryTracker.getInventoryContainer().updateCraftingOutput(); } -@@ -658,7 +661,6 @@ public final class InventoryPackets { +@@ -663,7 +666,6 @@ public final class InventoryPackets { final BedrockItem storageItem = wrapper.read(itemRewriter.optionalNewItemType()); // storage item final BedrockItem item = wrapper.read(itemRewriter.newItemType()); // item @@ -81,7 +81,7 @@ index 176bdfd5..c7461506 100644 final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); final Container container = -@@ -668,6 +670,20 @@ public final class InventoryPackets { +@@ -673,6 +675,20 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeSlot(container, slot); @@ -102,7 +102,7 @@ index 176bdfd5..c7461506 100644 if (container.type() == ContainerType.HUD && ((slot >= 28 && slot <= 31) || slot == 50)) { inventoryTracker.getInventoryContainer().refreshCraftingOutput(); -@@ -961,6 +977,8 @@ public final class InventoryPackets { +@@ -966,6 +982,8 @@ public final class InventoryPackets { } final Container container = inventoryTracker.getContainerServerbound((byte) containerId); @@ -111,7 +111,7 @@ index 176bdfd5..c7461506 100644 final var nativeContainer = wrapper.user().get(NativeContainerUiState.class); if (nativeContainer != null && nativeContainer.blocksVanillaClicks()) { if (container != null) { -@@ -968,10 +986,11 @@ public final class InventoryPackets { +@@ -973,10 +991,11 @@ public final class InventoryPackets { } return; } @@ -125,7 +125,7 @@ index 176bdfd5..c7461506 100644 final boolean requestOpen = !requestTracker.isWaitingForInventoryOpen(); if (!requestTracker.queueInventoryOpenClick( revision, slot, button, action)) { -@@ -994,13 +1013,15 @@ public final class InventoryPackets { +@@ -999,13 +1018,15 @@ public final class InventoryPackets { wrapper.cancel(); return; } @@ -143,7 +143,7 @@ index 176bdfd5..c7461506 100644 if (!container.handleClick(revision, slot, button, action)) { if (container.type() != ContainerType.INVENTORY) { PacketFactory.sendJavaContainerSetContent( -@@ -1065,7 +1086,10 @@ public final class InventoryPackets { +@@ -1070,7 +1091,10 @@ public final class InventoryPackets { return; } final InventoryRequestTracker requestTracker = wrapper.user().get(InventoryRequestTracker.class); @@ -155,7 +155,7 @@ index 176bdfd5..c7461506 100644 if (!requestTracker.queueButtonClick(container, button)) { PacketFactory.sendJavaContainerSetContent(wrapper.user(), container); } -@@ -1870,6 +1894,120 @@ public final class InventoryPackets { +@@ -1875,6 +1899,120 @@ public final class InventoryPackets { new InventoryTransactionData.NormalTransactionData()); } @@ -277,7 +277,7 @@ index 176bdfd5..c7461506 100644 throws InformativeException { return readCreativeItem(wrapper, itemRewriter.getItems().getOrDefault("minecraft:shield", 0), itemRewriter); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java -index 8a380c35..c47f9dde 100644 +index ac448e28..885579ea 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java @@ -19,6 +19,10 @@ package net.raphimc.viabedrock.protocol.packet; @@ -572,10 +572,10 @@ index 1e40221d..3dcb9b50 100644 } diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java -index 44b29c83..19fe63ab 100644 +index add7f0d2..1cfb02f6 100644 --- a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java -@@ -170,6 +170,72 @@ class InventoryPacketsTest { +@@ -204,6 +204,72 @@ class InventoryPacketsTest { BedrockTypes.BYTE_ARRAY.write(buffer, userData); } diff --git a/patches/viabedrock/series.json b/patches/viabedrock/series.json index dfddddbb..30e8982b 100644 --- a/patches/viabedrock/series.json +++ b/patches/viabedrock/series.json @@ -7,6 +7,10 @@ "file": "0001-update-bedrock-to-1-26-60.patch", "title": "fix(protocol): complete Bedrock 1.26.60 compatibility" }, + { + "file": "0109-keep-resource-image-codecs-in-memory.patch", + "title": "fix(resources): keep resource image codecs in memory" + }, { "file": "0001-cache-converted-resource-packs.patch", "title": "perf(resource-packs): cache converted packs by content", @@ -228,11 +232,6 @@ "project": "viabedrock", "patch": "0064-share-native-float-molang-runtime.patch", "reason": "Land the bounded float runtime before actor property evaluation." - }, - { - "project": "cubeconverter", - "patch": "0002-preserve-actor-lighting-controls.patch", - "reason": "Publish ordered material assignments, including repeated patterns, before evaluating actor materials." } ] }, @@ -248,10 +247,6 @@ "file": "0057-forward-java-book-edits-to-bedrock.patch", "title": "fix(books): forward Java book edits to Bedrock" }, - { - "file": "0058-render-paired-bedrock-chests.patch", - "title": "fix(block-entities): render paired Bedrock chests" - }, { "file": "0061-expose-native-player-emotes.patch", "title": "feat(players): expose native emotes to client integrations" @@ -308,10 +303,6 @@ "file": "0074-match-native-extended-formatting-colors.patch", "title": "fix(text): match native extended formatting colors" }, - { - "file": "0075-encode-head-rotation-in-java-26-3-field-order.patch", - "title": "fix(players): encode head rotation in Java 26.3 field order" - }, { "file": "0076-translate-shared-bedrock-cooldown-categories.patch", "title": "feat(items): translate shared Bedrock cooldown categories" diff --git a/patches/viabedrock/setup/0001-stackanvil-branding.patch b/patches/viabedrock/setup/0001-stackanvil-branding.patch index d0718d70..9d1435ac 100644 --- a/patches/viabedrock/setup/0001-stackanvil-branding.patch +++ b/patches/viabedrock/setup/0001-stackanvil-branding.patch @@ -1,4 +1,4 @@ -From d5ce0cd14449fe8de9477d35bd97e79e87f310f8 Mon Sep 17 00:00:00 2001 +From 5f637e7b355c80929140d739327bbe260549115c Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:33 +0200 Subject: [PATCH] chore(build): configure StackAnvil identity and compatibility @@ -10,18 +10,20 @@ ViaProxy and capture tooling still import it. Core upstream removes these classes in1d76de72. Keep this host compatibility bridge in setup, outside all upstream PR branches. Preserve the original license and packet implementation; transport feature patches declare the compile APIs. + +Route the local CubeConverter declaration through withDependencies so both resolution and the published POM select the same StackAnvil artifact. Leave the upstream declaration untouched so a dependency-only PR applies without setup. --- - build.gradle | 17 ++- + build.gradle | 27 +++++ .../main/groovy/viabedrock.checkstyle.gradle | 1 + gradle.properties | 2 +- .../netty/raknet/AesEncryptionCodec.java | 100 ++++++++++++++++++ .../viabedrock/netty/raknet/MessageCodec.java | 65 ++++++++++++ - 5 files changed, 183 insertions(+), 2 deletions(-) + 5 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/raphimc/viabedrock/netty/raknet/AesEncryptionCodec.java create mode 100644 src/main/java/net/raphimc/viabedrock/netty/raknet/MessageCodec.java diff --git a/build.gradle b/build.gradle -index 0c726ca3..bc49b9ed 100644 +index e1fad9f6..6f57cc23 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,10 @@ plugins { @@ -35,14 +37,26 @@ index 0c726ca3..bc49b9ed 100644 checkstyleMain { exclude("**/generated/**") } -@@ -19,7 +23,18 @@ sourceSets { +@@ -19,7 +23,30 @@ sourceSets { tool.runtimeClasspath += tool.compileClasspath } +def stackAnvilMavenRepo = providers.gradleProperty("stackanvilMavenRepo") -+def cubeConverterDependency = stackAnvilMavenRepo.present -+ ? "org.oryxel.cube:cubeconverter:1.3-StackAnvil" -+ : "com.github.oryxel1:CubeConverter:7cf736c4a5" ++if (stackAnvilMavenRepo.present) { ++ configurations.configureEach { ++ withDependencies { declaredDependencies -> ++ def upstreamCubeConverter = declaredDependencies.find { ++ it.group == "com.github.oryxel1" && it.name == "CubeConverter" ++ } ++ if (upstreamCubeConverter != null) { ++ declaredDependencies.remove(upstreamCubeConverter) ++ def localCubeConverter = project.dependencies.create("org.oryxel.cube:cubeconverter:1.3-StackAnvil") ++ localCubeConverter.transitive = upstreamCubeConverter.transitive ++ declaredDependencies.add(localCubeConverter) ++ } ++ } ++ } ++} + repositories { + if (stackAnvilMavenRepo.present) { @@ -54,15 +68,6 @@ index 0c726ca3..bc49b9ed 100644 //mavenLocal() // Uncomment when updating maven { name = "ViaVersion" -@@ -80,7 +95,7 @@ dependencies { - api "com.vdurmont:semver4j:3.1.0" - api "com.mojang:brigadier:1.3.10" - api "at.yawk.lz4:lz4-java:1.12.1" -- api("com.github.oryxel1:CubeConverter:7cf736c4a5") { -+ api(cubeConverterDependency) { - transitive = false - } - api("team.unnamed:mocha:3.0.1") { diff --git a/buildSrc/src/main/groovy/viabedrock.checkstyle.gradle b/buildSrc/src/main/groovy/viabedrock.checkstyle.gradle index 55dbc1bd..f1597378 100644 --- a/buildSrc/src/main/groovy/viabedrock.checkstyle.gradle diff --git a/patches/viabedrock/upstreamable/0001-cache-converted-resource-packs.patch b/patches/viabedrock/upstreamable/0001-cache-converted-resource-packs.patch index 72effae6..0e58d846 100644 --- a/patches/viabedrock/upstreamable/0001-cache-converted-resource-packs.patch +++ b/patches/viabedrock/upstreamable/0001-cache-converted-resource-packs.patch @@ -1,4 +1,4 @@ -From 21eaf85681d93db079ce683dd1e88177328517a6 Mon Sep 17 00:00:00 2001 +From 65f916bfbe564e994de730680bf94ae0b49a2b13 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:33 +0200 Subject: [PATCH] perf(resource-packs): cache converted packs by content @@ -178,10 +178,10 @@ conversion change, while the metadata manifest schema remains compatible. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/ResourcePackLoadStateTrackerTest.java diff --git a/build.gradle b/build.gradle -index f6b3a313..5d8aa157 100644 +index cadcba22..abde84d0 100644 --- a/build.gradle +++ b/build.gradle -@@ -103,6 +103,8 @@ dependencies { +@@ -104,6 +104,8 @@ dependencies { exclude group: "com.viaversion.mcstructs", module: "snbt" exclude group: "com.viaversion.mcstructs", module: "converter" } @@ -279,17 +279,19 @@ index 00000000..abc19f6c + +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java -index 622e99d2..21dd17dd 100644 +index e63777a3..b136b605 100644 --- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java -@@ -21,22 +21,25 @@ import com.viaversion.viaversion.libs.gson.JsonObject; +@@ -21,6 +21,8 @@ import com.viaversion.viaversion.libs.gson.JsonObject; import com.viaversion.viaversion.util.GsonUtil; import net.raphimc.viabedrock.api.util.JsonUtil; +import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream; + import javax.imageio.ImageIO; - import java.awt.image.BufferedImage; + import javax.imageio.stream.MemoryCacheImageInputStream; + import javax.imageio.stream.MemoryCacheImageOutputStream; +@@ -28,17 +30,18 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -312,7 +314,7 @@ index 622e99d2..21dd17dd 100644 public abstract List getFilesShallow(final String path, final String extension); -@@ -59,7 +62,24 @@ public abstract class Content { +@@ -61,7 +64,24 @@ public abstract class Content { public abstract byte[] get(final String path); @@ -338,7 +340,7 @@ index 622e99d2..21dd17dd 100644 public String getString(final String path) { final byte[] bytes = this.get(path); -@@ -157,22 +177,22 @@ public abstract class Content { +@@ -159,22 +179,22 @@ public abstract class Content { } public void copyFrom(final Content content, final String sourcePath, final String targetPath) { diff --git a/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.patch b/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.patch index 5adda315..e026c8dc 100644 --- a/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.patch +++ b/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.patch @@ -1,4 +1,4 @@ -From b89e13e1ac9c1c4109cc91efaaa237897821d7c4 Mon Sep 17 00:00:00 2001 +From aeed94c88e5a5b3b959b07be3db26030fd20f73e Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:53:57 +0100 Subject: [PATCH] fix(protocol): complete Bedrock 1.26.60 compatibility @@ -118,10 +118,10 @@ index 02470c82..4f9b391e 100644 +Its identifier, spawn egg, and numeric ID 155 come from the matching client definition and `ActorType` metadata. +The Frostbite entry is a curated experimental addition, not part of the default server registry capture. diff --git a/build.gradle b/build.gradle -index bf1ef357..f6b3a313 100644 +index 6f57cc23..cadcba22 100644 --- a/build.gradle +++ b/build.gradle -@@ -116,6 +116,17 @@ dependencies { +@@ -117,6 +117,17 @@ dependencies { toolImplementation "org.jsoup:jsoup:1.23.2" toolImplementation "org.ow2.asm:asm-tree:9.11.1" toolImplementation "net.lenni0451.commons:asm:1.9.2" diff --git a/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch b/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch index 1cda1c28..8ffae213 100644 --- a/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch +++ b/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch @@ -1,4 +1,4 @@ -From 6fdbf2ac49f8051d39622acfeffef60293135c86 Mon Sep 17 00:00:00 2001 +From 647da9bd992fdb8fccca1c9380a95539847c78c9 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(metadata): preserve sparse properties and composed actor @@ -2856,7 +2856,7 @@ index 00000000..53a14284 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/rewriter/EntityNameFormattingTest.java b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/EntityNameFormattingTest.java new file mode 100644 -index 00000000..d2100b69 +index 00000000..19e49800 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/EntityNameFormattingTest.java @@ -0,0 +1,256 @@ @@ -2969,7 +2969,7 @@ index 00000000..d2100b69 + final List parts = parts(component); + assertEquals(3, parts.size()); + assertEquals(0x55FF55, parts.get(0).getStyle().getColor().getRgbValue()); -+ assertEquals(0x8AD22C, parts.get(1).getStyle().getColor().getRgbValue()); ++ assertEquals(0x89D22C, parts.get(1).getStyle().getColor().getRgbValue()); + assertTrue(parts.get(0).getStyle().isBold()); + assertTrue(parts.get(1).getStyle().isBold()); + assertFalse(parts.get(parts.size() - 1).getStyle().isBold()); @@ -2981,13 +2981,13 @@ index 00000000..d2100b69 + final Entity green = actor(false, palette(0.541, 0.824, 0.173)); + final Entity other = actor(true, palette(0.2, 0.4, 0.8)); + final Tag first = updateName(green, "§pA"); -+ assertEquals(0x8AD22C, component(first).getStyle().getColor().getRgbValue()); ++ assertEquals(0x89D22C, component(first).getStyle().getColor().getRgbValue()); + assertEquals(0x3366CC, component(updateName(other, "§pA")).getStyle().getColor().getRgbValue()); + final List sparse = new ArrayList<>(); + green.updateEntityData(new EntityData[]{new EntityData(81, EntityDataTypesBedrock.BYTE, (byte) 1)}, sparse); + assertTrue(sparse.stream().noneMatch(data -> data.id() == FIELDS.indexOf(EntityDataFields.CUSTOM_NAME))); -+ assertEquals(0x8AD22C, component(updateName(green, "§pB")).getStyle().getColor().getRgbValue()); -+ assertEquals(0x8AD22C, component(first).getStyle().getColor().getRgbValue()); ++ assertEquals(0x89D22C, component(updateName(green, "§pB")).getStyle().getColor().getRgbValue()); ++ assertEquals(0x89D22C, component(first).getStyle().getColor().getRgbValue()); + assertNull(updateName(green, "")); + assertEquals(0xDEB12D, component(updateName(actor(false, null), "§pA")).getStyle().getColor().getRgbValue()); + } @@ -3009,7 +3009,7 @@ index 00000000..d2100b69 + assertEquals(profile, player.name()); + final List styled = parts(prefix); + assertEquals(0x55FF55, styled.get(0).getStyle().getColor().getRgbValue()); -+ assertEquals(0x8AD22C, styled.get(1).getStyle().getColor().getRgbValue()); ++ assertEquals(0x89D22C, styled.get(1).getStyle().getColor().getRgbValue()); + assertTrue(styled.get(0).getStyle().isBold()); + assertTrue(styled.get(1).getStyle().isBold()); + assertFalse(styled.get(styled.size() - 1).getStyle().isBold()); @@ -3024,7 +3024,7 @@ index 00000000..d2100b69 + assertEquals(sent, this.teamPackets.size()); + updateName(player, "§pReplacement"); + assertEquals("Replacement", this.teamPrefix().asUnformattedString()); -+ assertEquals(0x8AD22C, this.teamPrefix().getStyle().getColor().getRgbValue()); ++ assertEquals(0x89D22C, this.teamPrefix().getStyle().getColor().getRgbValue()); + assertEquals("RenamedProfile", player.name()); + + assertNull(updateName(player, "")); diff --git a/patches/viabedrock/upstreamable/0007-forward-local-game-mode-updates.patch b/patches/viabedrock/upstreamable/0007-forward-local-game-mode-updates.patch index 793d302e..95281b47 100644 --- a/patches/viabedrock/upstreamable/0007-forward-local-game-mode-updates.patch +++ b/patches/viabedrock/upstreamable/0007-forward-local-game-mode-updates.patch @@ -1,4 +1,4 @@ -From 9f73e4ed06fd2b203464c82c8eed4b84d37262e5 Mon Sep 17 00:00:00 2001 +From 51de57250252105c4fee3c4d0fba6cdc2bcb4a2d Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:33 +0200 Subject: [PATCH] fix(players): refresh local and inherited game modes @@ -109,7 +109,7 @@ index 82795b85..46d77ccc 100644 infoAddPacket.write(Types.OPTIONAL_TAG, TextUtil.stringToNbt(profile.name())); // display name } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java -index c7ab885f..15efb4c2 100644 +index 14382631..3361cbac 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java @@ -50,6 +50,7 @@ import net.raphimc.viabedrock.protocol.rewriter.GameTypeRewriter; diff --git a/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.patch b/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.patch index 06b59f4f..4b842d69 100644 --- a/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.patch +++ b/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.patch @@ -1,4 +1,4 @@ -From c01ffe7080e8397ec84906c1f63a1f7492efb4b2 Mon Sep 17 00:00:00 2001 +From e91a6b486f4aac421ec8c79679707f2e7bbce523 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:33 +0200 Subject: [PATCH] fix(entities): resolve and order Bedrock passenger links @@ -280,7 +280,7 @@ index d7260f7c..919182e9 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java -index 15efb4c2..15fc3e6d 100644 +index 3361cbac..109beb06 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java @@ -42,6 +42,7 @@ import net.raphimc.viabedrock.protocol.data.enums.java.Relative; diff --git a/patches/viabedrock/upstreamable/0013-translate-remote-player-locations.patch b/patches/viabedrock/upstreamable/0013-translate-remote-player-locations.patch index 245938e5..959e00e5 100644 --- a/patches/viabedrock/upstreamable/0013-translate-remote-player-locations.patch +++ b/patches/viabedrock/upstreamable/0013-translate-remote-player-locations.patch @@ -1,4 +1,4 @@ -From 306d27d95b002198ba9e4b2d2c64294abb0c5885 Mon Sep 17 00:00:00 2001 +From 4f721d506c4ee25af2ad58a0d2851f471a007b5c Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:34 +0200 Subject: [PATCH] feat(hud): preserve native Bedrock locator updates @@ -395,7 +395,7 @@ index 88b9ea57..41c7532f 100644 if (channels.contains(ViaBedrockUtilityInterface.CONFIRM_CHANNEL)) { ViaBedrockUtilityInterface.confirmPresence(wrapper.user()); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java -index 15fc3e6d..9e3e9714 100644 +index 109beb06..a5875895 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java @@ -38,6 +38,7 @@ import net.raphimc.viabedrock.protocol.BedrockProtocol; diff --git a/patches/viabedrock/upstreamable/0015-transport-native-resources-and-sound-commands.patch b/patches/viabedrock/upstreamable/0015-transport-native-resources-and-sound-commands.patch index 3a460193..a9cb6ff7 100644 --- a/patches/viabedrock/upstreamable/0015-transport-native-resources-and-sound-commands.patch +++ b/patches/viabedrock/upstreamable/0015-transport-native-resources-and-sound-commands.patch @@ -1,4 +1,4 @@ -From 8f712e314c70e188a62a63842aea0412304a9043 Mon Sep 17 00:00:00 2001 +From 8aa6e2e8064f13ba8905a68f167e55439a28f651 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:34 +0200 Subject: [PATCH] feat(effects): transport native resources and sound commands @@ -222,10 +222,10 @@ at this first use; later feature patches reuse it. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativeFluidLayersTest.java diff --git a/build.gradle b/build.gradle -index 5d8aa157..0460f912 100644 +index abde84d0..82d1b53d 100644 --- a/build.gradle +++ b/build.gradle -@@ -124,6 +124,7 @@ dependencies { +@@ -125,6 +125,7 @@ dependencies { testImplementation "io.netty:netty-handler:4.2.18.Final" testImplementation "org.junit.jupiter:junit-jupiter:6.1.3" testImplementation sourceSets.tool.output @@ -233,7 +233,7 @@ index 5d8aa157..0460f912 100644 testRuntimeOnly "org.junit.platform:junit-platform-launcher:6.1.3" } -@@ -173,3 +174,9 @@ registerToolTask("sortJson", "net.raphimc.viabedrock.tool.JsonSorter", +@@ -174,3 +175,9 @@ registerToolTask("sortJson", "net.raphimc.viabedrock.tool.JsonSorter", "Sorts data asset json files in place. Options: --file (required), --deep") registerToolTask("convertResourcePack", "net.raphimc.viabedrock.test.ResourcePackConverterTest", "Converts a bedrock .mcpack to a java resource pack. Options: --input (required), --output") diff --git a/patches/viabedrock/upstreamable/0025-apply-dependencies-and-selected-subpacks.patch b/patches/viabedrock/upstreamable/0025-apply-dependencies-and-selected-subpacks.patch index 15543927..f409e769 100644 --- a/patches/viabedrock/upstreamable/0025-apply-dependencies-and-selected-subpacks.patch +++ b/patches/viabedrock/upstreamable/0025-apply-dependencies-and-selected-subpacks.patch @@ -1,4 +1,4 @@ -From 287dc12767e89927f79e85b835624650e3c1cd1f Mon Sep 17 00:00:00 2001 +From d5398165c38fc7cce1791389f9fd5e8d6d428fbf Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:33 +0200 Subject: [PATCH] feat(resource-packs): apply dependencies and selected @@ -224,10 +224,10 @@ index 00000000..6d24866c + +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java -index 21dd17dd..c40530a3 100644 +index b136b605..24331e25 100644 --- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java -@@ -138,7 +138,8 @@ public abstract class Content { +@@ -140,7 +140,8 @@ public abstract class Content { } public LazyImage getShortnameImage(final String path) { diff --git a/patches/viabedrock/upstreamable/0050-decode-bedrock-tga-textures.patch b/patches/viabedrock/upstreamable/0050-decode-bedrock-tga-textures.patch index 6cb4dc18..c86bbdf9 100644 --- a/patches/viabedrock/upstreamable/0050-decode-bedrock-tga-textures.patch +++ b/patches/viabedrock/upstreamable/0050-decode-bedrock-tga-textures.patch @@ -1,4 +1,4 @@ -From c2786b4ed702793b85a3385b888de0b575e1a3f7 Mon Sep 17 00:00:00 2001 +From 7a81a11ac3fedcaf03c12be49403b7723952dd27 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(resource-packs): decode Bedrock TGA textures @@ -13,10 +13,10 @@ Some Bedrock packs supply entity textures as uncompressed or RLE true-color TGA create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/content/TgaContentTest.java diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java -index c40530a3..642bbdbd 100644 +index 24331e25..94504b1f 100644 --- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java -@@ -138,7 +138,7 @@ public abstract class Content { +@@ -140,7 +140,7 @@ public abstract class Content { } public LazyImage getShortnameImage(final String path) { @@ -25,7 +25,7 @@ index c40530a3..642bbdbd 100644 return fullPath == null ? null : this.getImage(fullPath); } -@@ -150,11 +150,12 @@ public abstract class Content { +@@ -152,11 +152,12 @@ public abstract class Content { final boolean isPng = bytes.length > 8 && bytes[0] == (byte) 0x89 && bytes[1] == (byte) 0x50 && bytes[2] == (byte) 0x4E && bytes[3] == (byte) 0x47 && bytes[4] == (byte) 0x0D && bytes[5] == (byte) 0x0A && bytes[6] == (byte) 0x1A && bytes[7] == (byte) 0x0A; final boolean isJpg = bytes.length > 2 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xD8 && bytes[2] == (byte) 0xFF; @@ -40,7 +40,7 @@ index c40530a3..642bbdbd 100644 } public boolean putPngImage(final String path, final LazyImage image) { -@@ -209,6 +210,10 @@ public abstract class Content { +@@ -211,6 +212,10 @@ public abstract class Content { public BufferedImage getImage() { if (this.image == null) { @@ -49,8 +49,8 @@ index c40530a3..642bbdbd 100644 + return this.image; + } try { - this.image = ImageIO.read(new ByteArrayInputStream(this.bytes)); - } catch (final IOException e) { + final var input = new MemoryCacheImageInputStream(new ByteArrayInputStream(this.bytes)); + this.image = ImageIO.read(input); diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/TgaImage.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/TgaImage.java new file mode 100644 index 00000000..66f6d843 diff --git a/patches/viabedrock/upstreamable/0051-render-copper-golem-statue-poses.patch b/patches/viabedrock/upstreamable/0051-render-copper-golem-statue-poses.patch index 2fba5773..ff9b6ffb 100644 --- a/patches/viabedrock/upstreamable/0051-render-copper-golem-statue-poses.patch +++ b/patches/viabedrock/upstreamable/0051-render-copper-golem-statue-poses.patch @@ -1,4 +1,4 @@ -From 922e029b0c766f8330d2664dc74f02c031cb6a04 Mon Sep 17 00:00:00 2001 +From dfd2be33afc91b7feadc24dc7d9c86fb7b019939 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] fix(block-entities): render copper golem statue poses @@ -13,7 +13,7 @@ Bedrock stores copper golem statue pose in the block entity Pose integer while i create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/blockentity/CopperGolemStatueBlockEntityRewriterTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java -index ea378ad0..85dc3b67 100644 +index 5dcfb801..de8da8e0 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java @@ -47,7 +47,7 @@ public final class BlockEntityRewriter { diff --git a/patches/viabedrock/upstreamable/0052-translate-moving-and-extended-pistons.patch b/patches/viabedrock/upstreamable/0052-translate-moving-and-extended-pistons.patch index 0322f146..c024bff6 100644 --- a/patches/viabedrock/upstreamable/0052-translate-moving-and-extended-pistons.patch +++ b/patches/viabedrock/upstreamable/0052-translate-moving-and-extended-pistons.patch @@ -1,4 +1,4 @@ -From 4255c5f2808f938355aaa2b2f233556432bfd6d2 Mon Sep 17 00:00:00 2001 +From 806a22c19079a3bc8487962d2a6b614d2febf044 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] fix(block-entities): translate moving and extended pistons @@ -50,7 +50,7 @@ index 476ebd8c..5eb76980 100644 wrapper.write(Types.VAR_INT, javaBlockEntity.typeId()); // type wrapper.write(Types.COMPOUND_TAG, javaBlockEntity.tag()); // block entity tag diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java -index 85dc3b67..e903b7de 100644 +index de8da8e0..b25875aa 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java @@ -49,8 +49,8 @@ public final class BlockEntityRewriter { diff --git a/patches/viabedrock/upstreamable/0055-use-shulker-facing-from-bedrock-data.patch b/patches/viabedrock/upstreamable/0055-use-shulker-facing-from-bedrock-data.patch index f05e038b..8c247196 100644 --- a/patches/viabedrock/upstreamable/0055-use-shulker-facing-from-bedrock-data.patch +++ b/patches/viabedrock/upstreamable/0055-use-shulker-facing-from-bedrock-data.patch @@ -1,4 +1,4 @@ -From 38cfcd47043aa472952466ad6c21ceed19496cd6 Mon Sep 17 00:00:00 2001 +From 73701a6fa8141963c0facce9d98c76fa1a6fdb1a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] fix(block-entities): use shulker facing from Bedrock data @@ -23,7 +23,7 @@ facing, and subsequent down-facing updates. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/blockentity/ShulkerBoxBlockEntityRewriterTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java -index e903b7de..c7202129 100644 +index b25875aa..9cd6c0ba 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java @@ -93,7 +93,7 @@ public final class BlockEntityRewriter { diff --git a/patches/viabedrock/upstreamable/0058-render-paired-bedrock-chests.patch b/patches/viabedrock/upstreamable/0058-render-paired-bedrock-chests.patch deleted file mode 100644 index 1c36d2f8..00000000 --- a/patches/viabedrock/upstreamable/0058-render-paired-bedrock-chests.patch +++ /dev/null @@ -1,172 +0,0 @@ -From 6ef3773eff13659071502f4bd371de966c02c2b3 Mon Sep 17 00:00:00 2001 -From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> -Date: Sun, 18 Oct 2026 16:55:35 +0200 -Subject: [PATCH] fix(block-entities): render paired Bedrock chests - -Bedrock stores the adjacent half in chest block entity pairx/pairz tags, while its block palette does not include a chest type. Derive the Java left or right state from those coordinates and the mapped facing for regular, trapped, and copper chests. ---- - .../rewriter/BlockEntityRewriter.java | 4 +- - .../blockentity/ChestBlockEntityRewriter.java | 77 +++++++++++++++++++ - .../ChestBlockEntityRewriterTest.java | 45 +++++++++++ - 3 files changed, 124 insertions(+), 2 deletions(-) - create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/rewriter/blockentity/ChestBlockEntityRewriter.java - create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/blockentity/ChestBlockEntityRewriterTest.java - -diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java -index c7202129..9cd6c0ba 100644 ---- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java -+++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockEntityRewriter.java -@@ -65,7 +65,7 @@ public final class BlockEntityRewriter { - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.BRUSHABLE_BLOCK, new BrushableBlockBlockEntityRewriter()); - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.CAMPFIRE, new CampfireBlockEntityRewriter()); - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.CAULDRON, NULL_REWRITER); -- BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.CHEST, new LootableContainerBlockEntityRewriter()); -+ BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.CHEST, new ChestBlockEntityRewriter()); - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.CHISELED_BOOKSHELF, new ChiseledBookshelfBlockEntityRewriter()); - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.COMMAND_BLOCK, new CommandBlockBlockEntityRewriter()); - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.COMPARATOR, new ComparatorBlockEntityRewriter()); -@@ -99,7 +99,7 @@ public final class BlockEntityRewriter { - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.SMOKER, new FurnaceBlockEntityRewriter()); - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.SPORE_BLOSSOM, NULL_REWRITER); - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.STRUCTURE_BLOCK, new StructureBlockBlockEntityRewriter()); -- BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.TRAPPED_CHEST, new LootableContainerBlockEntityRewriter()); -+ BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.TRAPPED_CHEST, new ChestBlockEntityRewriter()); - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.TRIAL_SPAWNER, new TrialSpawnerBlockEntityRewriter()); - BLOCK_ENTITY_REWRITERS.put(CustomBlockTags.VAULT, new VaultBlockEntityRewriter()); - -diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/blockentity/ChestBlockEntityRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/blockentity/ChestBlockEntityRewriter.java -new file mode 100644 -index 00000000..2a2d7acf ---- /dev/null -+++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/blockentity/ChestBlockEntityRewriter.java -@@ -0,0 +1,77 @@ -+/* -+ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock -+ * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors -+ * -+ * This program is free software: you can redistribute it and/or modify -+ * it under the terms of the GNU General Public License as published by -+ * the Free Software Foundation, either version 3 of the License, or -+ * (at your option) any later version. -+ * -+ * This program is distributed in the hope that it will be useful, -+ * but WITHOUT ANY WARRANTY; without even the implied warranty of -+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ * GNU General Public License for more details. -+ * -+ * You should have received a copy of the GNU General Public License -+ * along with this program. If not, see . -+ */ -+package net.raphimc.viabedrock.protocol.rewriter.blockentity; -+ -+import com.viaversion.nbt.tag.CompoundTag; -+import com.viaversion.nbt.tag.IntTag; -+import com.viaversion.viaversion.api.connection.UserConnection; -+import com.viaversion.viaversion.api.minecraft.blockentity.BlockEntity; -+import net.raphimc.viabedrock.api.chunk.BedrockBlockEntity; -+import net.raphimc.viabedrock.api.chunk.BlockEntityWithBlockState; -+import net.raphimc.viabedrock.api.model.BlockState; -+import net.raphimc.viabedrock.protocol.BedrockProtocol; -+import net.raphimc.viabedrock.protocol.storage.ChunkTracker; -+ -+public final class ChestBlockEntityRewriter extends LootableContainerBlockEntityRewriter { -+ -+ @Override -+ public BlockEntity toJava(final UserConnection user, final BedrockBlockEntity bedrockBlockEntity) { -+ final BlockEntity javaBlockEntity = super.toJava(user, bedrockBlockEntity); -+ final int originalId = user.get(ChunkTracker.class).getJavaBlockState(bedrockBlockEntity.position()); -+ final BlockState original = BedrockProtocol.MAPPINGS.getJavaBlockStates().inverse().get(originalId); -+ final BlockState paired = withPair(original, bedrockBlockEntity); -+ if (paired == original) { -+ return javaBlockEntity; -+ } -+ final Integer javaId = BedrockProtocol.MAPPINGS.getJavaBlockStates().get(paired); -+ return javaId == null ? javaBlockEntity : new BlockEntityWithBlockState(javaBlockEntity, javaId); -+ } -+ -+ static BlockState withPair(final BlockState state, final BedrockBlockEntity blockEntity) { -+ if (state == null || !state.properties().containsKey("type") || !state.properties().containsKey("facing")) { -+ return state; -+ } -+ final CompoundTag tag = blockEntity.tag(); -+ if (!(tag.get("pairx") instanceof IntTag pairX) || !(tag.get("pairz") instanceof IntTag pairZ)) { -+ return state; -+ } -+ -+ final long dx = (long) pairX.asInt() - blockEntity.position().x(); -+ final long dz = (long) pairZ.asInt() - blockEntity.position().z(); -+ if (Math.abs(dx) + Math.abs(dz) != 1) { -+ return state; -+ } -+ -+ final boolean left = switch (state.properties().get("facing")) { -+ case "north" -> dx == 1; -+ case "east" -> dz == 1; -+ case "south" -> dx == -1; -+ case "west" -> dz == -1; -+ default -> false; -+ }; -+ final boolean right = switch (state.properties().get("facing")) { -+ case "north" -> dx == -1; -+ case "east" -> dz == -1; -+ case "south" -> dx == 1; -+ case "west" -> dz == 1; -+ default -> false; -+ }; -+ return left ? state.withProperty("type", "left") : right ? state.withProperty("type", "right") : state; -+ } -+ -+} -diff --git a/src/test/java/net/raphimc/viabedrock/protocol/rewriter/blockentity/ChestBlockEntityRewriterTest.java b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/blockentity/ChestBlockEntityRewriterTest.java -new file mode 100644 -index 00000000..9d27bf12 ---- /dev/null -+++ b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/blockentity/ChestBlockEntityRewriterTest.java -@@ -0,0 +1,45 @@ -+package net.raphimc.viabedrock.protocol.rewriter.blockentity; -+ -+import com.viaversion.nbt.tag.CompoundTag; -+import com.viaversion.viaversion.api.minecraft.BlockPosition; -+import net.raphimc.viabedrock.api.chunk.BedrockBlockEntity; -+import net.raphimc.viabedrock.api.model.BlockState; -+import org.junit.jupiter.api.Test; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+ -+class ChestBlockEntityRewriterTest { -+ -+ @Test -+ void pairsAllChestVariantsOnEitherSide() { -+ for (String chest : new String[]{"chest", "trapped_chest", "copper_chest"}) { -+ for (String facing : new String[]{"north", "east", "south", "west"}) { -+ final BlockState state = BlockState.fromString("minecraft:" + chest + "[facing=" + facing + ",type=single,waterlogged=false]"); -+ final int[] leftOffset = switch (facing) { -+ case "north" -> new int[]{1, 0}; -+ case "east" -> new int[]{0, 1}; -+ case "south" -> new int[]{-1, 0}; -+ default -> new int[]{0, -1}; -+ }; -+ assertEquals(state.withProperty("type", "left"), ChestBlockEntityRewriter.withPair(state, chestAt(leftOffset[0], leftOffset[1]))); -+ assertEquals(state.withProperty("type", "right"), ChestBlockEntityRewriter.withPair(state, chestAt(-leftOffset[0], -leftOffset[1]))); -+ } -+ } -+ } -+ -+ @Test -+ void ignoresMissingAndNonAdjacentPairs() { -+ final BlockState state = BlockState.fromString("minecraft:chest[facing=north,type=single,waterlogged=false]"); -+ assertEquals(state, ChestBlockEntityRewriter.withPair(state, new BedrockBlockEntity(new BlockPosition(10, 64, 20), new CompoundTag()))); -+ assertEquals(state, ChestBlockEntityRewriter.withPair(state, chestAt(2, 0))); -+ assertEquals(state, ChestBlockEntityRewriter.withPair(state, chestAt(0, 1))); -+ } -+ -+ private static BedrockBlockEntity chestAt(final int dx, final int dz) { -+ final CompoundTag tag = new CompoundTag(); -+ tag.putInt("pairx", 10 + dx); -+ tag.putInt("pairz", 20 + dz); -+ return new BedrockBlockEntity(new BlockPosition(10, 64, 20), tag); -+ } -+ -+} --- -2.56.0 diff --git a/patches/viabedrock/upstreamable/0058-render-paired-bedrock-chests.pr.md b/patches/viabedrock/upstreamable/0058-render-paired-bedrock-chests.pr.md deleted file mode 100644 index 4536485c..00000000 --- a/patches/viabedrock/upstreamable/0058-render-paired-bedrock-chests.pr.md +++ /dev/null @@ -1,18 +0,0 @@ -## Purpose - -Render paired Bedrock chests with the correct Java left and right block states. -Bedrock supplies the adjacent half through block-entity `pairx` and `pairz` coordinates. -Its block palette does not supply the Java chest type. - -## Review focus - -Apply the pairing coordinates relative to the mapped chest facing. -Preserve the existing lootable-container data and the fallback for missing, invalid, or non-adjacent pairs. -The state conversion covers regular, trapped, and copper chests. -Inventory capacity and request handling remain separate from this rendering change. - -## Validation - -Local regression fixtures cover both sides, every horizontal facing, chest variants, and invalid pairing coordinates. -Those tests stay in the local stack and do not form part of the upstream PR. -Build the filtered production patch independently against the pinned upstream base before publication. diff --git a/patches/viabedrock/upstreamable/0061-expose-native-player-emotes.patch b/patches/viabedrock/upstreamable/0061-expose-native-player-emotes.patch index 89ff49e3..818bfc85 100644 --- a/patches/viabedrock/upstreamable/0061-expose-native-player-emotes.patch +++ b/patches/viabedrock/upstreamable/0061-expose-native-player-emotes.patch @@ -1,4 +1,4 @@ -From 5444704220c8087544703b8bf128cb1818023bc8 Mon Sep 17 00:00:00 2001 +From ff529c403bce7bc6b0183cf7307a91a0056f1709 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(players): expose native emotes to client integrations @@ -383,7 +383,7 @@ index 954469d8..6b1f5dd0 100644 if (channels.contains(LocatorBarSnapshot.CHANNEL) && wrapper.user().has(PlayerWaypointTracker.class)) { wrapper.user().get(PlayerWaypointTracker.class).sendSnapshot(); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java -index 9e3e9714..506c457e 100644 +index a5875895..60aeadba 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java @@ -48,6 +48,7 @@ import net.raphimc.viabedrock.protocol.model.PlayerAbilities; diff --git a/patches/viabedrock/upstreamable/0064-evaluate-named-actor-properties.pr.md b/patches/viabedrock/upstreamable/0064-evaluate-named-actor-properties.pr.md index 75c426cb..82affb3c 100644 --- a/patches/viabedrock/upstreamable/0064-evaluate-named-actor-properties.pr.md +++ b/patches/viabedrock/upstreamable/0064-evaluate-named-actor-properties.pr.md @@ -12,7 +12,6 @@ Prerequisites: - viabedrock/0064-share-native-float-molang-runtime.patch: Land the bounded float runtime before actor property evaluation. -- cubeconverter/0002-preserve-actor-lighting-controls.patch: Publish ordered material assignments, including repeated patterns, before evaluating actor materials. ## Validation and remaining limits diff --git a/patches/viabedrock/upstreamable/0064-share-native-float-molang-runtime.patch b/patches/viabedrock/upstreamable/0064-share-native-float-molang-runtime.patch index d8cca41d..ffe2b3da 100644 --- a/patches/viabedrock/upstreamable/0064-share-native-float-molang-runtime.patch +++ b/patches/viabedrock/upstreamable/0064-share-native-float-molang-runtime.patch @@ -1,4 +1,4 @@ -From 590142971482435cd09a197bf98dd2a3a19ad160 Mon Sep 17 00:00:00 2001 +From 9a1f9fd7cfbe10f6f15bb374efcfe130d26524b3 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(molang): share bounded native float evaluation @@ -77,11 +77,11 @@ its expected result. create mode 100644 src/test/java/net/raphimc/viabedrock/api/util/MoLangMathTest.java diff --git a/build.gradle b/build.gradle -index 0460f912..c7655913 100644 +index 82d1b53d..ac5f09a5 100644 --- a/build.gradle +++ b/build.gradle -@@ -111,8 +111,8 @@ dependencies { - api(cubeConverterDependency) { +@@ -112,8 +112,8 @@ dependencies { + api("com.github.oryxel1:CubeConverter:8a7b73a8ed") { transitive = false } - api("team.unnamed:mocha:3.0.1") { diff --git a/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.patch b/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.patch index c9f153aa..c582537b 100644 --- a/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.patch +++ b/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.patch @@ -1,4 +1,4 @@ -From 8f25f6e7e0e25460bb2c8bf054487cbc4d9c8b91 Mon Sep 17 00:00:00 2001 +From dc5d721e6df4f5ea1f8e5550c877c844231be960 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] feat(resource-packs): select native resources by world @@ -77,7 +77,7 @@ complete rendering or audio parity. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/provider/BundledImagePacksTest.java diff --git a/build.gradle b/build.gradle -index c7655913..3643f0c8 100644 +index ac5f09a5..9d621901 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,17 @@ tasks.withType(Jar).configureEach { diff --git a/patches/viabedrock/upstreamable/0072-convert-native-sidebar-title-images-and-styled-scores.patch b/patches/viabedrock/upstreamable/0072-convert-native-sidebar-title-images-and-styled-scores.patch index aac14232..a91e32d7 100644 --- a/patches/viabedrock/upstreamable/0072-convert-native-sidebar-title-images-and-styled-scores.patch +++ b/patches/viabedrock/upstreamable/0072-convert-native-sidebar-title-images-and-styled-scores.patch @@ -1,8 +1,11 @@ -From 06dd591d5f9a6ccf499f6f50fb41ffdd07ee69ef Mon Sep 17 00:00:00 2001 +From 543cf8710d7fb0aafe19811905bda64475d8f51a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] feat(scoreboard): convert native sidebar title images and styled scores +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit Bedrock 1.26.51 resource packs can replace the objective title with a JSON UI image binding. Convert a bounded subset with literal title equality, a matching hidden label, explicit dimensions, and native sidebar anchors. Resolve active server UI and texture priority, isolate each image in its own font, and advertise authored dimensions for client integrations. Unsupported controls retain ordinary text. @@ -26,11 +29,15 @@ Preserve the authored sidebar image box and transport source dimensions with kee Bedrock 1.26.51.1 enables keep_ratio by default. Its image loader selects flag 8 when the property is absent, and its draw path fits the source aspect inside the destination. The captured CubeCraft logo is 360 by 94 pixels inside a 66 by 21 box; its fitted height is about 17.23 units. Tests cover default aspect preservation, explicit stretching, metadata dimensions, and transparent fallback padding. + +Bedrock 1.26.60.5 glyph batching at 140e6d97f..140e6da30 multiplies float channels by 255, truncates toward zero and clamps before packing. Original instructions map the native red palette [1,.333,.333] to FF5454 and [.5,.1,1] to 7F19FF. Match that quantization for effective formatting colors instead of rounding to a neighboring byte. The installed native label control retains the resulting red glyph pixels. + +Read the full effective palette from the UI program’s merged globals separately from server-only protocol-text overrides. The native default red [1,.333,.333] differs from the Java legacy default even without a server override. Keep each palette immutable and preserve pack priority; do not apply UI-only default colors to protocol text. --- .../api/model/entity/PlayerEntity.java | 2 +- .../model/scoreboard/ScoreboardObjective.java | 22 +- .../definition/ScoreboardUiDefinitions.java | 430 ++++++++++++++++++ - .../definition/TextFormattingDefinitions.java | 109 +++++ + .../definition/TextFormattingDefinitions.java | 126 +++++ .../http/ConvertedResourcePackCache.java | 29 +- .../raphimc/viabedrock/api/util/TextUtil.java | 30 +- .../protocol/packet/HudPackets.java | 38 +- @@ -39,11 +46,11 @@ Tests cover default aspect preservation, explicit stretching, metadata dimension .../protocol/storage/ResourcePackStorage.java | 12 + .../ScoreboardPresentationTest.java | 72 +++ .../ScoreboardUiDefinitionsTest.java | 353 ++++++++++++++ - .../TextFormattingDefinitionsTest.java | 119 +++++ + .../TextFormattingDefinitionsTest.java | 121 +++++ .../http/ConvertedResourcePackCacheTest.java | 32 ++ .../api/util/TextPaletteFormattingTest.java | 39 ++ .../protocol/packet/HudPacketsTest.java | 39 ++ - 16 files changed, 1433 insertions(+), 21 deletions(-) + 16 files changed, 1452 insertions(+), 21 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/ScoreboardUiDefinitions.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/TextFormattingDefinitions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/ScoreboardUiResourceRewriter.java @@ -562,10 +569,10 @@ index 00000000..acd07aa2 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/TextFormattingDefinitions.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/TextFormattingDefinitions.java new file mode 100644 -index 00000000..72539af5 +index 00000000..0f94de2b --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/TextFormattingDefinitions.java -@@ -0,0 +1,109 @@ +@@ -0,0 +1,126 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors @@ -595,13 +602,17 @@ index 00000000..72539af5 +import java.util.Map; +import java.util.Set; + -+/** Static formatting-color overrides from the effective server UI globals. */ ++/** Native UI colors and server formatting overrides from effective pack globals. */ +public final class TextFormattingDefinitions { + + private static final Map COLOR_VARIABLES = colorVariables(); + private final Map colors; + + public TextFormattingDefinitions(final ResourcePackStorage storage) { ++ this.colors = readColors(storage); ++ } ++ ++ private static Map readColors(final ResourcePackStorage storage) { + final Map colors = new HashMap<>(); + final Set seen = new HashSet<>(); + for (final ResourcePack pack : storage.getPackStackTopToBottom()) { @@ -625,7 +636,20 @@ index 00000000..72539af5 + // Dynamic variables and malformed files are outside this static subset. + } + } -+ this.colors = Map.copyOf(colors); ++ return Map.copyOf(colors); ++ } ++ ++ /** Reads the UI program's merged globals, including its bundled defaults. */ ++ public static Map uiColors(final Map globals) { ++ final Map colors = new HashMap<>(); ++ COLOR_VARIABLES.forEach((code, variable) -> { ++ final JsonElement value = globals.get(variable); ++ final Integer color = value == null ? null : rgb(value); ++ if (color != null) { ++ colors.put(code, color); ++ } ++ }); ++ return Map.copyOf(colors); + } + + public Map colors() { @@ -669,7 +693,7 @@ index 00000000..72539af5 + if (!Float.isFinite(n) || n < 0F || n > 1F) { + return null; + } -+ rgb = rgb << 8 | Math.round(n * 255F); ++ rgb = rgb << 8 | (int) (n * 255F); + } + return rgb; + } @@ -1536,10 +1560,10 @@ index 00000000..0a21d906 +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/definition/TextFormattingDefinitionsTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/definition/TextFormattingDefinitionsTest.java new file mode 100644 -index 00000000..6f0ed29b +index 00000000..b632cad7 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/definition/TextFormattingDefinitionsTest.java -@@ -0,0 +1,119 @@ +@@ -0,0 +1,121 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors @@ -1576,17 +1600,19 @@ index 00000000..6f0ed29b +class TextFormattingDefinitionsTest { + + @Test -+ void effectiveColorsUsePackPriorityAndNearestRgbWithoutChangingDefaultFormatting() { ++ void effectiveColorsUsePackPriorityAndNativeChannelTruncation() { + final var lower = new JsonObject(); + lower.add("$9_color_format", rgb(1, 0, 0)); + lower.add("$a_color_format", rgb(0, 1, 0)); + lower.add("$b_color_format", rgb(0.5, 0.1, 1)); ++ lower.add("$c_color_format", rgb(1, 0.333, 0.333)); + final var higher = new JsonObject(); + higher.add("$9_color_format", rgb(0.22745098, 0.662745098, 1)); + final var colors = new ResourcePackStorage(List.of(pack(higher), pack(lower))).getTextFormatting().colors(); + assertEquals(0x3AA9FF, colors.get('9')); + assertEquals(0x00FF00, colors.get('a')); -+ assertEquals(0x801AFF, colors.get('b')); ++ assertEquals(0x7F19FF, colors.get('b')); ++ assertEquals(0xFF5454, TextUtil.stringToTextComponent("§cRed", colors).getStyle().getColor().getRgbValue()); + assertEquals(0x3AA9FF, TextUtil.stringToTextComponent("§9Blue", colors).getStyle().getColor().getRgbValue()); + assertEquals(0x5555FF, TextUtil.stringToTextComponent("§9Blue").getStyle().getColor().getRgbValue()); + assertTrue(TextUtil.stringToTextComponent("§l§9Blue", colors).getStyle().isBold()); @@ -1620,10 +1646,10 @@ index 00000000..6f0ed29b + final var higher = new JsonObject(); + higher.add("$material_gold_color", rgb(0.541, 0.824, 0.173)); + final var colors = new ResourcePackStorage(List.of(pack(higher), pack(lower))).getTextFormatting().colors(); -+ assertEquals(0x8AD22C, colors.get('p')); ++ assertEquals(0x89D22C, colors.get('p')); + assertEquals(0x0000FF, colors.get('q')); -+ assertEquals(0x8AD22C, TextUtil.stringToTextComponent("§pMaterial", colors).getStyle().getColor().getRgbValue()); -+ assertEquals(0x8AD22C, TextUtil.stringToTextComponent("§PMaterial", colors).getStyle().getColor().getRgbValue()); ++ assertEquals(0x89D22C, TextUtil.stringToTextComponent("§pMaterial", colors).getStyle().getColor().getRgbValue()); ++ assertEquals(0x89D22C, TextUtil.stringToTextComponent("§PMaterial", colors).getStyle().getColor().getRgbValue()); + assertEquals(0xDEB12D, TextUtil.stringToTextComponent("§pMaterial").getStyle().getColor().getRgbValue()); + } + diff --git a/patches/viabedrock/upstreamable/0072-convert-native-sidebar-title-images-and-styled-scores.pr.md b/patches/viabedrock/upstreamable/0072-convert-native-sidebar-title-images-and-styled-scores.pr.md index dca12a64..4c1ea101 100644 --- a/patches/viabedrock/upstreamable/0072-convert-native-sidebar-title-images-and-styled-scores.pr.md +++ b/patches/viabedrock/upstreamable/0072-convert-native-sidebar-title-images-and-styled-scores.pr.md @@ -1,6 +1,6 @@ CubeCraft updates an existing sidebar objective from a text title to a pack image. The previous translation discarded that title update. -Preserve objective and score identities, emit title changes, hide sidebar numbers with Java BlankFormat, and send styled row components. Apply finite static ordinary and named material color overrides from effective server UI globals. CubeCraft supplies §9 as RGB 58,169,255. Convert float colors to the nearest representable Java RGB; native reverse lookup does not establish framebuffer rounding. +Preserve objective and score identities, emit title changes, hide sidebar numbers with Java BlankFormat, and send styled row components. Apply finite static ordinary and named material color overrides from effective server UI globals. CubeCraft supplies §9 as RGB 58,169,255. Native Bedrock 1.26.60.5 glyph batching multiplies float channels by 255 and truncates them to bytes. Original instruction controls establish this conversion. Convert bounded active scoreboard UI image definitions into dedicated fonts, original textures, and metadata for the add-on. The recorded protocol 2193 CubeCraft pack declares a 66×21 title image and 10-pixel rows. Unsupported dynamic UI definitions retain text fallback. Bump the converted-pack cache version so old receipts cannot omit these assets. @@ -14,4 +14,4 @@ Keep global palette parsing in `TextFormattingDefinitions`, separate from scoreb Preserve active bold and italic styles across palette color codes, including codes the dependency does not know. Entity metadata tests exercise player and custom actor names, sparse updates, blank names, independent connection palettes, and immutable emitted components. Pack storage finishes before supported entity creation; the protocol rejects replacement pack information within an existing connection. -The converted-pack identity advances to version 10 after correcting equipped attachable selectors and adding cutout-plane back faces. This prevents earlier cached exports with doubled variant names or missing plant faces from reaching the client. Cache regressions cover version rollover and reuse. +Native UI programs read their complete merged palette, including bundled defaults. Protocol text retains server-only overrides. Regressions cover channel truncation, pack priority, program transport, and protocol fallback. diff --git a/patches/viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.patch b/patches/viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.patch deleted file mode 100644 index 58675566..00000000 --- a/patches/viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.patch +++ /dev/null @@ -1,143 +0,0 @@ -From c3f35c9cc09f69b3cd8293779d565fa4e73e62a8 Mon Sep 17 00:00:00 2001 -From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> -Date: Sun, 18 Oct 2026 16:55:36 +0200 -Subject: [PATCH] fix(players): encode head rotation in Java 26.3 field order - -Java 26.3 ClientboundMoveEntityPacket.Rot.STREAM_CODEC reads and writes -entity ID, on-ground boolean, yaw byte, then pitch byte. The Bedrock -OnlyHeadRot translation retained the older order with on-ground last, -so the client consumed the source pitch as yaw and the boolean as pitch. -Actual Hive seated-player observations confirmed the failure: a captured -25.15747-degree pitch became a 23.90625-degree Java yaw, while on-ground -true became a 1.40625-degree pitch. This changes only the Java wire order; -retained body/head rotation, position and ground-state semantics stay as -upstream currently implements them. ---- - .../protocol/packet/OtherPlayerPackets.java | 2 +- - .../packet/PlayerHeadRotationPacketTest.java | 100 ++++++++++++++++++ - 2 files changed, 101 insertions(+), 1 deletion(-) - create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerHeadRotationPacketTest.java - -diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java -index 506c457e..60aeadba 100644 ---- a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java -+++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java -@@ -182,9 +182,9 @@ public final class OtherPlayerPackets { - entity.setRotation(new Position3f(rotation.x(), entity.rotation().y(), entity.rotation().z())); - wrapper.setPacketType(ClientboundPackets26_3.MOVE_ENTITY_ROT); - wrapper.write(Types.VAR_INT, entity.javaId()); // entity id -+ wrapper.write(Types.BOOLEAN, entity.isOnGround()); // on ground - wrapper.write(Types.BYTE, MathUtil.float2Byte(entity.rotation().y())); // yaw - wrapper.write(Types.BYTE, MathUtil.float2Byte(rotation.x())); // pitch -- wrapper.write(Types.BOOLEAN, entity.isOnGround()); // on ground - return; - } - -diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerHeadRotationPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerHeadRotationPacketTest.java -new file mode 100644 -index 00000000..5103f06c ---- /dev/null -+++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerHeadRotationPacketTest.java -@@ -0,0 +1,100 @@ -+/* -+ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock -+ * Copyright (C) 2026 RK_01/RaphiMC and contributors -+ * -+ * This program is free software: you can redistribute it and/or modify -+ * it under the terms of the GNU General Public License as published by -+ * the Free Software Foundation, either version 3 of the License, or -+ * (at your option) any later version. -+ */ -+package net.raphimc.viabedrock.protocol.packet; -+ -+import com.viaversion.viaversion.api.connection.UserConnection; -+import com.viaversion.viaversion.api.minecraft.entities.EntityTypes26_3; -+import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; -+import com.viaversion.viaversion.api.protocol.packet.State; -+import com.viaversion.viaversion.api.type.Types; -+import com.viaversion.viaversion.protocol.packet.PacketWrapperImpl; -+import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets26_3; -+import io.netty.buffer.ByteBuf; -+import io.netty.buffer.Unpooled; -+import net.raphimc.viabedrock.api.model.entity.Entity; -+import net.raphimc.viabedrock.protocol.BedrockProtocol; -+import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; -+import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.PositionMode; -+import net.raphimc.viabedrock.protocol.model.Position3f; -+import net.raphimc.viabedrock.protocol.storage.EntityTracker; -+import net.raphimc.viabedrock.protocol.types.BedrockTypes; -+import org.junit.jupiter.params.ParameterizedTest; -+import org.junit.jupiter.params.provider.ValueSource; -+ -+import java.lang.reflect.Proxy; -+import java.util.UUID; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+ -+class PlayerHeadRotationPacketTest { -+ -+ @ParameterizedTest -+ @ValueSource(booleans = {false, true}) -+ void headOnlyMovementEncodesTheJavaRotationCodecOrder(final boolean onGround) throws Exception { -+ final EntityTracker[] trackers = new EntityTracker[1]; -+ final UserConnection user = (UserConnection) Proxy.newProxyInstance(UserConnection.class.getClassLoader(), -+ new Class[]{UserConnection.class}, (proxy, method, args) -> -+ method.getName().equals("get") && args[0] == EntityTracker.class ? trackers[0] : null); -+ final EntityTracker tracker = trackers[0] = new EntityTracker(user); -+ final Entity player = tracker.addEntity(new Entity(user, 21, 101, "minecraft:player", 301, -+ UUID.randomUUID(), EntityTypes26_3.PLAYER), false); -+ final Position3f position = new Position3f(1F, 65F, 3F); -+ player.setPosition(position); -+ player.setRotation(new Position3f(0F, 90F, -45F)); -+ player.setOnGround(onGround); -+ -+ final ByteBuf input = Unpooled.buffer(); -+ final ByteBuf output = Unpooled.buffer(); -+ try { -+ BedrockTypes.UNSIGNED_VAR_LONG.write(input, player.runtimeId()); -+ BedrockTypes.POSITION_3F.write(input, new Position3f(20F, 80F, 40F)); -+ BedrockTypes.POSITION_3F.write(input, new Position3f(-22.5F, 180F, 135F)); -+ Types.BYTE.write(input, (byte) PositionMode.OnlyHeadRot.getValue()); -+ Types.BOOLEAN.write(input, !onGround); -+ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 0L); -+ Types.BOOLEAN.write(input, false); -+ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 123L); -+ final PacketWrapper packet = new PacketWrapperImpl(ClientboundBedrockPackets.MOVE_PLAYER, input, user); -+ -+ new TestProtocol().translate(packet); -+ packet.writeToBuffer(output); -+ -+ assertEquals(ClientboundPackets26_3.MOVE_ENTITY_ROT.getId(), Types.VAR_INT.read(output)); -+ // Java 26.3 ClientboundMoveEntityPacket.Rot reads id, onGround, yaw, pitch. -+ assertEquals(player.javaId(), Types.VAR_INT.read(output)); -+ assertEquals(onGround, Types.BOOLEAN.read(output)); -+ assertEquals((byte) 64, Types.BYTE.read(output)); -+ assertEquals((byte) -16, Types.BYTE.read(output)); -+ assertEquals(0, output.readableBytes()); -+ assertEquals(0, input.readableBytes()); -+ assertEquals(position, player.position()); -+ assertEquals(new Position3f(-22.5F, 90F, -45F), player.rotation()); -+ assertEquals(onGround, player.isOnGround()); -+ } finally { -+ input.release(); -+ output.release(); -+ } -+ } -+ -+ private static final class TestProtocol extends BedrockProtocol { -+ -+ private TestProtocol() { -+ OtherPlayerPackets.register(this); -+ } -+ -+ private void translate(final PacketWrapper packet) throws Exception { -+ final var mapping = this.clientboundMappings.mappedPacket(State.PLAY, packet.getId()); -+ mapping.applyType(packet); -+ mapping.handler().handle(packet); -+ } -+ -+ } -+ -+} --- -2.56.0 diff --git a/patches/viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md b/patches/viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md deleted file mode 100644 index 91066440..00000000 --- a/patches/viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md +++ /dev/null @@ -1,15 +0,0 @@ -# Correct Java 26.3 head rotation packets - -Bedrock head-only movement produced corrupted Java player rotations. Java 26.3 reads the on-ground boolean before yaw and pitch. ViaBedrock wrote it after both angles. - -The patch corrects the Java packet field order and preserves the Bedrock state handling. Two registered-handler cases serialize the output and decode the current Java wire layout. Both ground-state cases pass in the current verified build. - -## Evidence and remaining limits - -The earlier Hive control uses Bedrock 1.26.51.1 on protocol 2193. The gray seated player's source pitch became its Java yaw, and its ground flag became a 1.40625-degree pitch. A fixed local replay restores the expected draw-call angles within the existing byte-angle quantization. This evidence establishes the wire-order error without a skin mesh change. - -Current controls use official Bedrock 1.26.60.5/BDS 1.26.60.6 on protocol 2225. After a same-identity yaw cycle, a mode-3 packet supplies a 22.5-degree pitch, conflicting yaw and head yaw, and a different position. Native applies the pitch while retaining the earlier position and yaw. The current direct and ViaProxy clients reproduce this selected result. Their runtime audits retain position, actor yaw, and head yaw while pitch changes to 22.5 degrees. A normal packet restores the level head pose. - -Strict decoders verify identical source spawn and movement payloads across the routes. The accepted Java fixtures disable their onboarding tutorial before observations. The [shared player-runtime controls](../../viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md) describe the complete sequence, marker measurements, and cleanup. - -This stationary, unmounted control supports the retained pitch-only handling for the selected inputs. Mounted head/body rules, native ground-state parity, interpolation timing, and complete skin animation parity remain open. Private captures, client assets, and runtime traces stay outside Git. No packet-semantic change follows from these controls. diff --git a/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.patch b/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.patch index afd75c48..c08d244e 100644 --- a/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.patch +++ b/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.patch @@ -1,4 +1,4 @@ -From 09deabe74c8034104bd32d97854cc9548dbc037c Mon Sep 17 00:00:00 2001 +From f130f3fc7f0ae874dc41314f7f11275a57747b44 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] fix(network): finish transport-initiated RakNet disconnects @@ -30,11 +30,11 @@ code; its build prerequisite is recorded in the patch series. create mode 100644 src/test/java/net/raphimc/viabedrock/netty/DisconnectHandlerTest.java diff --git a/build.gradle b/build.gradle -index 46fdf3f8..065fdf4f 100644 +index 9d621901..79f3bbca 100644 --- a/build.gradle +++ b/build.gradle -@@ -40,6 +40,16 @@ def cubeConverterDependency = stackAnvilMavenRepo.present - : "com.github.oryxel1:CubeConverter:7cf736c4a5" +@@ -52,6 +52,16 @@ if (stackAnvilMavenRepo.present) { + } repositories { + if (findByName("OpenCollab Snapshots") == null) { @@ -50,7 +50,7 @@ index 46fdf3f8..065fdf4f 100644 if (stackAnvilMavenRepo.present) { maven { name = "StackAnvilLocal" -@@ -80,6 +90,8 @@ dependencies { +@@ -92,6 +102,8 @@ dependencies { compileOnly "com.google.guava:guava:33.7.2-jre" compileOnly "io.netty:netty-handler:4.2.19.Final" compileOnly "com.google.code.gson:gson:2.15.0" diff --git a/patches/viabedrock/upstreamable/0085-resolve-server-effects-relative-to-their-actor.patch b/patches/viabedrock/upstreamable/0085-resolve-server-effects-relative-to-their-actor.patch index fe9704df..34909527 100644 --- a/patches/viabedrock/upstreamable/0085-resolve-server-effects-relative-to-their-actor.patch +++ b/patches/viabedrock/upstreamable/0085-resolve-server-effects-relative-to-their-actor.patch @@ -1,4 +1,4 @@ -From 96b927906902fd0fc9d40807b538917e63425c1b Mon Sep 17 00:00:00 2001 +From 46f372c135a9357aeb7bdc51198605f37ea296bc Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] feat(particles): route server effects through native playback @@ -77,7 +77,7 @@ client bindings. Direct and ViaProxy share the same core hash implementation. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/SpawnParticleEffectTest.java diff --git a/build.gradle b/build.gradle -index e01266de..e8d20827 100644 +index 79f3bbca..b97edf35 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,12 @@ checkstyleMain { diff --git a/patches/viabedrock/upstreamable/0087-inherit-client-entity-descriptions-across-layers.patch b/patches/viabedrock/upstreamable/0087-inherit-client-entity-descriptions-across-layers.patch index b8d9bedd..0a8aa9d5 100644 --- a/patches/viabedrock/upstreamable/0087-inherit-client-entity-descriptions-across-layers.patch +++ b/patches/viabedrock/upstreamable/0087-inherit-client-entity-descriptions-across-layers.patch @@ -1,4 +1,4 @@ -From f2b98a1ea551d2c12dcdd0a7f2150aec76311569 Mon Sep 17 00:00:00 2001 +From 52ea1590fa191890bb4d385354306817266c83a1 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] fix(resource-packs): inherit client entity descriptions @@ -17,10 +17,10 @@ Expose the same immutable description merger for licensed appearance readers. Th create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/definition/EntityDefinitionsTest.java diff --git a/build.gradle b/build.gradle -index e8d20827..671ad639 100644 +index b97edf35..2d2fe8b9 100644 --- a/build.gradle +++ b/build.gradle -@@ -91,6 +91,13 @@ repositories { +@@ -104,6 +104,13 @@ repositories { dependencies { compileOnly "com.viaversion:viaversion-common:5.12.0" @@ -34,7 +34,7 @@ index e8d20827..671ad639 100644 compileOnly "org.yaml:snakeyaml:2.7" compileOnly "com.google.guava:guava:33.7.2-jre" compileOnly "io.netty:netty-handler:4.2.19.Final" -@@ -160,6 +167,10 @@ def registerToolTask(String name, String toolMainClass, String toolDescription) +@@ -172,6 +179,10 @@ def registerToolTask(String name, String toolMainClass, String toolDescription) } } diff --git a/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.patch b/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.patch index f56315c9..5d8f148b 100644 --- a/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.patch +++ b/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.patch @@ -1,4 +1,4 @@ -From 436ad0fd90ba7b35674c167180fa0f53dd98cfe7 Mon Sep 17 00:00:00 2001 +From 0a157c17726ded1c35797a6ea4025b52fcfc65bb Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] fix(nethernet): accept BDS capability responses without @@ -41,10 +41,10 @@ Keep the established compile API version compatible with both host routes. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/AuthDataTest.java diff --git a/build.gradle b/build.gradle -index 10525c72..bc575a6e 100644 +index 2d2fe8b9..3df21a26 100644 --- a/build.gradle +++ b/build.gradle -@@ -104,6 +104,7 @@ dependencies { +@@ -116,6 +116,7 @@ dependencies { compileOnly "io.netty:netty-handler:4.2.19.Final" compileOnly "com.google.code.gson:gson:2.15.0" compileOnly "org.cloudburstmc.netty:netty-transport-raknet:2.0.0.CR4-20260921.144528-1" diff --git a/patches/viabedrock/upstreamable/0092-retain-completed-client-prediction-frames.patch b/patches/viabedrock/upstreamable/0092-retain-completed-client-prediction-frames.patch index cc266b96..c444e27e 100644 --- a/patches/viabedrock/upstreamable/0092-retain-completed-client-prediction-frames.patch +++ b/patches/viabedrock/upstreamable/0092-retain-completed-client-prediction-frames.patch @@ -1,4 +1,4 @@ -From 3bb6e1edc1a46357bd3e7e02243b81528f1ad1b2 Mon Sep 17 00:00:00 2001 +From dc82fa3bb6039581f82c418785cae45d92bdd058 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] feat(movement): retain completed client prediction frames @@ -853,7 +853,7 @@ This comparison excludes physical snapshot consumption and world effects. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/TickedMotionPacketTest.java diff --git a/build.gradle b/build.gradle -index 671ad639..2fd72ae4 100644 +index 3df21a26..851110da 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,21 @@ checkstyleMain { @@ -878,7 +878,7 @@ index 671ad639..2fd72ae4 100644 configurations.testImplementation.extendsFrom configurations.compileOnly testing.suites.test { -@@ -209,3 +224,17 @@ configurations.testImplementation.extendsFrom configurations.compileOnly +@@ -222,3 +237,17 @@ configurations.testImplementation.extendsFrom configurations.compileOnly testing.suites.test { useJUnitJupiter("6.1.3") } diff --git a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch index 17b7324b..3236a783 100644 --- a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch +++ b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch @@ -1,4 +1,4 @@ -From 95b41a4a5c2de68bb8cbdb08157f8ec649bbbd18 Mon Sep 17 00:00:00 2001 +From d4c9c95d838679a0b24abfc2bed187bd3f97b126 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(ui): resolve authored Bedrock server UI scenes @@ -857,7 +857,7 @@ index 9d80118a..7c7f68ae 100644 +These are definition-owned keys, separate from item factory identity, runtime IDs, auxiliary state resolution, and localization. +Asset-defined blocks and custom display-name components remain outside this table. diff --git a/build.gradle b/build.gradle -index 8e9eab8b..dfec773e 100644 +index 6493daf2..5ef41e2a 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,7 @@ dependencies { diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch index 003fa55f..935a4667 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch @@ -1,4 +1,4 @@ -From 0483e2cf00dcd4739a0a7775de6e770976d175b6 Mon Sep 17 00:00:00 2001 +From a09bd53e920475b760bc5e4e48c581bb673881af Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(ui): share bounded native UI programs and resource @@ -535,6 +535,30 @@ font. An unavailable suffix after NUL does not force fallback. Truncate the translated display property before component and font resolution, while retaining the controller's original binding values and declarations. The native controls include newline, Unicode and formatting suffixes. + +Selected TrueType labels carry the native 1.5 display multiplier separately from authored font size so clients snap the label origin on the same grid as the glyphs. Compute it after whole-label fallback and discard authored values for this internal field. Native Bedrock 1.26.60.5 glyph controls at x10,90,170,250,330,410,570 distinguish the scale grid from a constant offset; the installed private STB provider matches all nine selected TrueType crops at Java GUI scales1,2,3 and return1 when using that grid and truncated palette channels. This is not a production TrueType provider or complete font parity claim. Advance the conversion cache to forms15 because cached program palettes retain the former rounded channels. + +Build UI formatting colors from the fully merged program globals, including builtin_ui.zip. ResourcePackStorage does not expose those bundled UI defaults as a pack layer. Keep the serialized palette through program transport and suppress a lower static value when a higher dynamic override replaces it. A boundary regression uses the real bundled red/grey palette, a server override, a dynamic override, export/import, and protocol-text fallback. + +Export each native TrueType cache render height beside the original font bytes. The add-on reads this descriptor from the selected font pack, while older exports retain Java font loading. Advance conversion identity to forms16 so cached packs include the descriptor. Tests retain original bytes and distinguish heights 64 and 32. + +Keep native font registrations when downloaded server packs declare new +families or redirect MinecraftTen. A Bedrock 1.26.60.5/protocol 2225 capture +uses the bundled face and bitmap fallback despite those declarations. +Preserve existing font-file precedence. Canonical-file override behavior +remains unverified. + +Resolve unrestricted, unscaled primary font aliases from native metadata, +including MinecraftTenEmoticon for covered text. Retain identifiers, +coverage and line heights through program transport. Explicit unicode +labels select the Unicode font even for ASCII. Cyclic alias references +cannot advertise a face. Mixed aliases, ranged/scaled fallbacks and +complete typography remain open. Native font lookup at 141c97c1b and the +Unicode fallback at 141c97c9f support the captured selection behavior. + +Advance conversion identity to forms17. Boundary regressions reject +server font registration changes, preserve native alias selection and +Unicode through export/import, and bound cyclic references. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -560,7 +584,7 @@ The native controls include newline, Unicode and formatting suffixes. .../resourcepack/ui/CookingNavigation.java | 83 + .../resourcepack/ui/CookingRecipeOrder.java | 62 + .../resourcepack/ui/CookingSearchText.java | 45 + - .../api/resourcepack/ui/CookingUi.java | 433 +++++ + .../api/resourcepack/ui/CookingUi.java | 433 ++++ .../api/resourcepack/ui/FormBindings.java | 290 +++ .../api/resourcepack/ui/HudActionbarUi.java | 20 + .../api/resourcepack/ui/HudFactoryUi.java | 132 ++ @@ -568,7 +592,7 @@ The native controls include newline, Unicode and formatting suffixes. .../api/resourcepack/ui/HudTitleUi.java | 28 + .../resourcepack/ui/InventoryItemFilter.java | 43 + .../resourcepack/ui/InventoryStackCount.java | 21 + - .../api/resourcepack/ui/JsonUiProgram.java | 1730 +++++++++++++++++ + .../api/resourcepack/ui/JsonUiProgram.java | 1736 +++++++++++++++++ .../resourcepack/ui/NativeRecipeSearch.java | 124 ++ .../ui/NativeSearchNormalization.java | 189 ++ .../api/resourcepack/ui/NpcDialogueUi.java | 316 +++ @@ -581,7 +605,7 @@ The native controls include newline, Unicode and formatting suffixes. .../api/resourcepack/ui/UiBitmapFonts.java | 303 +++ .../api/resourcepack/ui/UiExpression.java | 306 +++ .../api/resourcepack/ui/UiFontGlyphs.java | 235 +++ - .../api/resourcepack/ui/UiFonts.java | 326 ++++ + .../api/resourcepack/ui/UiFonts.java | 395 ++++ .../api/resourcepack/ui/UiTextureLimits.java | 24 + .../api/resourcepack/ui/UiTextures.java | 101 + .../viabedrock/api/util/NativeSort.java | 168 ++ @@ -616,7 +640,7 @@ The native controls include newline, Unicode and formatting suffixes. .../ui/FormCollectionActivationTest.java | 97 + .../api/resourcepack/ui/HudRootUiTest.java | 90 + .../ui/InventoryItemFilterTest.java | 33 + - .../resourcepack/ui/JsonUiProgramTest.java | 615 ++++++ + .../resourcepack/ui/JsonUiProgramTest.java | 679 +++++++ .../ui/NativeRecipeSearchTest.java | 110 ++ .../ui/NativeSearchNormalizationTest.java | 46 + .../resourcepack/ui/NpcDialogueUiTest.java | 108 + @@ -630,11 +654,11 @@ The native controls include newline, Unicode and formatting suffixes. .../ui/UiCollectionWindowTest.java | 131 ++ .../api/resourcepack/ui/UiExpressionTest.java | 77 + .../api/resourcepack/ui/UiFontGlyphsTest.java | 60 + - .../api/resourcepack/ui/UiFontsTest.java | 289 +++ + .../api/resourcepack/ui/UiFontsTest.java | 311 +++ .../api/resourcepack/ui/UiTexturesTest.java | 231 +++ .../ui/native-inventory-item-filter.json | 311 +++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 98 files changed, 15315 insertions(+), 60 deletions(-) + 98 files changed, 15476 insertions(+), 60 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerMessage.java @@ -729,10 +753,10 @@ The native controls include newline, Unicode and formatting suffixes. create mode 100644 src/tool/java/net/raphimc/viabedrock/tool/generator/SearchUnicodeDataGenerator.java diff --git a/build.gradle b/build.gradle -index 8e003758..7d4816d6 100644 +index 851110da..6493daf2 100644 --- a/build.gradle +++ b/build.gradle -@@ -239,3 +239,14 @@ class MockitoAgentArgumentProvider implements org.gradle.process.CommandLineArgu +@@ -251,3 +251,14 @@ class MockitoAgentArgumentProvider implements org.gradle.process.CommandLineArgu return ["-javaagent:${this.classpath.singleFile}"] } } @@ -2251,7 +2275,7 @@ index 00000000..f7b0cb2a + +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/http/ConvertedResourcePackCache.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/http/ConvertedResourcePackCache.java -index f7460be9..ccb0cc7c 100644 +index f7460be9..a2f6a884 100644 --- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/http/ConvertedResourcePackCache.java +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/http/ConvertedResourcePackCache.java @@ -17,6 +17,7 @@ @@ -2267,7 +2291,7 @@ index f7460be9..ccb0cc7c 100644 public class ConvertedResourcePackCache { - private static final String CONVERSION_VERSION = "native-appearance-rendering-metadata-and-offsets-11"; -+ private static final String CONVERSION_VERSION = "native-appearance-rendering-metadata-offsets-and-forms-14"; ++ private static final String CONVERSION_VERSION = "native-appearance-rendering-metadata-offsets-and-forms-17"; // Advance when the set or format of emitted resources changes independently of converter profiles. - private static final int RESOURCE_FORMAT_VERSION = 7; + private static final int RESOURCE_FORMAT_VERSION = 10; @@ -4106,10 +4130,10 @@ index 00000000..5dfbb3f8 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java new file mode 100644 -index 00000000..022cbcea +index 00000000..07903a79 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java -@@ -0,0 +1,1730 @@ +@@ -0,0 +1,1736 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -4146,6 +4170,7 @@ index 00000000..022cbcea +import java.util.regex.Pattern; +import java.util.zip.ZipInputStream; +import net.raphimc.viabedrock.api.resourcepack.ResourcePack; ++import net.raphimc.viabedrock.api.resourcepack.definition.TextFormattingDefinitions; +import net.raphimc.viabedrock.api.util.TextUtil; +import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; + @@ -4167,13 +4192,12 @@ index 00000000..022cbcea + private final UiFonts fonts = new UiFonts(); + private final Set customizedFiles = new HashSet<>(); + private final UnaryOperator translator; -+ private final Map formattingColors; ++ private final Map formattingColors = new HashMap<>(); + private final boolean nativeHurtPolicy; + + public JsonUiProgram(final ResourcePackStorage storage) { + this.nativeHurtPolicy = true; + this.translator = storage.getTexts()::translate; -+ this.formattingColors = storage.getTextFormatting().colors(); + final Map builtin = Builtins.FILES; + this.loadFiles(builtin, false); + UiTextures.load(builtin, this.textureFiles); @@ -4204,7 +4228,7 @@ index 00000000..022cbcea + fontFiles.put(path, bytes); + } + } -+ this.fonts.load(fontFiles); ++ this.fonts.load(fontFiles, !storage.isServerPack(pack)); + } + } + @@ -4222,7 +4246,7 @@ index 00000000..022cbcea + for (final var files : acceptedLayers) { + this.loadFiles(files, true); + UiTextures.load(files, this.textureFiles); -+ this.fonts.load(files); ++ this.fonts.load(files, false); + } + } + @@ -4240,12 +4264,12 @@ index 00000000..022cbcea + final Map formattingColors, final boolean nativeHurtPolicy) { + this.nativeHurtPolicy = nativeHurtPolicy; + this.translator = translator; -+ this.formattingColors = formattingColors; + for (final Map files : layers) { + this.loadFiles(files, true); + UiTextures.load(files, this.textureFiles); + this.fonts.load(files); + } ++ this.formattingColors.putAll(formattingColors); + } + + public Map textures() { @@ -4658,6 +4682,8 @@ index 00000000..022cbcea + final JsonObject document = parsed.getAsJsonObject(); + if (file.getKey().equals("ui/_global_variables.json")) { + document.entrySet().forEach(entry -> this.globals.put(entry.getKey(), entry.getValue())); ++ this.formattingColors.clear(); ++ this.formattingColors.putAll(TextFormattingDefinitions.uiColors(this.globals)); + continue; + } + final String namespace = string(document, "namespace"); @@ -5317,9 +5343,13 @@ index 00000000..022cbcea + node.addProperty("localize", false); + node.add(textProperty + "_component", TextUtil.textComponentToGson(TextUtil.stringToTextComponent(text, JsonUiProgram.this.formattingColors))); + } ++ node.remove("font_origin_scale"); + final String font = JsonUiProgram.this.fonts.id(string(node, "font_type"), string(node, textProperty)); + if (font != null) { + node.addProperty("font", font); ++ if (font.equals(JsonUiProgram.this.fonts.id(string(node, "font_type")))) { ++ node.addProperty("font_origin_scale", UiFonts.TTF_DISPLAY_SCALE); ++ } + final Double lineHeight = JsonUiProgram.this.fonts.lineHeight(string(node, "font_type")); + if (lineHeight != null && font.equals(JsonUiProgram.this.fonts.id(string(node, "font_type")))) { + node.addProperty("font_line_height", lineHeight); @@ -8749,10 +8779,10 @@ index 00000000..808216eb +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiFonts.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiFonts.java new file mode 100644 -index 00000000..97d7793f +index 00000000..f7290d9c --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiFonts.java -@@ -0,0 +1,326 @@ +@@ -0,0 +1,395 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -8778,13 +8808,14 @@ index 00000000..97d7793f +final class UiFonts { + + // The native TrueTypeFont display transform scales both glyph axes after cache normalization. -+ private static final double TTF_DISPLAY_SCALE = 1.5D; ++ static final double TTF_DISPLAY_SCALE = 1.5D; + private static final int MAX_FONTS = 256; + private static final int MAX_FONT_BYTES = 16 << 20; + private String importedBitmapCharacters = ""; + private final UiBitmapFonts bitmaps = new UiBitmapFonts(); + private final Map files = new LinkedHashMap<>(); + private final Map families = new LinkedHashMap<>(); ++ private final Map aliases = new LinkedHashMap<>(); + private final Map renderHeights = new LinkedHashMap<>(); + private final Map importedIds = new LinkedHashMap<>(); + private final Map importedLineHeights = new LinkedHashMap<>(); @@ -8792,6 +8823,11 @@ index 00000000..97d7793f + private final Map importedGlyphs = new LinkedHashMap<>(); + + void load(final Map layer) { ++ this.load(layer, true); ++ } ++ ++ /** Downloaded packs retain file precedence without replacing native font registrations. */ ++ void load(final Map layer, final boolean registerDefinitions) { + this.bitmaps.load(layer); + layer.forEach((path, bytes) -> { + if (path.startsWith("font/") && JsonUiProgram.safePath(path) @@ -8801,7 +8837,7 @@ index 00000000..97d7793f + } + }); + final byte[] metadata = layer.get("font/font_metadata.json"); -+ if (metadata == null || metadata.length > JsonUiProgram.MAX_PAYLOAD_BYTES) { ++ if (!registerDefinitions || metadata == null || metadata.length > JsonUiProgram.MAX_PAYLOAD_BYTES) { + return; + } + try { @@ -8837,14 +8873,49 @@ index 00000000..97d7793f + } + } + } ++ if (root.has("font_aliases") && root.get("font_aliases").isJsonArray()) { ++ for (final JsonElement entry : root.getAsJsonArray("font_aliases")) { ++ if (!entry.isJsonObject()) { ++ continue; ++ } ++ final JsonObject alias = entry.getAsJsonObject(); ++ final String name = JsonUiProgram.string(alias, "alias"); ++ if (name.isEmpty() || name.length() > 256 || !this.aliases.containsKey(name) ++ && this.aliases.size() + this.families.size() >= MAX_FONTS) { ++ continue; ++ } ++ this.aliases.remove(name); ++ if (alias.has("font_scale_factor_override") || !alias.has("fonts") || !alias.get("fonts").isJsonArray() ++ || alias.getAsJsonArray("fonts").isEmpty()) { ++ continue; ++ } ++ // A whole-string face is available only for an unrestricted, unscaled primary. ++ final JsonElement first = alias.getAsJsonArray("fonts").get(0); ++ if (!first.isJsonObject()) { ++ continue; ++ } ++ final JsonObject reference = first.getAsJsonObject(); ++ if (reference.has("font_ranges") || reference.has("font_scale") && reference.get("font_scale").getAsDouble() != 1) { ++ continue; ++ } ++ final String family = JsonUiProgram.string(reference, "font_reference"); ++ if (!family.isEmpty() && family.length() <= 256) { ++ this.aliases.put(name, family); ++ } ++ } ++ } + } catch (final RuntimeException ignored) { + // An unusable optional font does not disable the server's entire UI program. + } + } + + String id(final String name) { -+ final String path = this.families.get(name); -+ return path != null && metrics(this.files.get(path), this.renderHeights.getOrDefault(name, 64)) != null ? identifier(name) : this.importedIds.get(name); ++ final String family = this.family(name); ++ if (family == null) { ++ return null; ++ } ++ final String path = this.families.get(family); ++ return path != null && metrics(this.files.get(path), this.renderHeights.getOrDefault(family, 64)) != null ? identifier(family) : this.importedIds.get(name); + } + + String id(final String name, final String text) { @@ -8856,6 +8927,9 @@ index 00000000..97d7793f + return declared; + } + } ++ if (name.equals("unicode")) { ++ return "viabedrock:unicode"; ++ } + final String characters = this.bitmapCharacters(); + if (characters.isEmpty()) { + return null; @@ -8874,9 +8948,22 @@ index 00000000..97d7793f + return null; + } + ++ private String family(final String name) { ++ String family = name; ++ for (int depth = 0; depth < MAX_FONTS; depth++) { ++ final String parent = this.aliases.get(family); ++ if (parent == null) { ++ return family; ++ } ++ family = parent; ++ } ++ return null; ++ } ++ + private UiFontGlyphs glyphs(final String name) { -+ final String path = this.families.get(name); -+ return path == null || metrics(this.files.get(path), this.renderHeights.getOrDefault(name, 64)) == null ++ final String family = this.family(name); ++ final String path = family == null ? null : this.families.get(family); ++ return path == null || metrics(this.files.get(path), this.renderHeights.getOrDefault(family, 64)) == null + ? this.importedGlyphs.get(name) + : this.fileGlyphs.computeIfAbsent(path, value -> UiFontGlyphs.read(this.files.get(value))); + } @@ -8921,12 +9008,18 @@ index 00000000..97d7793f + + Map ids() { + final Map result = new LinkedHashMap<>(this.importedIds); -+ this.families.forEach((name, path) -> { -+ final byte[] bytes = this.files.get(path); -+ if (metrics(bytes, this.renderHeights.getOrDefault(name, 64)) != null) { -+ result.put(name, identifier(name)); ++ for (final String name : this.families.keySet()) { ++ final String id = this.id(name); ++ if (id != null) { ++ result.put(name, id); + } -+ }); ++ } ++ for (final String name : this.aliases.keySet()) { ++ final String id = this.id(name); ++ if (id != null) { ++ result.put(name, id); ++ } ++ } + return Collections.unmodifiableMap(result); + } + @@ -8944,8 +9037,9 @@ index 00000000..97d7793f + } + + Double lineHeight(final String name) { -+ final String path = this.families.get(name); -+ final Metrics metrics = path != null ? metrics(this.files.get(path), this.renderHeights.getOrDefault(name, 64)) : null; ++ final String family = this.family(name); ++ final String path = family == null ? null : this.families.get(family); ++ final Metrics metrics = path != null ? metrics(this.files.get(path), this.renderHeights.getOrDefault(family, 64)) : null; + if (metrics != null) { + return metrics.lineHeight(); + } @@ -8954,10 +9048,10 @@ index 00000000..97d7793f + + Map lineHeights() { + final Map result = new LinkedHashMap<>(this.importedLineHeights); -+ this.families.forEach((name, path) -> { -+ final Metrics metrics = metrics(this.files.get(path), this.renderHeights.getOrDefault(name, 64)); -+ if (metrics != null) { -+ result.put(name, metrics.lineHeight()); ++ this.ids().keySet().forEach(name -> { ++ final Double height = this.lineHeight(name); ++ if (height != null) { ++ result.put(name, height); + } + }); + return Collections.unmodifiableMap(result); @@ -9010,6 +9104,11 @@ index 00000000..97d7793f + definition.add("providers", providers); + result.put("assets/viabedrock/font/" + resource + ".json", definition.toString().getBytes(StandardCharsets.UTF_8)); + result.put("assets/viabedrock/font/" + resource + ".ttf", bytes); ++ final JsonObject nativeLayout = new JsonObject(); ++ nativeLayout.addProperty("render_height", this.renderHeights.getOrDefault(name, 64)); ++ final JsonObject nativeMetadata = new JsonObject(); ++ nativeMetadata.add("viabedrock:truetype", nativeLayout); ++ result.put("assets/viabedrock/font/" + resource + ".ttf.mcmeta", nativeMetadata.toString().getBytes(StandardCharsets.UTF_8)); + }); + return Collections.unmodifiableMap(result); + } @@ -13739,10 +13838,10 @@ index 00000000..bc575caa +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgramTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgramTest.java new file mode 100644 -index 00000000..c8beea74 +index 00000000..88d01004 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgramTest.java -@@ -0,0 +1,615 @@ +@@ -0,0 +1,679 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -13755,6 +13854,10 @@ index 00000000..c8beea74 +import com.viaversion.viaversion.libs.gson.JsonParser; +import com.viaversion.viaversion.libs.gson.JsonPrimitive; +import java.nio.charset.StandardCharsets; ++import net.raphimc.viabedrock.api.resourcepack.ResourcePack; ++import net.raphimc.viabedrock.api.resourcepack.content.InMemoryContent; ++import net.raphimc.viabedrock.api.util.TextUtil; ++import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @@ -13785,6 +13888,66 @@ index 00000000..c8beea74 + } + + @Test ++ void serverFontDeclarationsKeepNativeRegistrationsAndAliasesAcrossTransport() { ++ final var content = new InMemoryContent(); ++ content.putString("manifest.json", """ ++ {"format_version":2,"header":{"uuid":"8d5f92b8-3df6-4b36-89f2-c45df58195b1","version":[1,0,0],"name":"font declarations"}} ++ """); ++ content.putString("font/font_metadata.json", """ ++ {"version":1,"fonts":[ ++ {"font_name":"MinecraftTen","font_format":"ttf","font_file":"font/minecraft-ten","target_font_render_size":32}, ++ {"font_name":"HeightProbe","font_format":"ttf","font_file":"font/minecraft-ten"}], ++ "font_aliases":[{"alias":"MinecraftTenEmoticon","fonts":[{"font_reference":"HeightProbe"}]}]} ++ """); ++ content.putString("ui/server_form.json", """ ++ {"namespace":"font_probe","root":{"type":"panel","controls":[ ++ {"native":{"type":"label","text":"WWW","font_type":"MinecraftTen"}}, ++ {"server":{"type":"label","text":"WWW","font_type":"HeightProbe"}}, ++ {"alias":{"type":"label","text":"WWW","font_type":"MinecraftTenEmoticon"}}, ++ {"unicode":{"type":"label","text":"ASCII","font_type":"unicode"}}]}} ++ """); ++ final var program = new ResourcePackStorage(List.of(new ResourcePack(content))).getJsonUiProgram(); ++ final var scene = program.resolve("font_probe.root", new Bindings()); ++ final var imported = JsonUiProgram.fromProgram(program.export(scene.requiredDefinitions(), new JsonObject())); ++ assertEquals(scene.root(), imported.resolve("font_probe.root", new Bindings()).root()); ++ final var labels = scene.root().getAsJsonArray("children"); ++ final var nativeLabel = labels.get(0).getAsJsonObject(); ++ final var serverLabel = labels.get(1).getAsJsonObject(); ++ final var aliasLabel = labels.get(2).getAsJsonObject(); ++ final var unicodeLabel = labels.get(3).getAsJsonObject(); ++ assertEquals(11.8125, nativeLabel.get("font_line_height").getAsDouble()); ++ assertFalse(serverLabel.has("font")); ++ assertFalse(serverLabel.has("font_origin_scale")); ++ assertEquals(nativeLabel.get("font"), aliasLabel.get("font")); ++ assertEquals(nativeLabel.get("font_line_height"), aliasLabel.get("font_line_height")); ++ assertEquals(1.5, aliasLabel.get("font_origin_scale").getAsDouble()); ++ assertEquals("viabedrock:unicode", unicodeLabel.get("font").getAsString()); ++ assertFalse(unicodeLabel.has("font_origin_scale")); ++ } ++ ++ @Test ++ void bundledAndOverriddenUiColorsSurviveProgramTransport() { ++ final var content = new InMemoryContent(); ++ content.putString("manifest.json", """ ++ {"format_version":2,"header":{"uuid":"d73ba1d0-2f38-489d-994f-68080d9c7c85","version":[1,0,0],"name":"palette"}} ++ """); ++ content.putString("ui/_global_variables.json", """ ++ {"$9_color_format":[0,1,0],"$a_color_format":"$dynamic_color"} ++ """); ++ final var storage = new ResourcePackStorage(List.of(new ResourcePack(content))); ++ final var ui = storage.getJsonUiProgram(); ++ final var imported = JsonUiProgram.fromProgram(ui.export(Set.of(), new JsonObject())); ++ for (final var program : List.of(ui, imported)) { ++ assertEquals("#ff5454", program.textComponent("§cRed").getAsJsonObject().get("color").getAsString()); ++ assertEquals("#c5c5c5", program.textComponent("§7Grey").getAsJsonObject().get("color").getAsString()); ++ assertEquals("#00ff00", program.textComponent("§9Blue").getAsJsonObject().get("color").getAsString()); ++ assertEquals("#55ff55", program.textComponent("§aFallback").getAsJsonObject().get("color").getAsString()); ++ } ++ assertEquals(0xFF5555, TextUtil.stringToTextComponent("§cRed", storage.getTextFormatting().colors()) ++ .getStyle().getColor().getRgbValue()); ++ } ++ ++ @Test + void aFactoryOwnsGridPopulationAndTheBoundMaximumLimitsItsRows() { + final var ui = program(""" + {"namespace":"example","root":{"type":"grid","collection_name":"first", @@ -16589,10 +16752,10 @@ index 00000000..1fb2286c +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiFontsTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiFontsTest.java new file mode 100644 -index 00000000..6cf17b80 +index 00000000..e94582bb --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiFontsTest.java -@@ -0,0 +1,289 @@ +@@ -0,0 +1,311 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -16607,9 +16770,9 @@ index 00000000..6cf17b80 +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Map; ++import java.util.Set; +import java.util.HashMap; +import java.util.List; -+import java.util.Set; +import com.viaversion.viaversion.libs.gson.JsonPrimitive; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; @@ -16617,6 +16780,21 @@ index 00000000..6cf17b80 +final class UiFontsTest { + + @Test ++ void cyclicAliasesDoNotExposeAnUnresolvableFont() { ++ final var fonts = new UiFonts(); ++ final JsonObject declarations = JsonParser.parseString(new String(metadata("Title", "ttf", "font/title"), StandardCharsets.UTF_8)).getAsJsonObject(); ++ declarations.add("font_aliases", JsonParser.parseString(""" ++ [{"alias":"First","fonts":[{"font_reference":"Second"}]}, ++ {"alias":"Second","fonts":[{"font_reference":"First"}]}] ++ """)); ++ fonts.load(Map.of("font/font_metadata.json", declarations.toString().getBytes(StandardCharsets.UTF_8), ++ "font/title.ttf", face(1000, 990, -250))); ++ assertNull(fonts.id("First", "WWW")); ++ assertEquals(Set.of("Title"), fonts.ids().keySet()); ++ assertEquals(Set.of("Title"), fonts.lineHeights().keySet()); ++ } ++ ++ @Test + void normalizesEmAndBaselineAgainstNativeHeightAndRejectsInvalidTables() { + final byte[] face = face(1000, 990, -250); + final var metrics = UiFonts.metrics(face); @@ -16655,6 +16833,8 @@ index 00000000..6cf17b80 + final Map resources = fonts.resources(); + final String path = "assets/viabedrock/font/" + id.substring("viabedrock:".length()); + assertArrayEquals(original, resources.get(path + ".ttf")); ++ assertEquals(64, JsonParser.parseString(new String(resources.get(path + ".ttf.mcmeta"), StandardCharsets.UTF_8)) ++ .getAsJsonObject().getAsJsonObject("viabedrock:truetype").get("render_height").getAsInt()); + final var providers = JsonParser.parseString(new String(resources.get(path + ".json"), StandardCharsets.UTF_8)) + .getAsJsonObject().getAsJsonArray("providers"); + assertEquals(12D * 1000 / 1240, providers.get(0).getAsJsonObject().get("size").getAsDouble(), 0.000001); @@ -16698,6 +16878,9 @@ index 00000000..6cf17b80 + lowResolution.getAsJsonArray("fonts").get(0).getAsJsonObject().addProperty("target_font_render_size", 32); + server.load(Map.of("font/font_metadata.json", lowResolution.toString().getBytes(StandardCharsets.UTF_8))); + assertEquals(11.625, server.lineHeight("Title")); ++ final String nativePath = "assets/viabedrock/font/" + server.id("Title").substring("viabedrock:".length()) + ".ttf.mcmeta"; ++ assertEquals(32, JsonParser.parseString(new String(server.resources().get(nativePath), StandardCharsets.UTF_8)) ++ .getAsJsonObject().getAsJsonObject("viabedrock:truetype").get("render_height").getAsInt()); + ids.addProperty("Unsafe", "other:font"); + assertThrows(IllegalArgumentException.class, () -> client.importIds(ids)); + } @@ -16770,12 +16953,14 @@ index 00000000..6cf17b80 + if (Set.of("WWW", "W\rW", "§cWWW", "WéW", "W", "§cW").contains(display)) { + assertEquals(titleFont, actual.get("font").getAsString(), text); + assertEquals(11.8125, actual.get("font_line_height").getAsDouble(), text); ++ assertEquals(1.5, actual.get("font_origin_scale").getAsDouble(), text); + } else { + assertTrue(!actual.has("font") || !titleFont.equals(actual.get("font").getAsString()), text); + assertFalse(actual.has("font_line_height"), text); ++ assertFalse(actual.has("font_origin_scale"), text); + } + } -+ assertEquals(2, server.fontResources().size()); ++ assertEquals(3, server.fontResources().size()); + assertTrue(client.fontResources().isEmpty()); + } + diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index 4958f348..00508e02 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -72,22 +72,33 @@ Regressions cover bound-text changes across program transport, winning-file repl Malformed tables and truncated or oversized coverage are rejected or select the bitmap fallback. Disabling coverage selection makes the binding-refresh regression fail. The target native font uses cmap format 4. Supplementary format-12 behavior has synthetic coverage and still needs matching native controls. -Complete TrueType raster appearance and native font-type aliases remain open. +Other font families, mixed aliases, ranged or scaled fallbacks, and complete typography remain open. Bedrock 1.26.60.5 derives the base TrueType scale from the target render height and hhea ascent/descent range. Its glyph cache doubles that scale for 2×2 oversampling and retains fractional packed metrics. -Java 26.3 rounds the FreeType EM pixel size after multiplying the provider size by its oversample value. +Stock Java 26.3 rounds the FreeType EM pixel size after multiplying the provider size by its oversample value. For MinecraftTen at target height 64, the native base scale corresponds to about 51.61 EM pixels and the raster scale to 103.23. -The current Java provider requests 77. Changing oversampling alone does not reproduce the native raster path. +The stock Java provider requests 77. Changing oversampling alone does not reproduce the native raster path. Original initializer, glyph lookup, outline, rectangle, raster, and filter instructions supply 36 CPU cache controls. All cache pixels and packed metrics match the installed LWJGL 3.4.3 STB rasterizer. The controls cover six glyphs at heights 32, 64, and 96 for MinecraftTen and an additional curved-outline font. The fixture supplies atlas placement and external allocation, memory, and rounding interfaces. It does not execute cache I/O, GPU upload, shaders, or complete native screens. -A replacement provider still needs bounded font validation, resource lifecycle checks, and native GPU comparisons. - -Dynamic domain text uses the same native palette as ordinary labels. +Original page packing and raster instructions produce a 1,024-pixel atlas with 256 glyph records. +All atlas bytes and packed records match LWJGL STB with the native placements. +Selected TrueType labels carry their 1.5 display grid through program transport. Bitmap fallbacks retain their own grid. +TrueType exports retain original font bytes and a descriptor with the native cache height. +The add-on reads that descriptor from the selected font pack. Older exports retain Java font loading. +The [client provider](../../viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md) owns bounded outline rendering and resource retirement. + +Native font registrations survive downloaded font metadata. Unrestricted, unscaled primary aliases retain their face, coverage, and line height across transport. +Explicit `unicode` labels select Unicode even for ASCII. Canonical font-file overrides remain unverified. +Regressions cover these selection boundaries and cyclic alias rejection. +Native Bedrock 1.26.60.5 controls cover server declarations, the built-in primary alias, and explicit Unicode. + +UI labels and dynamic domain text use the fully merged globals, including the bundled default palette. +Program transport retains these colors. Protocol text keeps server-only overrides. Help regressions cover translated components and exported program round trips. The [owning patch](0095-share-native-ui-programs-and-resources.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viabedrock/upstreamable/0102-match-native-language-parsing-and-formatting.patch b/patches/viabedrock/upstreamable/0102-match-native-language-parsing-and-formatting.patch index ac02990f..3732805e 100644 --- a/patches/viabedrock/upstreamable/0102-match-native-language-parsing-and-formatting.patch +++ b/patches/viabedrock/upstreamable/0102-match-native-language-parsing-and-formatting.patch @@ -1,4 +1,4 @@ -From ed8f66752756636cd24fb6ee5f1966946268bf2c Mon Sep 17 00:00:00 2001 +From 4d83acc5d610d342c3b769b4b2c9d29fb2052d23 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:11:46 +0200 Subject: [PATCH] fix(text): match native language parsing and formatting @@ -62,7 +62,7 @@ controls; keep unsupported Microsoft length extensions outside the grammar. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/ItemPrimaryNameTest.java diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java -index 642bbdbd..ad30a660 100644 +index 94504b1f..0222a06a 100644 --- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java @@ -20,6 +20,7 @@ package net.raphimc.viabedrock.api.resourcepack.content; @@ -73,7 +73,7 @@ index 642bbdbd..ad30a660 100644 import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream; -@@ -33,7 +34,6 @@ import java.io.UncheckedIOException; +@@ -35,7 +36,6 @@ import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -81,7 +81,7 @@ index 642bbdbd..ad30a660 100644 public abstract class Content { -@@ -64,6 +64,7 @@ public abstract class Content { +@@ -66,6 +66,7 @@ public abstract class Content { public final boolean put(final String path, final byte[] data) { final boolean replaced = this.putBytes(path, data); @@ -89,7 +89,7 @@ index 642bbdbd..ad30a660 100644 this.storedZipEntries.remove(path); return replaced; } -@@ -71,6 +72,7 @@ public abstract class Content { +@@ -73,6 +74,7 @@ public abstract class Content { /** Stores an already compressed payload without another deflation pass when exporting the ZIP. */ public final boolean putStored(final String path, final byte[] data) { final boolean replaced = this.putBytes(path, data); @@ -97,7 +97,7 @@ index 642bbdbd..ad30a660 100644 this.storedZipEntries.add(path); return replaced; } -@@ -108,15 +110,9 @@ public abstract class Content { +@@ -110,15 +112,9 @@ public abstract class Content { } public Map getLang(final String path) { diff --git a/patches/viabedrock/upstreamable/0109-keep-resource-image-codecs-in-memory.patch b/patches/viabedrock/upstreamable/0109-keep-resource-image-codecs-in-memory.patch new file mode 100644 index 00000000..3acf653c --- /dev/null +++ b/patches/viabedrock/upstreamable/0109-keep-resource-image-codecs-in-memory.patch @@ -0,0 +1,129 @@ +From c938338f2d4ec1a85fea04fd2767a047559b71ec Mon Sep 17 00:00:00 2001 +From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> +Date: Tue, 3 Nov 2026 20:20:17 +0100 +Subject: [PATCH] fix(resources): keep resource image codecs in memory + +Resource image bytes already reside in memory, but the ImageIO stream convenience methods can spool them to temporary files. An unavailable cache can prevent the bundled default skin from decoding and abort login, or stop PNG export. + +Use memory-backed input and output streams for Content images without changing global ImageIO cache settings. Respect ImageIO input ownership and retain the original PNG passthrough. Test pixel decoding and both PNG export paths with an unwritable disk cache. +--- + .../api/resourcepack/content/Content.java | 17 ++++-- + .../content/ContentImageCacheTest.java | 58 +++++++++++++++++++ + 2 files changed, 70 insertions(+), 5 deletions(-) + create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/content/ContentImageCacheTest.java + +diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java +index 622e99d2..e63777a3 100644 +--- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java ++++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/content/Content.java +@@ -22,6 +22,8 @@ import com.viaversion.viaversion.util.GsonUtil; + import net.raphimc.viabedrock.api.util.JsonUtil; + + import javax.imageio.ImageIO; ++import javax.imageio.stream.MemoryCacheImageInputStream; ++import javax.imageio.stream.MemoryCacheImageOutputStream; + import java.awt.image.BufferedImage; + import java.io.ByteArrayInputStream; + import java.io.ByteArrayOutputStream; +@@ -142,8 +144,8 @@ public abstract class Content { + + public boolean putPngImage(final String path, final BufferedImage image) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); +- try { +- ImageIO.write(image, "png", baos); ++ try (var output = new MemoryCacheImageOutputStream(baos)) { ++ ImageIO.write(image, "png", output); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } +@@ -189,7 +191,12 @@ public abstract class Content { + public BufferedImage getImage() { + if (this.image == null) { + try { +- this.image = ImageIO.read(new ByteArrayInputStream(this.bytes)); ++ final var input = new MemoryCacheImageInputStream(new ByteArrayInputStream(this.bytes)); ++ this.image = ImageIO.read(input); ++ // ImageIO closes the stream when it selects a reader, but leaves unmatched input open. ++ if (this.image == null) { ++ input.close(); ++ } + } catch (final IOException e) { + throw new RuntimeException(e); + } +@@ -207,8 +214,8 @@ public abstract class Content { + } else { + final BufferedImage image = this.getImage(); + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); +- try { +- ImageIO.write(image, "png", baos); ++ try (var output = new MemoryCacheImageOutputStream(baos)) { ++ ImageIO.write(image, "png", output); + } catch (final IOException e) { + throw new RuntimeException(e); + } +diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/content/ContentImageCacheTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/content/ContentImageCacheTest.java +new file mode 100644 +index 00000000..5b14f400 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/content/ContentImageCacheTest.java +@@ -0,0 +1,58 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.resourcepack.content; ++ ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.io.TempDir; ++import org.junit.jupiter.api.parallel.Isolated; ++ ++import javax.imageio.ImageIO; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.attribute.PosixFileAttributeView; ++import java.nio.file.attribute.PosixFilePermissions; ++import java.util.Base64; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.junit.jupiter.api.Assumptions.assumeTrue; ++ ++@Isolated("Changes ImageIO's disk-cache policy to exercise an unavailable cache") ++class ContentImageCacheTest { ++ ++ @TempDir ++ Path directory; ++ ++ @Test ++ void readsAndWritesResourcePixelsWhenTheImageIoDiskCacheIsUnwritable() throws Exception { ++ assumeTrue(Files.getFileStore(this.directory).supportsFileAttributeView(PosixFileAttributeView.class)); ++ final Path cache = Files.createDirectory(this.directory.resolve("image-cache")); ++ final boolean previousPolicy = ImageIO.getUseCache(); ++ final var previousDirectory = ImageIO.getCacheDirectory(); ++ final byte[] png = Base64.getDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAAEUlEQVR4nGMUWm3yP2XlvwYAEYkEfZ7zExIAAAAASUVORK5CYII="); ++ try { ++ ImageIO.setCacheDirectory(cache.toFile()); ++ ImageIO.setUseCache(true); ++ Files.setPosixFilePermissions(cache, PosixFilePermissions.fromString("r-x------")); ++ final var content = new InMemoryContent(); ++ content.put("texture.png", png); ++ final var image = content.getImage("texture.png").getImage(); ++ assertNotNull(image); ++ assertEquals(0xFF12AB34, image.getRGB(0, 0)); ++ assertEquals(0x7F765432, image.getRGB(1, 0)); ++ content.putPngImage("copy.png", image); ++ final var copy = new Content.LazyImage(content.getImage("copy.png").getPngBytes(true), "png").getImage(); ++ assertEquals(0xFF12AB34, copy.getRGB(0, 0)); ++ assertEquals(0x7F765432, copy.getRGB(1, 0)); ++ assertTrue(ImageIO.getUseCache()); ++ assertEquals(cache.toFile(), ImageIO.getCacheDirectory()); ++ } finally { ++ ImageIO.setCacheDirectory(previousDirectory); ++ ImageIO.setUseCache(previousPolicy); ++ Files.setPosixFilePermissions(cache, PosixFilePermissions.fromString("rwx------")); ++ } ++ } ++ ++} +-- +2.56.0 diff --git a/patches/viabedrock/upstreamable/0109-keep-resource-image-codecs-in-memory.pr.md b/patches/viabedrock/upstreamable/0109-keep-resource-image-codecs-in-memory.pr.md new file mode 100644 index 00000000..1039106e --- /dev/null +++ b/patches/viabedrock/upstreamable/0109-keep-resource-image-codecs-in-memory.pr.md @@ -0,0 +1,8 @@ +Keep resource image decoding and PNG export independent of temporary disk storage. +The source and destination bytes already reside in memory. +Use memory-backed ImageIO streams without changing the JVM-wide cache policy. + +A regression checks decoded pixels, PNG export, and forced PNG re-encoding with an unwritable ImageIO cache. +The live failure was a missing bundled default skin during login under an exhausted host temporary quota. +The standalone upstream patch and full stack build pass. +A separate probe preserves PNG pixels with the JVM's default temporary storage unavailable. diff --git a/patches/viafabricplus-bedrock/series.json b/patches/viafabricplus-bedrock/series.json index 2ab15985..498f3ea2 100644 --- a/patches/viafabricplus-bedrock/series.json +++ b/patches/viafabricplus-bedrock/series.json @@ -15,11 +15,6 @@ "project": "viabedrock", "patch": "0064-evaluate-named-actor-properties.patch", "reason": "Publish the versioned custom-actor sampler snapshot and immutable evaluated texture bindings." - }, - { - "project": "cubeconverter", - "patch": "0003-preserve-authored-face-uv-rotations.patch", - "reason": "Preserve authored per-face rotations in parsed geometry and Java item export." } ] }, @@ -59,11 +54,6 @@ "patch": "0099-bridge-native-actor-picking-hitboxes.patch", "reason": "Publish the negotiated picking message used by the actor adapter." }, - { - "project": "cubeconverter", - "patch": "0002-preserve-actor-lighting-controls.patch", - "reason": "Publish ordered render-controller material and visibility declarations." - }, { "project": "viabedrock", "patch": "0085-resolve-server-effects-relative-to-their-actor.patch", diff --git a/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.pr.md b/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.pr.md index 6bf9191e..e2c6803a 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.pr.md @@ -15,7 +15,6 @@ Prerequisites: - viabedrock/0064-evaluate-named-actor-properties.patch: Publish the versioned custom-actor sampler snapshot and immutable evaluated texture bindings. -- cubeconverter/0003-preserve-authored-face-uv-rotations.patch: Preserve authored per-face rotations in parsed geometry and Java item export. ## Validation and remaining limits diff --git a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.patch b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.patch index df539a23..7e98fe69 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.patch @@ -1,4 +1,4 @@ -From 262c26dbca8c630ced6c6709586b3bd4ef84c780 Mon Sep 17 00:00:00 2001 +From c87cda02d6851386dde9c64a00b6a0236ce2e860 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(runtime): share native actor animation and resource @@ -656,6 +656,7 @@ rotation, movement timing, or the original custom-skin report. .../appearance/BedrockParticleSoundTest.java | 176 +++ .../appearance/BedrockParticleSpaceTest.java | 160 +++ .../appearance/BedrockPcmStreamTest.java | 63 + + .../appearance/BedrockPlayerHeadwearTest.java | 91 ++ .../BedrockResourceLibraryTest.java | 571 +++++++++ .../BedrockServerActorResourcesTest.java | 140 +++ .../BedrockServerParticlesTest.java | 70 ++ @@ -688,7 +689,7 @@ rotation, movement timing, or the original custom-skin report. .../render/BedrockViewBobbingTest.java | 93 ++ .../render/BedrockWalkingAnimationTest.java | 65 + .../render/NativeActorColorPayloadTest.java | 50 + - 212 files changed, 25176 insertions(+), 16 deletions(-) + 213 files changed, 25267 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockActorAnimation.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockActorQueries.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockActorScale.java @@ -858,6 +859,7 @@ rotation, movement timing, or the original custom-skin report. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockParticleSoundTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockParticleSpaceTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockPcmStreamTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockPlayerHeadwearTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockResourceLibraryTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockServerActorResourcesTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockServerParticlesTest.java @@ -23686,6 +23688,103 @@ index 0000000..1aa8e73 + assertThrows(IllegalArgumentException.class, () -> new BedrockPcmStream(SAMPLE, FORMAT, -2, 0)); + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockPlayerHeadwearTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockPlayerHeadwearTest.java +new file mode 100644 +index 0000000..cfa37f3 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockPlayerHeadwearTest.java +@@ -0,0 +1,91 @@ ++package com.viaversion.viafabricplus.bedrock.appearance; ++ ++import com.mojang.blaze3d.vertex.PoseStack; ++import com.mojang.blaze3d.vertex.VertexConsumer; ++import com.viaversion.viafabricplus.bedrock.render.BedrockGeometry; ++import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerAnimationState; ++import java.nio.charset.StandardCharsets; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import net.minecraft.client.renderer.entity.state.AvatarRenderState; ++import net.minecraft.world.entity.EquipmentSlot; ++import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.item.ItemStack; ++import org.cube.converter.model.impl.bedrock.BedrockGeometryModel; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.BeforeAll; ++import net.minecraft.SharedConstants; ++import net.minecraft.server.Bootstrap; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.ArgumentMatchers.*; ++import static org.mockito.Mockito.*; ++ ++final class BedrockPlayerHeadwearTest { ++ @BeforeAll ++ static void bootstrap() { ++ SharedConstants.tryDetectVersion(); ++ Bootstrap.bootStrap(); ++ } ++ ++ @Test ++ void hidesOnlyTheHeadwearSurfaceAndRestoresItsVerticesAfterHelmetRemoval() throws Exception { ++ // Bedrock 1.26.60.5: an owned Black Hat loses exactly four brim vertices with a helmet. ++ final var geometry = BedrockGeometryModel.fromJson(""" ++ {"minecraft:geometry":[{"description":{"identifier":"geometry.headwear","texture_width":256,"texture_height":256}, ++ "bones":[{"name":"head","pivot":[0,24,0], ++ "cubes":[{"origin":[-4,24,-4],"size":[8,8,8],"uv":[64,64]}]}, ++ {"name":"helmet","parent":"head","pivot":[0,29.6,0], ++ "cubes":[{"origin":[-6,29.6,-6],"size":[12,0,12],"inflate":0.68, ++ "pivot":[0,29.6,0],"uv":{"up":{"uv":[13,13],"uv_size":[-12,-12]}}}]}]}]} ++ """).getFirst(); ++ final var library = BedrockResourceLibrary.read(Map.of( ++ "library/vanilla/entity/player.entity.json", """ ++ {"minecraft:client_entity":{"description":{"identifier":"minecraft:player", ++ "scripts":{"pre_animation":["v.helmet_layer_visible = !query.has_head_gear;"]}, ++ "render_controllers":["controller.render.player.third_person"]}}} ++ """.getBytes(StandardCharsets.UTF_8), ++ "library/vanilla/render_controllers/player.json", """ ++ {"render_controllers":{"controller.render.player.third_person":{ ++ "part_visibility":[{"*":true},{"helmet":"v.helmet_layer_visible"}]}}} ++ """.getBytes(StandardCharsets.UTF_8))); ++ final var playback = BedrockActorAnimation.read(library, BedrockClassicOptions.EMPTY).playback(); ++ final var actor = mock(LivingEntity.class); ++ final var head = mock(ItemStack.class); ++ when(actor.getItemBySlot(EquipmentSlot.HEAD)).thenReturn(head); ++ final var model = BedrockGeometry.playerModel(geometry, false); ++ final var state = new State(); ++ final var frames = new ArrayList>>(); ++ for (final boolean empty : new boolean[]{true, false, true}) { ++ when(head.isEmpty()).thenReturn(empty); ++ playback.sample(frames.size(), new BedrockActorAnimation.Frame( ++ BedrockActorQueries.equipmentAndVehicle(actor), Map.of())); ++ state.visibility = playback.visibility(); ++ model.setupAnim(state); ++ final var emitted = new ArrayList>(); ++ final var consumer = mock(VertexConsumer.class); ++ doAnswer(call -> { ++ emitted.add(List.of(call.getArgument(0), call.getArgument(1), call.getArgument(2), ++ call.getArgument(4), call.getArgument(5))); ++ return null; ++ }).when(consumer).addVertex(anyFloat(), anyFloat(), anyFloat(), anyInt(), anyFloat(), anyFloat(), ++ anyInt(), anyInt(), anyFloat(), anyFloat(), anyFloat()); ++ model.root().render(new PoseStack(), consumer, 0, 0, -1); ++ assertEquals(empty ? 28 : 24, emitted.size()); ++ frames.add(List.copyOf(emitted)); ++ } ++ assertEquals(frames.getFirst(), frames.getLast()); ++ assertTrue(frames.getFirst().containsAll(frames.get(1))); ++ } ++ ++ private static final class State extends AvatarRenderState implements BedrockPlayerAnimationState { ++ private BedrockActorAnimation.Visibility visibility = BedrockActorAnimation.Visibility.UNSPECIFIED; ++ private Map pose = Map.of(); ++ ++ @Override public BedrockActorAnimation.Visibility bedrockBodyVisibility() { return this.visibility; } ++ @Override public void bedrockBodyVisibility(final BedrockActorAnimation.Visibility value) { this.visibility = value; } ++ @Override public Map bedrockAnimationPose() { return this.pose; } ++ @Override public void bedrockAnimationPose(final Map value) { this.pose = value; } ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockResourceLibraryTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockResourceLibraryTest.java new file mode 100644 index 0000000..3e4fead diff --git a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md index 76d51b04..64791d20 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md @@ -18,7 +18,6 @@ Prerequisites: - viabedrock/0099-bridge-native-actor-picking-hitboxes.patch: Publish the negotiated picking message used by the actor adapter. -- cubeconverter/0002-preserve-actor-lighting-controls.patch: Publish ordered render-controller material and visibility declarations. - viabedrock/0085-resolve-server-effects-relative-to-their-actor.patch: Publish native animation-controller command snapshots consumed by the shared graph runtime. @@ -78,4 +77,13 @@ Both `mvmodels:holy_knight` and `mvmodels:mecha_oondasta` retain loaded graphs a The overall rendering check still fails on unresolved mappings, including the custom block gaps. This control does not establish every malformed expression's recovery or complete public-server actor appearance. +Official Bedrock 1.26.60.5 controls equip an owned Black Hat, add a diamond helmet, and remove it at a fixed camera position. +The helmet collapses exactly four brim vertices. Removal restores those four positions exactly. +An installed controlled replay preserves the full native persona, including its animated face, and observes the same visible, hidden, and visible sequence. +The helmet and return GPU frames use the captured body texture and face-frame bytes. Body draws change from 360 to 366 indices, while the face retains 72 indices. +`BedrockPlayerHeadwearTest` covers the same surface lifetime through equipment queries, the sampled controller, and vertex emission. +These controls cover one headwear item. Observer FOV effects and idle-pose differences prevent a pixel-parity claim. +Full skin geometry, lighting, and account parity remain open. +The full add-on suite passes: 1,464 tests, 153 optional skips, and no failures. A fresh full-stack replay reproduces the tested source tree. + The [owning patch](0008-share-native-actor-and-resource-runtime.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index 7000b72d..499ceb85 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From 4cd6d773025e44ce7880b029a34ce10ea8c9073a Mon Sep 17 00:00:00 2001 +From 83fe770a268dc50492d3e189505ae6ae4c2231d3 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] feat(ui): share native scene rendering and GUI actors @@ -852,6 +852,84 @@ endpoints leaves the screen interior transparent. Existing native captures paint that background black. Keep empty destinations and non-finite region rejection separate. A draw-boundary regression uses this native layout and also verifies that a zero-width destination emits no graphics calls. + +Snap selected TrueType label origins using the native font display multiplier carried by the shared scene, independently of authored font size and line admission. The 1.26.60.5 MinecraftTen controls distinguish three position remainders on its 1.5 grid; a constant shift cannot match them. The installed private STB prototype matches all nine selected TrueType crops through GUI scales1,2,3 and return1 with the corrected grid and native truncated palette. Production still uses its existing raster provider, so these controls do not establish general TrueType or complete screen parity. + +Original 1.26.60.5 origin instructions at 140e6ffd0..140e7001c multiply by the float pixel ratio, truncate toward zero, and multiply by its inverse. Execute them with the original scale lookup for 126 supplied render-context controls across UI scales1,2,3 and six font grids. Preserve this float operation order and negative-coordinate behavior instead of double floor arithmetic. The submitted-origin regression includes negative and near-zero positions; the existing bitmap controls remain covered. + +Rasterize native TrueType outlines with STB and its 2-by-2 prefilter. +FreeType reads font bytes. Only checked vertices reach STB. +Load unexpanded components and preserve native contour closure and compound +transforms, including the extra axis-length factors. Expanded FreeType +outlines can shift to the horizontal bearing. Read cache height from the +selected font pack. Older exports retain Java font loading. + +Bound files, raster dimensions, outline points, component depth, and +component count. Release native faces, libraries, and buffers on failures +and retirement. Invalid glyphs use the existing missing-glyph path. Java +owns glyph texture upload and retirement. The native GUI shader retains +grayscale color and coverage without Java's alpha cutoff. + +Original Bedrock 1.26.60.5 outline, raster, and filter instructions match +36 real-font controls at heights 32/64/96 and five authored edge controls. +Tests cover pixels, phase, translated and scaled components, bounds, +cycles, owned font bytes, native metrics, close/reload, and pack ownership. +The installed add-on matches all 72 selected label crops across Java GUI +scales 1/2/3/1 and after resource reload, without an experimental font mod. +Other fonts, native aliases, and complete screens retain their limits. + +Submit TrueType GUI vertices at native cache-space half precision before +the separate 1.5 display transform. The shared 1.26.60.5 font mesh path +stores half positions before projection. Display-space rounding loses +that order and changes multi-glyph sampling. Keep world text on its +existing vertex path and retain Java atlas allocation and retirement. + +Use exact texture bounds for owned native TrueType bitmaps. The original +glyph producer submits the cache rectangle without Java's 0.01-texel +inset. This correction and cache-space precision are both required by +the current native label controls. The baked-glyph regression checks +submitted vertices before the pose transform and preserves world text. + +Bedrock 1.26.60.5 TrueType glyph records retain an integer italic flag. +The original shared quad expansion at 0x140e6da3c..0x140e6dced shifts +top vertices by +1 and bottom vertices by -1 in cache coordinates. +Apply that fixed shear before half-float conversion and the 1.5 display +transform instead of Java's height-dependent shear. Preserve the Java +world-text path and bound both geometries for draw batching. The native +instruction fixture supplies settled cache and tessellator state; it does +not establish full styled-screen or bold/shadow parity. + +Apply the same native italic edge field to bitmap cells before half-float +packing. The 1.26.60.5 bitmap writer at 0x141590f60 retains an integer +italic flag in its glyph record. Shared quad expansion offsets top and +bottom by +1 and -1 regardless of the cell height or vertical offset. +Java's shear instead varies with those bounds, which distorts the native +12-unit cells with a -3 offset. Retain original Java world vertices and +conservative active bounds while the GUI path uses the native geometry. + +Preserve TrueType sample phase when Java relocates native glyphs. The +1.26.60.5/protocol2225 GPU capture supplies twelve real draws: all 288 UV +vertices use a 65536 packing scale and UNORM16 decode by 65535. Each +channel of the captured 1024 atlas matches the original native raster. +Nearest sampling depends on the resulting fraction of a texel, so applying +the same packing to Java atlas coordinates alone still changes pixels. + +Reconstruct the native 256-codepoint shelf layout lazily from checked font +bounds, including the shared missing-glyph cell and exact-fit wrap/reject +boundaries. Preserve its UV quantization in texel space at Java upload +locations. Keep the existing glyph atlas, resource ownership, and world +text path. Authored-font UV controls fail before the fix; isolated GPU +controls retain native samples after relocation to two different origins. + +Initialize the one-pixel trailing gutter reserved by FontTexture for each +owned bitmap. The paired native/Java GPU capture has identical six-glyph +interiors, but Java atlas gutter bytes differ from native zero padding. +At exact italic sample boundaries those stale bytes become visible white +pixels. Clearing only those reserved gutters removes both differences in +the captured draw replay without changing geometry or shader sampling. +Clamp upload extents at the texture edge; do not resize the glyph or +overwrite another atlas allocation. The upload regression checks native +pixels, zero padding, and an atlas-edge upload, and fails before the fix. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../appearance/BedrockBoneAnimation.java | 3 + @@ -864,8 +942,10 @@ also verifies that a zero-width destination emits no graphics calls. .../render/MixinAvatarAnimationRenderer.java | 2 + .../render/MixinBedrockActorSubmission.java | 6 + .../render/MixinBedrockBitmapFont.java | 40 + - .../MixinBedrockBitmapGlyphRenderState.java | 27 + + .../MixinBedrockBitmapGlyphRenderState.java | 29 + .../MixinBedrockRenderingResources.java | 2 + + .../render/MixinBedrockTrueTypeAtlas.java | 23 + + .../render/MixinBedrockTrueTypeFont.java | 28 + .../render/MixinEntityRenderDispatcher.java | 21 +- .../render/MixinGuiEntityRenderer.java | 53 + .../render/MixinNativeHudOverlays.java | 34 + @@ -885,8 +965,10 @@ also verifies that a zero-width destination emits no graphics calls. .../render/BedrockPlayerAttachables.java | 17 +- .../bedrock/render/BedrockPlayerSkins.java | 44 +- .../render/font/BedrockBitmapFont.java | 251 +++++ - .../render/font/BedrockBitmapGlyph.java | 105 ++ + .../render/font/BedrockBitmapGlyph.java | 127 +++ .../render/font/BedrockBitmapPipeline.java | 49 + + .../render/font/BedrockTrueTypeFont.java | 213 ++++ + .../render/font/BedrockTrueTypeRaster.java | 375 ++++++ .../bedrock/screen/ui/BedrockHudOverlays.java | 128 +++ .../screen/ui/BedrockNativeUiHurt.java | 67 ++ .../screen/ui/NativeHudOverlayState.java | 66 ++ @@ -904,7 +986,7 @@ also verifies that a zero-width destination emits no graphics calls. .../bedrock/screen/ui/NativeUiDimension.java | 93 ++ .../bedrock/screen/ui/NativeUiFlipbook.java | 47 + .../bedrock/screen/ui/NativeUiFocus.java | 329 ++++++ - .../bedrock/screen/ui/NativeUiFont.java | 66 ++ + .../bedrock/screen/ui/NativeUiFont.java | 80 ++ .../bedrock/screen/ui/NativeUiGraphics.java | 56 + .../screen/ui/NativeUiHurtPayload.java | 22 + .../bedrock/screen/ui/NativeUiImageCache.java | 229 ++++ @@ -933,8 +1015,9 @@ also verifies that a zero-width destination emits no graphics calls. .../shaders/core/bedrock_actor.fsh | 8 + .../shaders/core/bedrock_bitmap_font.fsh | 26 + .../shaders/core/bedrock_bitmap_font.vsh | 29 + + .../shaders/core/bedrock_truetype_font.fsh | 10 + .../viafabricplus-bedrock.accesswidener | 4 + - .../viafabricplus-bedrock.mixins.json | 13 +- + .../viafabricplus-bedrock.mixins.json | 15 +- .../BedrockBitmapFontResourcesTest.java | 67 ++ .../BedrockGuiActorAppearanceTest.java | 92 ++ .../BedrockGuiActorContextTest.java | 140 +++ @@ -943,8 +1026,9 @@ also verifies that a zero-width destination emits no graphics calls. .../render/BedrockActorMaterialsGpuTest.java | 38 +- .../render/BedrockShaderPipelinesTest.java | 24 + .../render/font/BedrockBitmapFontTest.java | 239 ++++ - .../render/font/BedrockBitmapGlyphTest.java | 71 ++ + .../render/font/BedrockBitmapGlyphTest.java | 107 ++ .../font/BedrockBitmapShaderGpuTest.java | 291 +++++ + .../render/font/BedrockTrueTypeFontTest.java | 329 ++++++ .../bedrock/screen/ui/AnimationPrograms.java | 23 + .../screen/ui/BedrockNativeUiHurtTest.java | 71 ++ .../screen/ui/NativeHudOverlayStateTest.java | 153 +++ @@ -957,7 +1041,7 @@ also verifies that a zero-width destination emits no graphics calls. .../ui/NativeUiCollectionWindowTest.java | 144 +++ .../screen/ui/NativeUiColorAnimationTest.java | 114 ++ .../bedrock/screen/ui/NativeUiFocusTest.java | 124 ++ - .../bedrock/screen/ui/NativeUiFontTest.java | 148 +++ + .../bedrock/screen/ui/NativeUiFontTest.java | 149 +++ .../screen/ui/NativeUiImageCacheTest.java | 142 +++ .../screen/ui/NativeUiImageRegionsTest.java | 217 ++++ .../bedrock/screen/ui/NativeUiImagesTest.java | 166 +++ @@ -968,7 +1052,7 @@ also verifies that a zero-width destination emits no graphics calls. .../screen/ui/NativeUiMultilineInputTest.java | 210 ++++ .../screen/ui/NativeUiPlayerCameraTest.java | 132 +++ .../screen/ui/NativeUiPresentationTest.java | 258 +++++ - .../screen/ui/NativeUiRendererTest.java | 173 +++ + .../screen/ui/NativeUiRendererTest.java | 201 ++++ .../NativeUiSceneScreenInteractionTest.java | 359 ++++++ .../bedrock/screen/ui/NativeUiSceneTest.java | 40 + .../screen/ui/NativeUiScissorTest.java | 69 ++ @@ -978,15 +1062,19 @@ also verifies that a zero-width destination emits no graphics calls. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ + .../resources/font/native-outline-fixture.ttf | Bin 0 -> 480 bytes + .../resources/font/native-outline-fixture.txt | 4 + .../native-scrollbar-visibility.json | 162 +++ .../resources/native-ui/scissor-2225.json | 622 ++++++++++ - 127 files changed, 14971 insertions(+), 37 deletions(-) + 135 files changed, 16058 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearance.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiActorRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiScissor.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinNativeUiInput.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockBitmapFont.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockBitmapGlyphRenderState.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockTrueTypeAtlas.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockTrueTypeFont.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinGuiEntityRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeHudOverlays.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeUiExtraction.java @@ -1001,6 +1089,8 @@ also verifies that a zero-width destination emits no graphics calls. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapFont.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapGlyph.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapPipeline.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeFont.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeRaster.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockHudOverlays.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockNativeUiHurt.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeHudOverlayState.java @@ -1044,12 +1134,14 @@ also verifies that a zero-width destination emits no graphics calls. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/settings/base/NativeUiScale.java create mode 100644 src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_bitmap_font.fsh create mode 100644 src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_bitmap_font.vsh + create mode 100644 src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_truetype_font.fsh create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBitmapFontResourcesTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearanceTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorContextTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapFontTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapGlyphTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapShaderGpuTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeFontTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/AnimationPrograms.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockNativeUiHurtTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeHudOverlayStateTest.java @@ -1083,6 +1175,8 @@ also verifies that a zero-width destination emits no graphics calls. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiTextureAnimationTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiToggleTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiViewportTest.java + create mode 100644 src/test/resources/font/native-outline-fixture.ttf + create mode 100644 src/test/resources/font/native-outline-fixture.txt create mode 100644 src/test/resources/native-scrollbar-visibility.json create mode 100644 src/test/resources/native-ui/scissor-2225.json @@ -1527,10 +1621,10 @@ index 0000000..deb4aa5 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockBitmapGlyphRenderState.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockBitmapGlyphRenderState.java new file mode 100644 -index 0000000..3371316 +index 0000000..f28fc2e --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockBitmapGlyphRenderState.java -@@ -0,0 +1,27 @@ +@@ -0,0 +1,29 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1540,6 +1634,7 @@ index 0000000..3371316 + +import com.mojang.renderpearl.api.textures.FilterMode; +import com.viaversion.viafabricplus.bedrock.render.font.BedrockBitmapPipeline; ++import com.viaversion.viafabricplus.bedrock.render.font.BedrockTrueTypeFont; +import net.minecraft.client.gui.font.TextRenderable; +import net.minecraft.client.renderer.state.gui.GlyphRenderState; +import org.spongepowered.asm.mixin.Final; @@ -1555,6 +1650,7 @@ index 0000000..3371316 + @ModifyArg(method = "textureSetup", at = @At(value = "INVOKE", target = + "Lcom/mojang/blaze3d/systems/SamplerCache;getClampToEdge(Lcom/mojang/renderpearl/api/textures/FilterMode;)Lcom/mojang/renderpearl/api/textures/GpuSampler;"), index = 0) + private FilterMode useNativeUnicodeSampling(final FilterMode original) { ++ if (BedrockTrueTypeFont.usesNativeSampling(this.renderable)) return FilterMode.NEAREST; + return BedrockBitmapPipeline.isSmoothed(this.renderable.guiPipeline()) ? FilterMode.LINEAR : original; + } +} @@ -1578,6 +1674,69 @@ index f70a0c9..f94612e 100644 } } } +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockTrueTypeAtlas.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockTrueTypeAtlas.java +new file mode 100644 +index 0000000..25417b5 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockTrueTypeAtlas.java +@@ -0,0 +1,23 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.injection.mixin.features.render; ++ ++import com.mojang.blaze3d.font.GlyphBitmap; ++import com.mojang.blaze3d.font.GlyphInfo; ++import com.viaversion.viafabricplus.bedrock.render.font.BedrockTrueTypeFont; ++import net.minecraft.client.gui.font.FontTexture; ++import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.injection.Constant; ++import org.spongepowered.asm.mixin.injection.ModifyConstant; ++ ++@Mixin(FontTexture.class) ++public abstract class MixinBedrockTrueTypeAtlas { ++ // Native glyph quads use exact cache boundaries before nearest sampling. ++ @ModifyConstant(method = "add", constant = @Constant(floatValue = 0.01F)) ++ private float retainNativeTextureBounds(final float inset, final GlyphInfo info, final GlyphBitmap bitmap) { ++ return BedrockTrueTypeFont.usesNativeSampling(bitmap) ? 0 : inset; ++ } ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockTrueTypeFont.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockTrueTypeFont.java +new file mode 100644 +index 0000000..323d914 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockTrueTypeFont.java +@@ -0,0 +1,28 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.injection.mixin.features.render; ++ ++import com.mojang.blaze3d.font.GlyphProvider; ++import com.viaversion.viafabricplus.bedrock.render.font.BedrockTrueTypeFont; ++import java.io.IOException; ++import net.minecraft.client.gui.font.providers.TrueTypeGlyphProviderDefinition; ++import net.minecraft.server.packs.resources.ResourceManager; ++import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.injection.At; ++import org.spongepowered.asm.mixin.injection.Inject; ++import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; ++ ++@Mixin(TrueTypeGlyphProviderDefinition.class) ++public abstract class MixinBedrockTrueTypeFont { ++ @Inject(method = "load", at = @At("HEAD"), cancellable = true) ++ private void retainNativeRaster(final ResourceManager resources, final CallbackInfoReturnable result) throws IOException { ++ final var definition = (TrueTypeGlyphProviderDefinition) (Object) this; ++ if (BedrockTrueTypeFont.owns(definition.location())) { ++ final var font = BedrockTrueTypeFont.load(resources, definition.location(), definition.skip()); ++ if (font != null) result.setReturnValue(font); ++ } ++ } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinEntityRenderDispatcher.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinEntityRenderDispatcher.java index c343816..94877d4 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinEntityRenderDispatcher.java @@ -2997,10 +3156,10 @@ index 0000000..c8d0c36 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapGlyph.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapGlyph.java new file mode 100644 -index 0000000..392b627 +index 0000000..c79fb7a --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapGlyph.java -@@ -0,0 +1,105 @@ +@@ -0,0 +1,127 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3023,7 +3182,7 @@ index 0000000..392b627 +import net.minecraft.network.chat.Style; +import org.joml.Matrix4fc; + -+/** Carries native cell bounds through GUI text while retaining Java's styled glyph geometry. */ ++/** Carries native cell geometry through GUI text and preserves Java's world-text path. */ +final class BedrockBitmapGlyph implements BakedGlyph { + private final BakedSheetGlyph glyph; + private final Cell cell; @@ -3041,27 +3200,35 @@ index 0000000..392b627 + @Override + public TextRenderable.Styled createGlyph(final float x, final float y, final int color, final int shadowColor, + final Style style, final float boldOffset, final float shadowOffset) { -+ return new Instance(this.glyph.createGlyph(x, y, color, shadowColor, style, boldOffset, shadowOffset), this.cell, ++ final var source = this.glyph.createGlyph(x, y, color, shadowColor, style, boldOffset, shadowOffset); ++ final var gui = style.isItalic() ++ ? this.glyph.createGlyph(x, y, color, shadowColor, style.withItalic(false), boldOffset, shadowOffset) : source; ++ return new Instance(source, gui, this.cell, style.isItalic(), + this.smooth && NativeUiViewport.current().scale() == 1); + } + -+ private record Instance(TextRenderable.Styled source, Cell cell, boolean smooth) implements TextRenderable.Styled { ++ private record Instance(TextRenderable.Styled source, TextRenderable.Styled gui, Cell cell, boolean italic, ++ boolean smooth) implements TextRenderable.Styled { + @Override + public void render(final Matrix4fc pose, final VertexConsumer vertices, final int light, final boolean inverseDepth) { -+ this.source.render(pose, inverseDepth ? new CellVertices(vertices, this.cell, Minecraft.getInstance().getWindow().getGuiScale()) : vertices, light, inverseDepth); ++ if (inverseDepth) { ++ this.gui.render(pose, new CellVertices(vertices, this.cell, Minecraft.getInstance().getWindow().getGuiScale(), this.italic), light, true); ++ } else { ++ this.source.render(pose, vertices, light, false); ++ } + } + + @Override public RenderType renderType(final Font.DisplayMode mode) { return this.source.renderType(mode); } + @Override public GpuTextureView textureView() { return this.source.textureView(); } + @Override public RenderPipeline guiPipeline() { return this.smooth ? BedrockBitmapPipeline.GUI_SMOOTH : BedrockBitmapPipeline.GUI; } + @Override public Style style() { return this.source.style(); } -+ @Override public float left() { return this.source.left(); } ++ @Override public float left() { return this.italic ? Math.min(this.source.left(), this.gui.left() - 1) : this.source.left(); } + @Override public float top() { return this.source.top(); } -+ @Override public float right() { return this.source.right(); } ++ @Override public float right() { return this.italic ? Math.max(this.source.right(), this.gui.right() + 1) : this.source.right(); } + @Override public float bottom() { return this.source.bottom(); } -+ @Override public float activeLeft() { return this.source.activeLeft(); } ++ @Override public float activeLeft() { return this.italic ? Math.min(this.source.activeLeft(), this.gui.activeLeft() - 1) : this.source.activeLeft(); } + @Override public float activeTop() { return this.source.activeTop(); } -+ @Override public float activeRight() { return this.source.activeRight(); } ++ @Override public float activeRight() { return this.italic ? Math.max(this.source.activeRight(), this.gui.activeRight() + 1) : this.source.activeRight(); } + @Override public float activeBottom() { return this.source.activeBottom(); } + } + @@ -3075,14 +3242,28 @@ index 0000000..392b627 + * Carries native vertex precision and top-edge coverage into Java's offscreen GUI target. + * One native 8-bit raster step selects the top half-pixel edge; the shader cancels its UV displacement. + */ -+ private record CellVertices(VertexConsumer target, Cell cell, int guiScale) implements VertexConsumer { ++ private static final class CellVertices implements VertexConsumer { ++ private final VertexConsumer target; ++ private final Cell cell; ++ private final int guiScale; ++ private final boolean italic; ++ private int vertex; ++ ++ private CellVertices(final VertexConsumer target, final Cell cell, final int guiScale, final boolean italic) { ++ this.target = target; ++ this.cell = cell; ++ this.guiScale = guiScale; ++ this.italic = italic; ++ } + @Override public VertexConsumer addVertex(final float x, final float y, final float z) { + this.target.addVertex(rasterSnap(x, this.guiScale), + rasterSnap(y, this.guiScale) - 1F / (256 * this.guiScale), z); + return this; + } + @Override public VertexConsumer addVertex(final Matrix4fc pose, final float x, final float y, final float z) { -+ return VertexConsumer.super.addVertex(pose, half(x), half(y), half(z)); ++ final int corner = this.vertex++ & 3; ++ final float shear = this.italic ? (corner == 0 || corner == 3 ? 1 : -1) : 0; ++ return VertexConsumer.super.addVertex(pose, half(x + shear), half(y), half(z)); + } + private static float half(final float value) { + return Float.float16ToFloat(Float.floatToFloat16(value)); @@ -3161,6 +3342,606 @@ index 0000000..299cd6d + return BedrockShaderPipelines.register(builder.build(), original); + } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeFont.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeFont.java +new file mode 100644 +index 0000000..6818204 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeFont.java +@@ -0,0 +1,213 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.render.font; ++ ++import com.mojang.blaze3d.font.GlyphBitmap; ++import com.mojang.blaze3d.font.GlyphInfo; ++import com.mojang.blaze3d.font.GlyphProvider; ++import com.mojang.blaze3d.font.UnbakedGlyph; ++import com.mojang.blaze3d.platform.NativeImage; ++import com.mojang.blaze3d.systems.RenderSystem; ++import com.mojang.blaze3d.vertex.VertexConsumer; ++import com.mojang.renderpearl.api.pipeline.ColorTargetState; ++import com.mojang.renderpearl.api.pipeline.RenderPipeline; ++import com.mojang.renderpearl.api.textures.GpuTexture; ++import com.mojang.renderpearl.api.textures.GpuTextureView; ++import com.mojang.renderpearl.api.vertex.VertexFormat; ++import com.mojang.serialization.Codec; ++import com.viaversion.viafabricplus.bedrock.render.BedrockShaderPipelines; ++import it.unimi.dsi.fastutil.ints.IntOpenHashSet; ++import it.unimi.dsi.fastutil.ints.IntSet; ++import it.unimi.dsi.fastutil.ints.IntSets; ++import java.io.IOException; ++import java.util.Optional; ++import net.minecraft.client.gui.Font; ++import net.minecraft.client.gui.font.TextRenderable; ++import net.minecraft.client.gui.font.glyphs.BakedGlyph; ++import net.minecraft.client.renderer.RenderPipelines; ++import net.minecraft.client.renderer.rendertype.RenderType; ++import net.minecraft.network.chat.Style; ++import net.minecraft.resources.Identifier; ++import net.minecraft.server.packs.PackType; ++import net.minecraft.server.packs.metadata.MetadataSectionType; ++import net.minecraft.server.packs.resources.ResourceManager; ++import net.minecraft.server.packs.resources.ResourceMetadata; ++import org.joml.Matrix4fc; ++ ++/** Owns native CPU font resources; Java's stitcher owns the uploaded glyph textures. */ ++public final class BedrockTrueTypeFont implements GlyphProvider { ++ private static final MetadataSectionType LAYOUT = new MetadataSectionType<>("viabedrock:truetype", ++ Codec.intRange(1, 255).fieldOf("render_height").codec()); ++ private final BedrockTrueTypeRaster raster; ++ private final IntSet supported; ++ private boolean closed; ++ ++ BedrockTrueTypeFont(final BedrockTrueTypeRaster raster, final String skip) { ++ this.raster = raster; ++ final var supported = new IntOpenHashSet(raster.supported()); ++ skip.codePoints().forEach(supported::remove); ++ this.supported = IntSets.unmodifiable(supported); ++ } ++ ++ public static boolean owns(final Identifier location) { ++ return location.getNamespace().equals("viabedrock") && location.getPath().matches("server_ui/[0-9a-f]{32}\\.ttf"); ++ } ++ ++ /** The descriptor and bytes must come from the same selected pack. Older exports use vanilla loading. */ ++ public static GlyphProvider load(final ResourceManager resources, final Identifier location, final String skip) throws IOException { ++ final var file = location.withPrefix("font/"); ++ final var resource = resources.getResourceOrThrow(file); ++ final var metadata = resource.source().getResource(PackType.CLIENT_RESOURCES, file.withSuffix(".mcmeta")); ++ if (metadata == null) return null; ++ final Optional height; ++ try (var input = metadata.get()) { height = ResourceMetadata.fromJsonStream(input).getSection(LAYOUT); } ++ if (height.isEmpty()) return null; ++ final byte[] bytes; ++ try (var input = resource.open()) { bytes = input.readNBytes(BedrockTrueTypeRaster.MAX_FONT_BYTES + 1); } ++ final var raster = new BedrockTrueTypeRaster(bytes, height.get()); ++ try { return new BedrockTrueTypeFont(raster, skip); } ++ catch (final RuntimeException | Error failure) { raster.close(); throw failure; } ++ } ++ ++ @Override public IntSet getSupportedGlyphs() { return this.supported; } ++ ++ @Override ++ public synchronized UnbakedGlyph getGlyph(final int codepoint) { ++ if (this.closed || !this.supported.contains(codepoint)) return null; ++ return new Glyph(this, codepoint, GlyphInfo.simple(this.raster.advance(codepoint))); ++ } ++ ++ private synchronized BakedGlyph bake(final UnbakedGlyph.Stitcher stitcher, final int codepoint, final GlyphInfo info) { ++ if (this.closed) throw new IllegalStateException("Native TrueType font is closed"); ++ final BedrockTrueTypeRaster.Glyph glyph; ++ try { glyph = this.raster.raster(codepoint); } ++ catch (final IOException malformed) { return stitcher.getMissing(); } ++ final BakedGlyph baked = stitcher.stitch(info, new Bitmap(glyph)); ++ return new BakedGlyph() { ++ @Override public GlyphInfo info() { return baked.info(); } ++ @Override public TextRenderable.Styled createGlyph(final float x, final float y, final int color, final int shadowColor, ++ final Style style, final float bold, final float shadow) { ++ final var source = baked.createGlyph(x, y, color, shadowColor, style, bold, shadow); ++ final var gui = style.isItalic() ++ ? baked.createGlyph(x, y, color, shadowColor, style.withItalic(false), bold, shadow) : source; ++ return new Instance(source, gui, style.isItalic(), glyph.sampling()); ++ } ++ }; ++ } ++ ++ @Override ++ public synchronized void close() { ++ if (this.closed) return; ++ this.closed = true; ++ this.raster.close(); ++ } ++ ++ public static boolean usesNativeSampling(final TextRenderable renderable) { return renderable instanceof Instance; } ++ ++ public static boolean usesNativeSampling(final GlyphBitmap bitmap) { return bitmap instanceof Bitmap; } ++ ++ private record Bitmap(BedrockTrueTypeRaster.Glyph glyph) implements GlyphBitmap { ++ @Override public int getPixelWidth() { return this.glyph.width(); } ++ @Override public int getPixelHeight() { return this.glyph.height(); } ++ @Override public float getOversample() { return this.glyph.oversample(); } ++ @Override public float getBearingLeft() { return this.glyph.left(); } ++ @Override public float getBearingTop() { return 7 - this.glyph.top(); } ++ @Override public boolean isColored() { return false; } ++ @Override public void upload(final int x, final int y, final GpuTexture texture) { ++ // FontTexture reserves a one-pixel gutter; native cache padding is zero. ++ final int width = Math.min(this.glyph.width() + 1, texture.getWidth(0) - x); ++ final int height = Math.min(this.glyph.height() + 1, texture.getHeight(0) - y); ++ try (var image = new NativeImage(NativeImage.Format.LUMINANCE, width, height, true)) { ++ final var pixels = image.getPixelBytes(); ++ for (int row = 0; row < this.glyph.height(); row++) { ++ pixels.position(row * width).put(this.glyph.pixels(), row * this.glyph.width(), this.glyph.width()); ++ } ++ RenderSystem.getDevice().createCommandEncoder().writeToTexture(texture, image, 0, 0, x, y); ++ } ++ } ++ } ++ ++ /** Native stores cache-space vertices as half floats before the 1.5 display transform. */ ++ private static final class CacheVertices implements VertexConsumer { ++ private final VertexConsumer target; ++ private final boolean italic; ++ private final BedrockTrueTypeRaster.Sampling sampling; ++ private final int width, height; ++ private int vertex; ++ ++ private CacheVertices(final VertexConsumer target, final boolean italic, final BedrockTrueTypeRaster.Sampling sampling, ++ final GpuTextureView texture) { ++ this.target = target; ++ this.italic = italic; ++ this.sampling = sampling; ++ this.width = texture.getWidth(0); ++ this.height = texture.getHeight(0); ++ } ++ @Override public VertexConsumer addVertex(final float x, final float y, final float z) { ++ this.target.addVertex(x, y, z); ++ return this; ++ } ++ @Override public VertexConsumer addVertex(final Matrix4fc pose, final float x, final float y, final float z) { ++ // Java submits top-left, bottom-left, bottom-right, top-right for each glyph quad. ++ final int corner = this.vertex++ & 3; ++ final float shear = this.italic ? (corner == 0 || corner == 3 ? 1 : -1) : 0; ++ this.target.addVertex(pose, cacheCoordinate(x, shear), cacheCoordinate(y, 0), z); ++ return this; ++ } ++ private static float cacheCoordinate(final float value, final float shear) { ++ return Float.float16ToFloat(Float.floatToFloat16(value / 1.5F + shear)) * 1.5F; ++ } ++ @Override public VertexConsumer setColor(final int r, final int g, final int b, final int a) { this.target.setColor(r, g, b, a); return this; } ++ @Override public VertexConsumer setColor(final int color) { this.target.setColor(color); return this; } ++ @Override public VertexConsumer setUv(final float u, final float v) { ++ final int corner = (this.vertex - 1) & 3; ++ this.target.setUv(u + this.sampling.correction(true, corner == 2 || corner == 3) / this.width, ++ v + this.sampling.correction(false, corner == 1 || corner == 2) / this.height); ++ return this; ++ } ++ @Override public VertexConsumer setUv1(final int u, final int v) { this.target.setUv1(u, v); return this; } ++ @Override public VertexConsumer setUv2(final int u, final int v) { this.target.setUv2(u, v); return this; } ++ @Override public VertexConsumer setUv3(final float u, final float v) { this.target.setUv3(u, v); return this; } ++ @Override public VertexConsumer setNormal(final float x, final float y, final float z) { this.target.setNormal(x, y, z); return this; } ++ @Override public VertexConsumer setLineWidth(final float width) { this.target.setLineWidth(width); return this; } ++ } ++ ++ private record Glyph(BedrockTrueTypeFont font, int codepoint, GlyphInfo info) implements UnbakedGlyph { ++ @Override public BakedGlyph bake(final Stitcher stitcher) { return this.font.bake(stitcher, this.codepoint, this.info); } ++ } ++ ++ private record Instance(TextRenderable.Styled source, TextRenderable.Styled gui, boolean italic, ++ BedrockTrueTypeRaster.Sampling sampling) implements TextRenderable.Styled { ++ @Override public void render(final Matrix4fc pose, final VertexConsumer vertices, final int light, final boolean inverseDepth) { ++ if (inverseDepth) this.gui.render(pose, new CacheVertices(vertices, this.italic, this.sampling, this.textureView()), light, true); ++ else this.source.render(pose, vertices, light, false); ++ } ++ @Override public RenderType renderType(final Font.DisplayMode mode) { return this.source.renderType(mode); } ++ @Override public GpuTextureView textureView() { return this.source.textureView(); } ++ @Override public RenderPipeline guiPipeline() { return Pipeline.GUI; } ++ @Override public Style style() { return this.source.style(); } ++ // Bounds cover both native GUI shear and the retained Java world-text geometry. ++ @Override public float left() { return this.italic ? Math.min(this.source.left(), this.gui.left() - 1.5F) : this.source.left(); } ++ @Override public float top() { return this.source.top(); } ++ @Override public float right() { return this.italic ? Math.max(this.source.right(), this.gui.right() + 1.5F) : this.source.right(); } ++ @Override public float bottom() { return this.source.bottom(); } ++ } ++ ++ private static final class Pipeline { ++ static final RenderPipeline GUI = create(); ++ private static RenderPipeline create() { ++ final var original = RenderPipelines.GUI_TEXT; ++ final var colors = original.getColorTargetStates().toArray(ColorTargetState[]::new); ++ final var snippet = new RenderPipeline.Snippet(original.getShaders(), Optional.of(original.getShaderDefines()), ++ Optional.of(original.getBindGroupLayouts()), colors, colors.length, Optional.ofNullable(original.getDepthStencilState()), ++ Optional.of(original.getPolygonMode()), Optional.of(original.isCull()), ++ original.getVertexFormatBindings().toArray(VertexFormat[]::new), Optional.of(original.getPrimitiveTopology()), original.pushConstantSize()); ++ return BedrockShaderPipelines.register(RenderPipeline.builder(snippet) ++ .withLocation(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "pipeline/truetype_font")) ++ .withFragmentShader(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "core/bedrock_truetype_font")).build(), original, 0); ++ } ++ } ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeRaster.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeRaster.java +new file mode 100644 +index 0000000..7a6265e +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeRaster.java +@@ -0,0 +1,375 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.render.font; ++ ++import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; ++import it.unimi.dsi.fastutil.ints.IntSet; ++import it.unimi.dsi.fastutil.ints.IntSets; ++import java.io.IOException; ++import java.nio.ByteBuffer; ++import java.nio.ByteOrder; ++import java.util.ArrayList; ++import java.util.HashMap; ++import it.unimi.dsi.fastutil.ints.IntOpenHashSet; ++import org.lwjgl.util.freetype.FT_Matrix; ++import java.util.Map; ++import org.lwjgl.stb.STBTTBitmap; ++import org.lwjgl.stb.STBTTVertex; ++import org.lwjgl.stb.STBTruetype; ++import org.lwjgl.system.MemoryStack; ++import org.lwjgl.system.MemoryUtil; ++import org.lwjgl.util.freetype.FT_CharMap; ++import org.lwjgl.util.freetype.FT_Face; ++import org.lwjgl.util.freetype.FT_Outline; ++import org.lwjgl.util.freetype.FreeType; ++ ++/** FreeType owns file parsing; STB receives only checked outline vertices, never font bytes. */ ++public final class BedrockTrueTypeRaster implements AutoCloseable { ++ public static final int MAX_FONT_BYTES = 16 * 1024 * 1024; ++ private static final int MAX_SIDE = 2048; ++ private final Tables tables; ++ private final int height; ++ private final float scale; ++ private final Int2IntOpenHashMap glyphs = new Int2IntOpenHashMap(); ++ private final Map pages = new HashMap<>(); ++ private ByteBuffer data; ++ private long library; ++ private FT_Face face; ++ private boolean closed; ++ ++ public BedrockTrueTypeRaster(final byte[] bytes, final int height) throws IOException { ++ if (height < 1 || height > 255) throw new IOException("Invalid native font render height"); ++ if (bytes.length < 12 || bytes.length > MAX_FONT_BYTES) throw new IOException("Native font exceeds file bounds"); ++ this.height = height; ++ try (var stack = MemoryStack.stackPush()) { ++ this.data = MemoryUtil.memAlloc(bytes.length).put(bytes).flip(); ++ this.tables = new Tables(this.data.asReadOnlyBuffer()); ++ this.scale = (float) height / (this.tables.ascent - this.tables.descent); ++ final var pointer = stack.mallocPointer(1); ++ if (FreeType.FT_Init_FreeType(pointer) != 0) throw new IOException("Cannot initialize native font reader"); ++ this.library = pointer.get(0); ++ if (FreeType.FT_New_Memory_Face(this.library, this.data, 0, pointer) != 0) throw new IOException("Invalid native TrueType font"); ++ this.face = FT_Face.create(pointer.get(0)); ++ if (this.face.num_glyphs() != this.tables.offsets.length - 1) throw new IOException("Inconsistent native font glyph count"); ++ FT_CharMap selected = null; ++ for (int index = 0; index < this.face.num_charmaps(); index++) { ++ final var map = FT_CharMap.create(this.face.charmaps().get(index)); ++ if (map.platform_id() == 0 || map.platform_id() == 3 && (map.encoding_id() == 1 || map.encoding_id() == 10)) selected = map; ++ } ++ if (selected == null || FreeType.FT_Set_Charmap(this.face, selected) != 0) throw new IOException("Native font has no Unicode map"); ++ final var glyph = stack.mallocInt(1); ++ long code = FreeType.FT_Get_First_Char(this.face, glyph); ++ while (glyph.get(0) != 0) { ++ if (code < 0 || code > 0x10FFFF || glyph.get(0) >= this.tables.offsets.length - 1) throw new IOException("Invalid native font character mapping"); ++ this.glyphs.put((int) code, glyph.get(0)); ++ final long next = FreeType.FT_Get_Next_Char(this.face, code, glyph); ++ if (glyph.get(0) != 0 && next <= code) throw new IOException("Unordered native font character mapping"); ++ code = next; ++ } ++ } catch (final IOException | RuntimeException | Error failure) { ++ this.close(); ++ throw failure; ++ } ++ } ++ ++ public IntSet supported() { return IntSets.unmodifiable(this.glyphs.keySet()); } ++ ++ public synchronized float advance(final int codepoint) { ++ this.requireOpen(); ++ return this.tables.advance(this.glyphs.get(codepoint)) * this.scale * (8F / this.height) * 1.5F; ++ } ++ ++ public synchronized Glyph raster(final int codepoint) throws IOException { ++ this.requireOpen(); ++ final int glyph = this.glyphs.get(codepoint); ++ if (glyph == 0) throw new IOException("Native font does not contain this character"); ++ final Box box = this.tables.box(glyph); ++ final float rasterScale = this.scale * 2; ++ final int x0 = (int) Math.floor(box.left * rasterScale); ++ final int y0 = (int) Math.floor(-box.top * rasterScale); ++ final int x1 = (int) Math.ceil(box.right * rasterScale); ++ final int y1 = (int) Math.ceil(-box.bottom * rasterScale); ++ final int width = x1 - x0 + 1; ++ final int rows = y1 - y0 + 1; ++ if (width < 1 || rows < 1 || width > MAX_SIDE || rows > MAX_SIDE) throw new IOException("Native font glyph exceeds raster bounds"); ++ final Sampling sampling = this.sampling(codepoint); ++ final ByteBuffer pixels = MemoryUtil.memCalloc(width * rows); ++ try (var bitmap = STBTTBitmap.malloc(); var vertices = this.vertices(glyph, new IntOpenHashSet())) { ++ for (final var vertex : vertices) { ++ if (!box.contains(vertex.x(), vertex.y()) || vertex.type() == 3 && !box.contains(vertex.cx(), vertex.cy())) throw new IOException("Native font outline exceeds declared bounds"); ++ } ++ if (vertices.hasRemaining() && width > 1 && rows > 1) { ++ bitmap.set(width - 1, rows - 1, width, pixels); ++ STBTruetype.stbtt_Rasterize(bitmap, .35F, vertices, rasterScale, rasterScale, 0, 0, x0, y0, true); ++ } ++ // Native cache uses a 2-by-2 box prefilter with a zero sample before each row/column. ++ for (int y = 0; y < rows; y++) { ++ int previous = 0; ++ for (int x = 0; x < width; x++) { ++ final int at = y * width + x; ++ final int value = pixels.get(at) & 255; ++ pixels.put(at, (byte) ((previous + value) >> 1)); ++ previous = value; ++ } ++ } ++ for (int x = 0; x < width; x++) { ++ int previous = 0; ++ for (int y = 0; y < rows; y++) { ++ final int at = y * width + x; ++ final int value = pixels.get(at) & 255; ++ pixels.put(at, (byte) ((previous + value) >> 1)); ++ previous = value; ++ } ++ } ++ final byte[] image = new byte[width * rows]; ++ pixels.get(image); ++ final float normalization = 8F / this.height; ++ final float baseline = (8 + (int) (this.tables.descent * this.scale) * normalization) * 1.5F; ++ return new Glyph(width, rows, (x0 * .5F - .25F) * normalization * 1.5F, ++ baseline + (y0 * .5F - .25F) * normalization * 1.5F, ++ this.advance(codepoint), this.height / 6F, image, sampling); ++ } finally { ++ MemoryUtil.memFree(pixels); ++ } ++ } ++ ++ /** Native packs each 256-codepoint page in codepoint order, including one missing-glyph cell. */ ++ private Sampling sampling(final int codepoint) throws IOException { ++ final int page = codepoint >>> 8; ++ final Sampling[] cached = this.pages.get(page); ++ if (cached != null) return cached[codepoint & 255]; ++ final int[][] sizes = new int[256][2]; ++ boolean missing = false; ++ final float rasterScale = this.scale * 2; ++ for (int index = 0; index < 256; index++) { ++ final int glyph = this.glyphs.get((page << 8) + index); ++ if (glyph == 0 && missing) continue; ++ missing |= glyph == 0; ++ final Box box = this.tables.box(glyph); ++ sizes[index][0] = (int) Math.ceil(box.right * rasterScale) - (int) Math.floor(box.left * rasterScale) + 2; ++ sizes[index][1] = (int) Math.ceil(-box.bottom * rasterScale) - (int) Math.floor(-box.top * rasterScale) + 2; ++ } ++ // These are the target client's candidates, including its non-power-of-two final page. ++ for (final int side : new int[]{256, 512, 1024, 2048, 4086}) { ++ final Sampling[] cells = new Sampling[256]; ++ int x = 0, y = 0, bottom = 0; ++ boolean complete = true; ++ for (int index = 0; index < 256; index++) { ++ final int width = sizes[index][0], height = sizes[index][1]; ++ if (width > side || height > side) { complete = false; break; } ++ if (x + width >= side) { x = 0; y = bottom; } ++ if (y + height >= side) { complete = false; break; } ++ cells[index] = new Sampling(side, x + 1, y + 1, width - 1, height - 1); ++ x += width; ++ bottom = Math.max(bottom, y + height); ++ } ++ if (complete) { ++ this.pages.put(page, cells); ++ return cells[codepoint & 255]; ++ } ++ } ++ throw new IOException("Native font page exceeds atlas bounds"); ++ } ++ ++ private STBTTVertex.Buffer vertices(final int glyph, final IntSet parents) throws IOException { ++ if (parents.size() >= 16 || !parents.add(glyph)) throw new IOException("Cyclic or excessive native glyph composition"); ++ try { ++ final int flags = FreeType.FT_LOAD_NO_SCALE | FreeType.FT_LOAD_NO_HINTING | FreeType.FT_LOAD_NO_BITMAP ++ | FreeType.FT_LOAD_IGNORE_TRANSFORM | FreeType.FT_LOAD_NO_RECURSE; ++ if (FreeType.FT_Load_Glyph(this.face, glyph, flags) != 0) throw new IOException("Cannot read native font glyph"); ++ final var slot = this.face.glyph(); ++ if (slot.format() == FreeType.FT_GLYPH_FORMAT_OUTLINE) return outlineVertices(slot.outline()); ++ if (slot.format() != FreeType.FT_GLYPH_FORMAT_COMPOSITE) throw new IOException("Unsupported native glyph format"); ++ final var components = new ArrayList(); ++ try (var stack = MemoryStack.stackPush()) { ++ final var index = stack.mallocInt(1); ++ final var options = stack.mallocInt(1); ++ final var x = stack.mallocInt(1); ++ final var y = stack.mallocInt(1); ++ final var matrix = FT_Matrix.malloc(stack); ++ for (int child = 0; ; child++) { ++ if (FreeType.FT_Get_SubGlyph_Info(slot, child, index, options, x, y, matrix) != 0) break; ++ if (child >= 1024 || (options.get(0) & 2) == 0 || index.get(0) < 0 || index.get(0) >= this.tables.offsets.length - 1) throw new IOException("Unsupported native glyph component"); ++ components.add(new Component(index.get(0), x.get(0), y.get(0), matrix.xx() / 65536F, ++ matrix.yx() / 65536F, matrix.xy() / 65536F, matrix.yy() / 65536F)); ++ } ++ } ++ final var vertices = new ArrayList(); ++ for (final var component : components) { ++ try (var child = this.vertices(component.glyph, parents)) { ++ if (vertices.size() + child.remaining() > 24576) throw new IOException("Native glyph composition exceeds vertex bounds"); ++ // Native STB multiplies by these axis lengths in addition to the component matrix. ++ final float m = (float) Math.sqrt(component.a * component.a + component.b * component.b); ++ final float n = (float) Math.sqrt(component.c * component.c + component.d * component.d); ++ for (final var vertex : child) { ++ vertices.add(new Vertex(vertex.type(), ++ (short) (m * (component.a * vertex.x() + component.c * vertex.y() + component.x)), ++ (short) (n * (component.b * vertex.x() + component.d * vertex.y() + component.y)), ++ (short) (m * (component.a * vertex.cx() + component.c * vertex.cy() + component.x)), ++ (short) (n * (component.b * vertex.cx() + component.d * vertex.cy() + component.y)))); ++ } ++ } ++ } ++ final var result = STBTTVertex.calloc(Math.max(1, vertices.size())); ++ for (final var vertex : vertices) vertex(result, vertex.type, vertex.x, vertex.y, vertex.cx, vertex.cy); ++ return result.flip(); ++ } finally { parents.remove(glyph); } ++ } ++ ++ private record Component(int glyph, int x, int y, float a, float b, float c, float d) { } ++ private record Vertex(int type, int x, int y, int cx, int cy) { } ++ ++ // Match native STB's integer midpoints and contour closure after FreeType reads the points. ++ private static STBTTVertex.Buffer outlineVertices(final FT_Outline outline) throws IOException { ++ final int count = outline.n_points(); ++ final int contours = outline.n_contours(); ++ if (count < 0 || count > 8192 || contours < 0 || contours > 2048 || FreeType.FT_Outline_Check(outline) != 0) throw new IOException("Invalid or oversized native font outline"); ++ final var vertices = STBTTVertex.calloc(Math.max(1, count + contours * 2)); ++ try { ++ for (int point = 0; point < count; point++) { ++ final var value = outline.points().get(point); ++ if (value.x() < Short.MIN_VALUE || value.x() > Short.MAX_VALUE || value.y() < Short.MIN_VALUE || value.y() > Short.MAX_VALUE ++ || (outline.tags().get(point) & 3) > 1) throw new IOException("Native font outline exceeds declared bounds"); ++ } ++ int begin = 0; ++ for (int contour = 0; contour < contours; contour++) { ++ final int end = Short.toUnsignedInt(outline.contours().get(contour)); ++ int point = begin; ++ int sx = (int) outline.points().get(point).x(); ++ int sy = (int) outline.points().get(point).y(); ++ final boolean startOff = (outline.tags().get(point) & 1) == 0; ++ final int scx = sx, scy = sy; ++ if (startOff) { ++ if (point == end) throw new IOException("Native font contour has no curve endpoint"); ++ final var next = outline.points().get(point + 1); ++ if ((outline.tags().get(point + 1) & 1) == 0) { ++ sx = (sx + (int) next.x()) >> 1; ++ sy = (sy + (int) next.y()) >> 1; ++ } else { sx = (int) next.x(); sy = (int) next.y(); point++; } ++ } ++ vertex(vertices, 1, sx, sy, 0, 0); ++ boolean off = false; ++ int cx = 0, cy = 0; ++ for (point++; point <= end; point++) { ++ final int x = (int) outline.points().get(point).x(); ++ final int y = (int) outline.points().get(point).y(); ++ if ((outline.tags().get(point) & 1) == 0) { ++ if (off) vertex(vertices, 3, (cx + x) >> 1, (cy + y) >> 1, cx, cy); ++ cx = x; cy = y; off = true; ++ } else { ++ vertex(vertices, off ? 3 : 2, x, y, off ? cx : 0, off ? cy : 0); ++ off = false; ++ } ++ } ++ if (startOff) { ++ if (off) vertex(vertices, 3, (cx + scx) >> 1, (cy + scy) >> 1, cx, cy); ++ vertex(vertices, 3, sx, sy, scx, scy); ++ } else vertex(vertices, off ? 3 : 2, sx, sy, off ? cx : 0, off ? cy : 0); ++ begin = end + 1; ++ } ++ return vertices.flip(); ++ } catch (final IOException | RuntimeException | Error failure) { ++ vertices.free(); ++ throw failure; ++ } ++ } ++ ++ private static void vertex(final STBTTVertex.Buffer vertices, final int type, final int x, final int y, final int cx, final int cy) { ++ final long at = vertices.get().address(); ++ MemoryUtil.memPutShort(at + STBTTVertex.X, (short) x); ++ MemoryUtil.memPutShort(at + STBTTVertex.Y, (short) y); ++ MemoryUtil.memPutShort(at + STBTTVertex.CX, (short) cx); ++ MemoryUtil.memPutShort(at + STBTTVertex.CY, (short) cy); ++ MemoryUtil.memPutByte(at + STBTTVertex.TYPE, (byte) type); ++ } ++ ++ private void requireOpen() { ++ if (this.closed) throw new IllegalStateException("Native TrueType font is closed"); ++ } ++ ++ @Override ++ public synchronized void close() { ++ if (this.closed) return; ++ this.closed = true; ++ if (this.face != null) FreeType.FT_Done_Face(this.face); ++ if (this.library != 0) FreeType.FT_Done_FreeType(this.library); ++ if (this.data != null) MemoryUtil.memFree(this.data); ++ this.pages.clear(); ++ } ++ ++ public record Glyph(int width, int height, float left, float top, float advance, float oversample, byte[] pixels, Sampling sampling) { } ++ public record Sampling(int side, int x, int y, int width, int height) { ++ /** Retain native UNORM16 rounding in texel space when Java relocates the bitmap. */ ++ float correction(final boolean horizontal, final boolean end) { ++ final int texel = horizontal ? this.x + (end ? this.width : 0) : this.y + (end ? this.height : 0); ++ final float uv = texel * (1F / this.side); ++ final int packed = Math.min(65535, (int) (uv * 65536F)); ++ return (packed / 65535F) * this.side - texel; ++ } ++ } ++ private record Box(int left, int bottom, int right, int top) { ++ boolean contains(final int x, final int y) { return x >= this.left && x <= this.right && y >= this.bottom && y <= this.top; } ++ } ++ ++ private static final class Tables { ++ final int ascent, descent, horizontalMetrics; ++ final int[] offsets; ++ final ByteBuffer glyf, hmtx; ++ ++ Tables(final ByteBuffer bytes) throws IOException { ++ try { ++ final var data = bytes.order(ByteOrder.BIG_ENDIAN); ++ final int count = Short.toUnsignedInt(data.getShort(4)); ++ if (data.getInt(0) != 0x10000 || count < 1 || count > 256 || 12L + count * 16L > bytes.capacity()) throw new IOException("Invalid native TrueType directory"); ++ final Map tables = new HashMap<>(); ++ for (int index = 0; index < count; index++) { ++ final int at = 12 + index * 16; ++ final long start = Integer.toUnsignedLong(data.getInt(at + 8)); ++ final long length = Integer.toUnsignedLong(data.getInt(at + 12)); ++ if (start + length > bytes.capacity() || tables.put(data.getInt(at), data.slice((int) start, (int) length).order(ByteOrder.BIG_ENDIAN)) != null) throw new IOException("Invalid native font table span"); ++ } ++ final var head = table(tables, 0x68656164, 54); ++ final var hhea = table(tables, 0x68686561, 36); ++ final var maxp = table(tables, 0x6D617870, 6); ++ final int glyphs = Short.toUnsignedInt(maxp.getShort(4)); ++ this.ascent = hhea.getShort(4); ++ this.descent = hhea.getShort(6); ++ this.horizontalMetrics = Short.toUnsignedInt(hhea.getShort(34)); ++ if (glyphs == 0 || this.ascent <= this.descent || this.horizontalMetrics < 1 || this.horizontalMetrics > glyphs) throw new IOException("Invalid native font metrics"); ++ this.hmtx = table(tables, 0x686D7478, this.horizontalMetrics * 4 + (glyphs - this.horizontalMetrics) * 2); ++ this.glyf = table(tables, 0x676C7966, 0); ++ final int format = head.getShort(50); ++ if (format != 0 && format != 1) throw new IOException("Invalid native glyph offset format"); ++ final var loca = table(tables, 0x6C6F6361, (glyphs + 1) * (format == 0 ? 2 : 4)); ++ this.offsets = new int[glyphs + 1]; ++ long previous = 0; ++ for (int glyph = 0; glyph <= glyphs; glyph++) { ++ final long value = format == 0 ? Short.toUnsignedInt(loca.getShort(glyph * 2)) * 2L : Integer.toUnsignedLong(loca.getInt(glyph * 4)); ++ if (value < previous || value > this.glyf.capacity()) throw new IOException("Invalid native glyph span"); ++ this.offsets[glyph] = (int) value; ++ previous = value; ++ } ++ } catch (final IndexOutOfBoundsException malformed) { throw new IOException("Truncated native font tables", malformed); } ++ } ++ ++ private static ByteBuffer table(final Map tables, final int tag, final int minimum) throws IOException { ++ final var table = tables.get(tag); ++ if (table == null || table.capacity() < minimum) throw new IOException("Missing or truncated native font table"); ++ return table; ++ } ++ ++ int advance(final int glyph) { return Short.toUnsignedInt(this.hmtx.getShort(Math.min(glyph, this.horizontalMetrics - 1) * 4)); } ++ ++ Box box(final int glyph) throws IOException { ++ final int at = this.offsets[glyph]; ++ if (at == this.offsets[glyph + 1]) return new Box(0, 0, 0, 0); ++ if (this.offsets[glyph + 1] - at < 10) throw new IOException("Truncated native glyph header"); ++ final var box = new Box(this.glyf.getShort(at + 2), this.glyf.getShort(at + 4), this.glyf.getShort(at + 6), this.glyf.getShort(at + 8)); ++ if (box.left > box.right || box.bottom > box.top) throw new IOException("Invalid native glyph bounds"); ++ return box; ++ } ++ } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockHudOverlays.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockHudOverlays.java new file mode 100644 index 0000000..df6dfa1 @@ -5395,10 +6176,10 @@ index 0000000..0d64363 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiFont.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiFont.java new file mode 100644 -index 0000000..1960ce6 +index 0000000..6e767d7 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiFont.java -@@ -0,0 +1,66 @@ +@@ -0,0 +1,80 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5427,8 +6208,22 @@ index 0000000..1960ce6 + return new Origin(snap(x, fontScale, uiScale), snap(y + 1, fontScale, uiScale)); + } + ++ public static Origin origin(final NativeUiScene.Node node, final float x, final float y, ++ final float fontScale, final int uiScale) { ++ final float displayScale = node.number("font_origin_scale", 1); ++ if (!Float.isFinite(displayScale) || displayScale <= 0) { ++ throw new IllegalArgumentException("Unsupported native font origin scale"); ++ } ++ final float grid = fontScale * displayScale; ++ if (!Float.isFinite(grid) || grid <= 0) { ++ throw new IllegalArgumentException("Unsupported native font origin grid"); ++ } ++ return origin(x, y, grid, uiScale); ++ } ++ + private static float snap(final float coordinate, final float fontScale, final int uiScale) { -+ return (float) (Math.floor(coordinate * (double) uiScale / fontScale) * fontScale) / uiScale; ++ // Native converts scaled float coordinates to integers before multiplying by the inverse scale. ++ return (int) (coordinate * (uiScale / fontScale)) * (fontScale / uiScale); + } + + public record Origin(float x, float y) { } @@ -7670,7 +8465,7 @@ index 0000000..99d79fd +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRenderer.java new file mode 100644 -index 0000000..476c650 +index 0000000..f64ab0d --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRenderer.java @@ -0,0 +1,182 @@ @@ -7776,7 +8571,7 @@ index 0000000..476c650 + for (final var line : lines) { + final int lineWidth = this.font.width(line); + final int x = alignment.equals("center") ? (width - lineWidth) / 2 : alignment.equals("right") ? width - lineWidth : 0; -+ final var origin = NativeUiFont.origin(box.bounds().x() + x * scale, y, scale, uiScale); ++ final var origin = NativeUiFont.origin(node, box.bounds().x() + x * scale, y, scale, uiScale); + graphics.pose().pushMatrix(); + try { + graphics.pose().translate(origin.x(), origin.y()); @@ -9534,6 +10329,22 @@ index 0000000..1564b4b + vertexColor = Color; + glyphGuiY = Position.y; +} +diff --git a/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_truetype_font.fsh b/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_truetype_font.fsh +new file mode 100644 +index 0000000..ecc427e +--- /dev/null ++++ b/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_truetype_font.fsh +@@ -0,0 +1,10 @@ ++#version 330 ++#extension GL_ARB_separate_shader_objects : require ++#include ++uniform sampler2D Sampler0; ++layout(location = 2) in vec4 vertexColor; ++layout(location = 3) in vec2 texCoord0; ++layout(location = 0) out vec4 fragColor; ++void main() { ++ fragColor = texture(Sampler0, texCoord0).rrrr * vertexColor * ColorModulator; ++} diff --git a/src/main/resources/viafabricplus-bedrock.accesswidener b/src/main/resources/viafabricplus-bedrock.accesswidener index 8e8672c..288cc7a 100644 --- a/src/main/resources/viafabricplus-bedrock.accesswidener @@ -9547,10 +10358,10 @@ index 8e8672c..288cc7a 100644 +accessible field net/minecraft/client/gui/GuiGraphicsExtractor scissorStack Lnet/minecraft/client/gui/GuiGraphicsExtractor$ScissorStack; +accessible field net/minecraft/client/gui/GuiGraphicsExtractor$ScissorStack stack Ljava/util/Deque; diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 16d451d..b4c2ad2 100644 +index 16d451d..0936821 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -150,6 +150,11 @@ +@@ -150,6 +150,13 @@ "features.render.MixinBedrockModelPartTransforms", "features.render.MixinPlayerSkinWidget", "features.render.MixinGuiSkinRenderer", @@ -9558,11 +10369,13 @@ index 16d451d..b4c2ad2 100644 + "features.render.MixinPictureInPictureRenderer", + "features.render.MixinNativeUiPictures", + "features.render.MixinBedrockBitmapFont", ++ "features.render.MixinBedrockTrueTypeAtlas", ++ "features.render.MixinBedrockTrueTypeFont", + "features.render.MixinBedrockBitmapGlyphRenderState", "features.input.MixinKeyboardInput", "features.input.MixinLocalPlayerInput", "features.input.MixinClientInput", -@@ -204,7 +209,13 @@ +@@ -204,7 +211,13 @@ "features.render.MixinBedrockCameraFade", "features.entities.MixinClientEntityTracking", "features.entities.MixinMinecraftProjectileOwners", @@ -10324,10 +11137,10 @@ index 0000000..64784a5 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapGlyphTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapGlyphTest.java new file mode 100644 -index 0000000..f6651d3 +index 0000000..8724755 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapGlyphTest.java -@@ -0,0 +1,71 @@ +@@ -0,0 +1,107 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -10347,6 +11160,8 @@ index 0000000..f6651d3 +import net.minecraft.network.chat.Style; +import org.joml.Matrix4f; +import org.junit.jupiter.api.Test; ++import org.mockito.ArgumentCaptor; ++import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; @@ -10398,6 +11213,40 @@ index 0000000..f6651d3 + verify(world).setUv(cell.u0(), cell.v0()); + verify(world, times(4)).setLight(0x00A00050); + } ++ @Test ++ void usesFixedItalicEdgesForTallBitmapCellsAndPreservesWorldVertices() { ++ final var cell = BedrockBitmapFont.Cell.bitmap(1, 4, 8, 12, 128, 128, -3); ++ final var sheet = new BakedSheetGlyph(GlyphInfo.simple(4), mock(GlyphRenderTypes.class), mock(GpuTextureView.class), ++ cell.u0(), cell.u1(), cell.v0(), cell.v1(), cell.left(), cell.right(), cell.top(), cell.bottom()); ++ final var style = Style.EMPTY.withItalic(true); ++ final var glyph = new BedrockBitmapGlyph(sheet, cell, false).createGlyph(0, 0, -1, 0, style, 0, 0); ++ final var pose = new Matrix4f(); ++ final var client = mock(Minecraft.class); ++ final var window = mock(Window.class); ++ when(client.getWindow()).thenReturn(window); ++ when(window.getGuiScale()).thenReturn(1); ++ final var gui = mock(VertexConsumer.class, RETURNS_SELF); ++ try (final var minecraft = mockStatic(Minecraft.class)) { ++ minecraft.when(Minecraft::getInstance).thenReturn(client); ++ glyph.render(pose, gui, 0, true); ++ } ++ final var guiX = ArgumentCaptor.forClass(Float.class); ++ verify(gui, times(4)).addVertex(guiX.capture(), anyFloat(), anyFloat()); ++ // Original 1.26.60.5 bitmap writer and quad expansion for height 12 and offset -3. ++ assertEquals(List.of(1F, -1F, 7F, 9F), guiX.getAllValues()); ++ assertTrue(glyph.left() <= -1); ++ assertTrue(glyph.right() >= 9); ++ assertTrue(glyph.activeLeft() <= -1); ++ assertTrue(glyph.activeRight() >= 5); ++ ++ final var world = mock(VertexConsumer.class, RETURNS_SELF); ++ glyph.render(pose, world, 0, false); ++ final var worldX = ArgumentCaptor.forClass(Float.class); ++ verify(world, times(4)).addVertex(same(pose), worldX.capture(), anyFloat(), anyFloat()); ++ assertEquals(List.of(1.75F, -1.25F, 6.75F, 9.75F), worldX.getAllValues()); ++ assertNotEquals(guiX.getAllValues(), worldX.getAllValues()); ++ } ++ +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapShaderGpuTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapShaderGpuTest.java new file mode 100644 @@ -10696,6 +11545,341 @@ index 0000000..0e8ffc2 + } catch (final IOException error) { throw new IllegalStateException(error); } + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeFontTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeFontTest.java +new file mode 100644 +index 0000000..29743c7 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockTrueTypeFontTest.java +@@ -0,0 +1,329 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.render.font; ++ ++import com.mojang.blaze3d.font.GlyphBitmap; ++import com.mojang.blaze3d.font.GlyphInfo; ++import com.mojang.blaze3d.font.UnbakedGlyph; ++import com.mojang.blaze3d.vertex.VertexConsumer; ++import com.mojang.blaze3d.platform.NativeImage; ++import com.mojang.blaze3d.systems.RenderSystem; ++import com.mojang.renderpearl.api.commands.CommandEncoder; ++import com.mojang.renderpearl.api.device.GpuDevice; ++import com.mojang.renderpearl.api.textures.GpuTexture; ++import com.mojang.renderpearl.api.textures.GpuTextureView; ++import java.io.ByteArrayInputStream; ++import java.io.IOException; ++import java.nio.ByteBuffer; ++import java.nio.ByteOrder; ++import java.util.HexFormat; ++import java.util.Arrays; ++import java.nio.charset.StandardCharsets; ++import net.minecraft.client.gui.font.glyphs.BakedGlyph; ++import net.minecraft.client.gui.font.glyphs.BakedSheetGlyph; ++import net.minecraft.network.chat.Style; ++import org.joml.Matrix4f; ++import org.mockito.Answers; ++import java.util.List; ++import java.util.ArrayList; ++import net.minecraft.resources.Identifier; ++import net.minecraft.server.packs.PackResources; ++import net.minecraft.server.packs.PackType; ++import net.minecraft.server.packs.resources.Resource; ++import net.minecraft.server.packs.resources.ResourceManager; ++import org.junit.jupiter.api.Test; ++import org.mockito.ArgumentCaptor; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.ArgumentMatchers.*; ++import static org.mockito.Mockito.*; ++ ++class BedrockTrueTypeFontTest { ++ // Original Bedrock 1.26.60.5 raster controls for this authored, six-glyph test font. ++ private static final String RECTANGLE = "193333333333230a59b2b2b2b2b27c237fffffffffffb2337fffffffffffb233" ++ + "7fffffffffffb2337fffffffffffb2337fffffffffffb2333f7f7f7f7f7f5919"; ++ private static final String TRANSLATED = "19597f7f7f7f7f3f33b2ffffffffff7f33b2ffffffffff7f33b2ffffffffff7f" ++ + "33b2ffffffffff7f33b2ffffffffff7f237cb2b2b2b2b2590a23333333333319"; ++ ++ private static byte[] fixture() throws IOException { ++ try (var stream = BedrockTrueTypeFontTest.class.getResourceAsStream("/font/native-outline-fixture.ttf")) { ++ assertNotNull(stream); ++ return stream.readAllBytes(); ++ } ++ } ++ ++ private static GpuTextureView texture() { ++ final var texture = mock(GpuTextureView.class); ++ when(texture.getWidth(0)).thenReturn(256); ++ when(texture.getHeight(0)).thenReturn(256); ++ return texture; ++ } ++ ++ @Test ++ void matchesNativeRasterForRectanglesCurvesAndUnshiftedCompositeCoordinates() throws Exception { ++ try (var raster = new BedrockTrueTypeRaster(fixture(), 32)) { ++ final var plain = raster.raster(65); ++ final var translated = raster.raster(67); ++ assertArrayEquals(HexFormat.of().parseHex(RECTANGLE), plain.pixels()); ++ assertArrayEquals(HexFormat.of().parseHex(TRANSLATED), translated.pixels()); ++ assertEquals(8, plain.width()); ++ assertEquals(8, translated.height()); ++ assertEquals(3, plain.advance()); ++ assertEquals(-.09375F, plain.left()); ++ assertEquals(.09375F, translated.left()); ++ final var scaled = raster.raster(68); ++ assertArrayEquals(HexFormat.of().parseHex("0000000000253b16000064a13c00007fcc4c00007fcc4c00003f66260000"), scaled.pixels()); ++ assertEquals(5, scaled.width()); ++ assertEquals(6, scaled.height()); ++ } ++ try (var raster = new BedrockTrueTypeRaster(fixture(), 8)) { ++ final var curve = raster.raster(66); ++ assertArrayEquals(HexFormat.of().parseHex("0000000000000000000004222608002e8c9b3f022a6a753702"), curve.pixels()); ++ assertEquals(5, curve.width()); ++ assertEquals(5, curve.height()); ++ } ++ } ++ ++ @Test ++ void clearsReservedGlyphGuttersAndKeepsUploadsInsideTheAtlas() throws Exception { ++ try (var font = new BedrockTrueTypeFont(new BedrockTrueTypeRaster(fixture(), 32), ""); ++ var system = mockStatic(RenderSystem.class)) { ++ final var texture = mock(GpuTexture.class); ++ when(texture.getWidth(0)).thenReturn(20); ++ when(texture.getHeight(0)).thenReturn(20); ++ final var device = mock(GpuDevice.class); ++ final var encoder = mock(CommandEncoder.class); ++ system.when(RenderSystem::getDevice).thenReturn(device); ++ when(device.createCommandEncoder()).thenReturn(encoder); ++ final var uploads = new ArrayList(); ++ final var dimensions = new ArrayList>(); ++ doAnswer(call -> { ++ final NativeImage image = call.getArgument(1); ++ final byte[] pixels = new byte[image.getWidth() * image.getHeight()]; ++ image.getPixelBytes().get(pixels); ++ uploads.add(pixels); ++ dimensions.add(List.of(image.getWidth(), image.getHeight())); ++ return null; ++ }).when(encoder).writeToTexture(same(texture), any(NativeImage.class), eq(0), eq(0), anyInt(), anyInt()); ++ final var stitcher = mock(UnbakedGlyph.Stitcher.class); ++ when(stitcher.stitch(any(), any())).thenAnswer(call -> { ++ final GlyphBitmap bitmap = call.getArgument(1); ++ bitmap.upload(5, 7, texture); ++ bitmap.upload(12, 12, texture); ++ return mock(BakedGlyph.class); ++ }); ++ font.getGlyph(65).bake(stitcher); ++ assertEquals(List.of(List.of(9, 9), List.of(8, 8)), dimensions); ++ assertArrayEquals(HexFormat.of().parseHex("193333333333230a00" ++ + "59b2b2b2b2b27c2300" + "7fffffffffffb23300" + "7fffffffffffb23300" ++ + "7fffffffffffb23300" + "7fffffffffffb23300" + "7fffffffffffb23300" ++ + "3f7f7f7f7f7f591900" + "000000000000000000"), uploads.getFirst()); ++ assertArrayEquals(HexFormat.of().parseHex(RECTANGLE), uploads.getLast()); ++ } ++ } ++ ++ @Test ++ void preservesNativeAtlasSamplingWhenJavaRelocatesGlyphs() throws Exception { ++ try (var font = new BedrockTrueTypeFont(new BedrockTrueTypeRaster(fixture(), 32), "")) { ++ final var stitcher = mock(UnbakedGlyph.Stitcher.class); ++ final var texture = texture(); ++ when(stitcher.stitch(any(), any())).thenAnswer(call -> { ++ final GlyphInfo info = call.getArgument(0); ++ final GlyphBitmap bitmap = call.getArgument(1); ++ return new BakedSheetGlyph(info, null, texture, 1 / 256F, 9 / 256F, 17 / 256F, 25 / 256F, ++ bitmap.getLeft(), bitmap.getRight(), bitmap.getTop(), bitmap.getBottom()); ++ }); ++ final var glyph = font.getGlyph(65).bake(stitcher).createGlyph(0, 0, -1, 0, Style.EMPTY, 0, 0); ++ final var gui = mock(VertexConsumer.class, Answers.RETURNS_SELF); ++ glyph.render(new Matrix4f(), gui, 0, true); ++ final var u = ArgumentCaptor.forClass(Float.class); ++ final var v = ArgumentCaptor.forClass(Float.class); ++ verify(gui, times(4)).setUv(u.capture(), v.capture()); ++ // Original 1.26.60.5 shelf places A at [5,1..13,9] on a 256 page. ++ // Retain its captured fixed-point sample phase at Java's [1,17..9,25]. ++ assertEquals(List.of(.003906548F, .003906548F, .035157025F, .035157025F), u.getAllValues()); ++ assertEquals(List.of(.06640631F, .09765679F, .09765679F, .06640631F), v.getAllValues()); ++ final var world = mock(VertexConsumer.class, Answers.RETURNS_SELF); ++ glyph.render(new Matrix4f(), world, 0, false); ++ final var worldU = ArgumentCaptor.forClass(Float.class); ++ verify(world, times(4)).setUv(worldU.capture(), anyFloat()); ++ assertEquals(List.of(1 / 256F, 1 / 256F, 9 / 256F, 9 / 256F), worldU.getAllValues()); ++ } ++ } ++ ++ @Test ++ void submitsCachePrecisionBeforeTheDisplayTransformAndPreservesWorldVertices() throws Exception { ++ try (var font = new BedrockTrueTypeFont(new BedrockTrueTypeRaster(fixture(), 32), "")) { ++ final var stitcher = mock(UnbakedGlyph.Stitcher.class); ++ when(stitcher.stitch(any(), any())).thenAnswer(call -> { ++ final GlyphInfo info = call.getArgument(0); ++ final GlyphBitmap bitmap = call.getArgument(1); ++ return new BakedSheetGlyph(info, null, texture(), 0, 1, 0, 1, ++ bitmap.getLeft(), bitmap.getRight(), bitmap.getTop(), bitmap.getBottom()); ++ }); ++ final var glyph = font.getGlyph(65).bake(stitcher).createGlyph(28.325806F, 0, -1, 0, Style.EMPTY, 0, 0); ++ final var pose = new Matrix4f().scaling(2); ++ final var nativeVertices = mock(VertexConsumer.class, Answers.RETURNS_SELF); ++ glyph.render(pose, nativeVertices, 0, true); ++ final var nativeX = ArgumentCaptor.forClass(Float.class); ++ verify(nativeVertices, times(4)).addVertex(same(pose), nativeX.capture(), anyFloat(), anyFloat()); ++ assertEquals(List.of(28.2421875F, 28.2421875F, 29.7421875F, 29.7421875F), nativeX.getAllValues()); ++ final var worldVertices = mock(VertexConsumer.class, Answers.RETURNS_SELF); ++ glyph.render(pose, worldVertices, 0, false); ++ final var worldX = ArgumentCaptor.forClass(Float.class); ++ verify(worldVertices, times(4)).addVertex(same(pose), worldX.capture(), anyFloat(), anyFloat()); ++ assertEquals(28.232056F, worldX.getAllValues().getFirst()); ++ assertNotEquals(nativeX.getAllValues(), worldX.getAllValues()); ++ } ++ } ++ ++ @Test ++ void usesFixedNativeItalicEdgesInGuiAndPreservesWorldGeometry() throws Exception { ++ try (var font = new BedrockTrueTypeFont(new BedrockTrueTypeRaster(fixture(), 32), "")) { ++ final var stitcher = mock(UnbakedGlyph.Stitcher.class); ++ final BakedSheetGlyph[] sheet = new BakedSheetGlyph[1]; ++ when(stitcher.stitch(any(), any())).thenAnswer(call -> { ++ final GlyphInfo info = call.getArgument(0); ++ final GlyphBitmap bitmap = call.getArgument(1); ++ sheet[0] = new BakedSheetGlyph(info, null, texture(), 0, 1, 0, 1, ++ bitmap.getLeft(), bitmap.getRight(), bitmap.getTop(), bitmap.getBottom()); ++ return sheet[0]; ++ }); ++ final var style = Style.EMPTY.withItalic(true); ++ final var glyph = font.getGlyph(65).bake(stitcher).createGlyph(0, 0, -1, 0, style, 0, 0); ++ final var pose = new Matrix4f(); ++ final var gui = mock(VertexConsumer.class, Answers.RETURNS_SELF); ++ glyph.render(pose, gui, 0, true); ++ final var guiX = ArgumentCaptor.forClass(Float.class); ++ verify(gui, times(4)).addVertex(same(pose), guiX.capture(), anyFloat(), anyFloat()); ++ // Native 1.26.60.5 quad expansion: top +1, bottom -1 before the 1.5 display transform. ++ assertEquals(List.of(1.40625F, -1.59375F, -.09375F, 2.90625F), guiX.getAllValues()); ++ assertTrue(glyph.left() <= -1.59375F); ++ assertTrue(glyph.right() >= 2.90625F); ++ assertEquals(style, glyph.style()); ++ final var fractional = font.getGlyph(65).bake(stitcher).createGlyph(.114990324F, 0, -1, 0, style, 0, 0); ++ final var fractionalVertices = mock(VertexConsumer.class, Answers.RETURNS_SELF); ++ fractional.render(pose, fractionalVertices, 0, true); ++ final var fractionalX = ArgumentCaptor.forClass(Float.class); ++ verify(fractionalVertices, times(4)).addVertex(same(pose), fractionalX.capture(), anyFloat(), anyFloat()); ++ // Applying display-space shear before normalization rounds onto the other half-float. ++ assertEquals(1.5205078125F, fractionalX.getAllValues().getFirst()); ++ ++ final var world = mock(VertexConsumer.class, Answers.RETURNS_SELF); ++ final var originalWorld = mock(VertexConsumer.class, Answers.RETURNS_SELF); ++ glyph.render(pose, world, 0, false); ++ sheet[0].createGlyph(0, 0, -1, 0, style, 0, 0).render(pose, originalWorld, 0, false); ++ final var worldX = ArgumentCaptor.forClass(Float.class); ++ final var originalX = ArgumentCaptor.forClass(Float.class); ++ verify(world, times(4)).addVertex(same(pose), worldX.capture(), anyFloat(), anyFloat()); ++ verify(originalWorld, times(4)).addVertex(same(pose), originalX.capture(), anyFloat(), anyFloat()); ++ assertEquals(originalX.getAllValues(), worldX.getAllValues()); ++ assertNotEquals(guiX.getAllValues(), worldX.getAllValues()); ++ } ++ } ++ ++ @Test ++ void ownsItsFontBytesAndRejectsUseAfterCloseAcrossReloads() throws Exception { ++ final byte[] bytes = fixture(); ++ final var raster = new BedrockTrueTypeRaster(bytes, 32); ++ Arrays.fill(bytes, (byte) 0); ++ assertArrayEquals(HexFormat.of().parseHex(RECTANGLE), raster.raster(65).pixels()); ++ raster.close(); ++ raster.close(); ++ assertThrows(IllegalStateException.class, () -> raster.raster(65)); ++ try (var replacement = new BedrockTrueTypeRaster(fixture(), 32)) { ++ assertArrayEquals(HexFormat.of().parseHex(RECTANGLE), replacement.raster(65).pixels()); ++ } ++ } ++ ++ @Test ++ void rejectsUnsignedTableSpansAndBoundsGlyphWorkBeforeRasterization() throws Exception { ++ final byte[] truncated = fixture(); ++ ByteBuffer.wrap(truncated).order(ByteOrder.BIG_ENDIAN).putInt(20, -1); ++ assertThrows(IOException.class, () -> new BedrockTrueTypeRaster(truncated, 32)); ++ final byte[] oversized = fixture(); ++ final var data = ByteBuffer.wrap(oversized).order(ByteOrder.BIG_ENDIAN); ++ final int glyphs = table(data, 0x676C7966); ++ data.putShort(glyphs + 6, Short.MAX_VALUE); ++ try (var raster = new BedrockTrueTypeRaster(oversized, 255)) { ++ assertThrows(IOException.class, () -> raster.raster(65)); ++ } ++ final byte[] inconsistent = fixture(); ++ final var bounds = ByteBuffer.wrap(inconsistent).order(ByteOrder.BIG_ENDIAN); ++ bounds.putShort(table(bounds, 0x676C7966) + 6, (short) 2); ++ try (var raster = new BedrockTrueTypeRaster(inconsistent, 32)) { ++ assertThrows(IOException.class, () -> raster.raster(65)); ++ } ++ } ++ ++ @Test ++ void rejectsCyclicComponentsAndUsesTheStitchersMissingGlyphForMalformedOutlines() throws Exception { ++ final byte[] bytes = fixture(); ++ final var data = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN); ++ final int composite = table(data, 0x676C7966) + data.getInt(table(data, 0x6C6F6361) + 4 * 4); ++ data.putShort(composite + 12, (short) 4); ++ try (var raster = new BedrockTrueTypeRaster(bytes, 32)) { ++ assertThrows(IOException.class, () -> raster.raster(67)); ++ } ++ try (var font = new BedrockTrueTypeFont(new BedrockTrueTypeRaster(bytes, 32), "")) { ++ final var stitcher = mock(UnbakedGlyph.Stitcher.class); ++ final var missing = mock(BakedGlyph.class); ++ when(stitcher.getMissing()).thenReturn(missing); ++ assertSame(missing, font.getGlyph(67).bake(stitcher)); ++ verify(stitcher).getMissing(); ++ verifyNoMoreInteractions(stitcher); ++ } ++ } ++ ++ @Test ++ void bakesNativeMetricsAndRetiresPreviouslyReturnedGlyphs() throws Exception { ++ final var font = new BedrockTrueTypeFont(new BedrockTrueTypeRaster(fixture(), 32), "B"); ++ try { ++ assertNull(font.getGlyph(66)); ++ final var glyph = font.getGlyph(65); ++ assertNotNull(glyph); ++ assertEquals(3, glyph.info().getAdvance()); ++ final var stitcher = mock(UnbakedGlyph.Stitcher.class); ++ when(stitcher.stitch(any(), any())).thenReturn(mock(BakedGlyph.class)); ++ assertNotNull(glyph.bake(stitcher)); ++ final var bitmap = ArgumentCaptor.forClass(GlyphBitmap.class); ++ verify(stitcher).stitch(any(GlyphInfo.class), bitmap.capture()); ++ assertEquals(8, bitmap.getValue().getPixelWidth()); ++ assertEquals(32F / 6, bitmap.getValue().getOversample()); ++ assertEquals(-.09375F, bitmap.getValue().getBearingLeft()); ++ font.close(); ++ assertNull(font.getGlyph(65)); ++ assertThrows(IllegalStateException.class, () -> glyph.bake(stitcher)); ++ } finally { font.close(); } ++ } ++ ++ @Test ++ void usesTheSelectedFontsOwnDescriptorAndKeepsOlderExportsOnVanillaLoading() throws Exception { ++ final var id = Identifier.parse("viabedrock:server_ui/0123456789abcdef0123456789abcdef.ttf"); ++ final var file = id.withPrefix("font/"); ++ final var pack = mock(PackResources.class); ++ final var resources = mock(ResourceManager.class); ++ final byte[] bytes = fixture(); ++ when(resources.getResourceOrThrow(file)).thenReturn(new Resource(pack, () -> new ByteArrayInputStream(bytes))); ++ assertNull(BedrockTrueTypeFont.load(resources, id, "")); ++ when(pack.getResource(PackType.CLIENT_RESOURCES, file.withSuffix(".mcmeta"))).thenReturn(() -> ++ new ByteArrayInputStream("{\"viabedrock:truetype\":{\"render_height\":32}}".getBytes(StandardCharsets.UTF_8))); ++ try (var font = BedrockTrueTypeFont.load(resources, id, "A")) { ++ assertInstanceOf(BedrockTrueTypeFont.class, font); ++ assertNull(font.getGlyph(65)); ++ assertNotNull(font.getGlyph(66)); ++ } ++ verify(resources, times(2)).getResourceOrThrow(file); ++ verifyNoMoreInteractions(resources); ++ } ++ ++ private static int table(final ByteBuffer data, final int tag) { ++ for (int record = 12; record < 12 + data.getShort(4) * 16; record += 16) { ++ if (data.getInt(record) == tag) return data.getInt(record + 8); ++ } ++ throw new AssertionError("Missing test-font table"); ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/AnimationPrograms.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/AnimationPrograms.java new file mode 100644 index 0000000..f0c01a7 @@ -12423,10 +13607,10 @@ index 0000000..f022766 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiFontTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiFontTest.java new file mode 100644 -index 0000000..3f67a8f +index 0000000..edbb2d8 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiFontTest.java -@@ -0,0 +1,148 @@ +@@ -0,0 +1,149 @@ +package com.viaversion.viafabricplus.bedrock.screen.ui; + +import com.google.gson.JsonArray; @@ -12468,6 +13652,7 @@ index 0000000..3f67a8f + } + assertEquals(new NativeUiFont.Origin(335.5F, 36.5F), NativeUiFont.origin(335.5F, 35.5F, .5F, 1)); + assertEquals(new NativeUiFont.Origin(480.5F, 36.5F), NativeUiFont.origin(480.75F, 35.75F, .5F, 1)); ++ assertEquals(new NativeUiFont.Origin(45, 36.333336F), NativeUiFont.origin(45.25F, 35.5F, 1, 3)); + } + + @Test @@ -15174,10 +16359,10 @@ index 0000000..d8bc9ec +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRendererTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRendererTest.java new file mode 100644 -index 0000000..5bf1740 +index 0000000..36fc175 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRendererTest.java -@@ -0,0 +1,173 @@ +@@ -0,0 +1,201 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -15314,6 +16499,34 @@ index 0000000..5bf1740 + } + } + ++ @Test ++ void submitsSelectedTrueTypeLabelsOnTheirDisplayGrid() { ++ try (final var viewport = mockStatic(NativeUiViewport.class)) { ++ viewport.when(NativeUiViewport::current).thenReturn(new NativeUiViewport(640, 360, 2, 2)); ++ final var font = drawingFont(); ++ final var renderer = new NativeUiRenderer(font, mock(NativeUiImages.class)); ++ final var origins = new ArrayList(); ++ final var graphics = mock(GuiGraphicsExtractor.class); ++ final var pose = new Matrix3x2fStack(16); ++ when(graphics.pose()).thenReturn(pose); ++ doAnswer(call -> { ++ origins.add(new NativeUiFont.Origin(pose.m20(), pose.m21())); ++ return null; ++ }).when(graphics).text(eq(font), any(FormattedCharSequence.class), eq(0), eq(0), anyInt(), eq(false)); ++ for (final float[] sample : new float[][] {{10, 300}, {90, 300}, {170, 300}, {-10, -3}, {-.125F, -.125F}}) { ++ final var node = label("W"); ++ node.properties().addProperty("font_origin_scale", 1.5F); ++ final var bounds = new NativeUiLayout.Rect(sample[0], sample[1], 70, 28); ++ final var box = new NativeUiLayout.Box(node, bounds, new NativeUiLayout.Rect(0, 0, 640, 360), ++ new NativeUiLayout.Size(70, 28), bounds, 0, List.of()); ++ renderer.label(graphics, box, 1); ++ } ++ assertEquals(List.of(new NativeUiFont.Origin(9.75F, 300.75F), new NativeUiFont.Origin(90, 300.75F), ++ new NativeUiFont.Origin(169.5F, 300.75F), new NativeUiFont.Origin(-9.75F, -1.5F), ++ new NativeUiFont.Origin(0, .75F)), origins); ++ } ++ } ++ + private static NativeUiLayout.Box box(final NativeUiScene.Node node, final float height) { + final var bounds = new NativeUiLayout.Rect(0, 0, 70, height); + return new NativeUiLayout.Box(node, bounds, new NativeUiLayout.Rect(0, 0, 640, 360), @@ -16489,6 +17702,31 @@ index 0000000..e394a6f + } + } +} +diff --git a/src/test/resources/font/native-outline-fixture.ttf b/src/test/resources/font/native-outline-fixture.ttf +new file mode 100644 +index 0000000000000000000000000000000000000000..f045ee47755c23634e593051f3fb1306eebc88dc +GIT binary patch +literal 480 +zcmZ9Ju}T9$5Qe|GJ!2HX)Or1oH-VwpJoOhv}~L +z0c`B@0yZ`)Gb_gPUzXkZ=bwLuT?ANVKt!5X*@P!)19!mj)%f=Ezmfm&^RTRL33Y*j0tX_ZaRL7Ehu2<;nwm(1ASjgbeY~ebhZ!>*k>0H{ +z>~O?LD|FyAxCAH8H}^lmE!@JKo6tT1K`r1C-v_=L{1Pq8X!ieZq6hX?r}`)6-Xcb9 +I+njNh-Ed;kCd + +literal 0 +HcmV?d00001 + +diff --git a/src/test/resources/font/native-outline-fixture.txt b/src/test/resources/font/native-outline-fixture.txt +new file mode 100644 +index 0000000..6f2f4d5 +--- /dev/null ++++ b/src/test/resources/font/native-outline-fixture.txt +@@ -0,0 +1,4 @@ ++This authored TrueType fixture contains six glyphs: .notdef, space, a 100-unit square, a quadratic arch, the square translated by (25,25), and a scaled square. ++Its Unicode map assigns space and A/B/C/D. The hhea range is 800 to -200 and each advance is 250 units. ++The composite has a zero horizontal bearing, which distinguishes raw native outline coordinates from FreeType's expanded bearing adjustment. ++Golden pixels come from the original Bedrock 1.26.60.5 cache raster instructions at heights 8 and 32. diff --git a/src/test/resources/native-scrollbar-visibility.json b/src/test/resources/native-scrollbar-visibility.json new file mode 100644 index 0000000..da3913b diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index bc2784de..f389a549 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -66,7 +66,7 @@ Java GUI scales 1, 2, 3, and return to 1 retain those masks. These controls cover short heights, scaled labels, explicit newlines, automatic wrapping, and overflow markers. At the same viewport and scale controls, all 24 installed MinecraftTen cases admit the native number of lines. -Their glyph masks still differ. These controls establish line admission, not complete TrueType raster parity. +These controls establish line admission. The current raster comparisons appear below. Labels preserve carriage-return glyphs during measurement and drawing, including before LF. NUL terminates label traversal across styled runs and excludes suffixes from measurement. @@ -82,7 +82,38 @@ Java GUI scales 1, 2, 3, and return to 1 retain that match and all 63 selected b The background comparison excludes text bounds and the separate action button. The complete 180-second replay preserves the source scene payload and verifies the installed add-on bytes. -Other TrueType families, glyph raster appearance, native font-type aliases, and complete screen appearance remain open. +Selected TrueType labels use their native 1.5 display grid. +Origin conversion follows native float operations and truncates toward zero, including negative coordinates. +Regressions cover submitted draw positions and the UI-scale-3 precision boundary. + +TrueType labels use FreeType to read font files and STB to turn checked outlines into pixels. +Font bytes never reach STB. The provider reads the cache height from the selected font pack and preserves older exports without this descriptor. +It bounds files, outlines, raster dimensions, and compound glyphs. Java owns glyph texture upload and retirement. +Regressions cover original native pixels, translated and scaled components, malformed data, cycles, close/reload, and pack ownership. + +Original Bedrock 1.26.60.5 instructions supply 36 raster controls across two fonts and heights 32, 64, and 96. +Five authored fixture controls also match, including curves and composite coordinates that FreeType normally shifts. +At 1280 × 720 and native UI scale 2, the shipping add-on matches all 72 tested label crops without an experimental mod. +Java GUI scales 1, 2, 3, and return to 1 retain all 63 bitmap and nine TrueType matches. +A completed resource reload retains all 72 matches. The full build and 180-second replay pass. + +TrueType GUI vertices retain half precision in cache coordinates before the 1.5 display transform. +The owned native bitmaps keep exact atlas bounds. A baked-glyph regression checks vertex submission before the pose and preserves world text. +At 1280 × 720 and native UI scale 2, all 32 font-selection labels match native Bedrock 1.26.60.5. +The controls include `AgW@éj`, formatted text, the primary MinecraftTenEmoticon alias, and explicit Unicode. +Java GUI scales 1, 2, 3, return to 1, and a completed resource reload retain these matches. +GUI italics use fixed cache-space edge shear before half-float conversion and display scaling. +Original Bedrock 1.26.60.5 bitmap and TrueType glyph writers and shared quad instructions establish that geometry. +Eighteen bitmap instruction cases retain the same shear across three cell heights and offsets. +Baked-glyph regressions cover tall cells, the TrueType normalization boundary, conservative bounds, and unchanged Java world vertices. +A fresh 24-label native scene covers plain and italic text at three font scales. +All 24 selected labels match across Java GUI scales 1, 2, 3, and return to 1. +Paired native and Java GPU captures establish the native TrueType UV packing and zero atlas gutters. +The renderer preserves that sampling phase after Java relocates glyphs and clears their reserved trailing gutters. +Regressions cover relocated UVs, unchanged world sampling, zero padding, and uploads at the atlas edge. +A normal resource reload preserves all 24 crops. The full build and complete 180-second replay pass. +Bold, shadow, other font families, mixed or scaled aliases, and complete screens remain open. + Other background layouts, action appearance, and complete column placement remain open. Unanchored manual-grid cells use their grid origins. @@ -102,7 +133,7 @@ Tests cover different durations, repeated requests, successor chains, missing co Native Bedrock 1.26.60.5 Escape frames retain the fade after the size track finishes. This mechanism does not establish exact native frame timing. These controls do not establish identical native pooling or frame timing. -Broader viewports, complete inventory pixels, entrance timing, and TrueType comparisons remain open. +Broader viewports, complete inventory pixels, entrance timing, and other TrueType families remain open. The shared hurt receiver binds queued events to the connection and local actor lifetime. It consumes pending events against the current scene and retains the counter across ordinary screen changes. diff --git a/targets.json b/targets.json index 78cb6c81..f6b8026e 100644 --- a/targets.json +++ b/targets.json @@ -5,7 +5,7 @@ "fork": "StackAnvil/CubeConverter", "prAssignees": ["oryxel1"], "baseBranch": "vv-json", - "baseSha": "7cf736c4a5472824bd7ec0a6da5222c3257136b4", + "baseSha": "8a7b73a8ed5e07dcb302fd2f42b8268e21d3704c", "java": "17", "publication": "Maven", "buildTask": "build" @@ -16,7 +16,7 @@ "fork": "StackAnvil/ViaBedrock", "prAssignees": ["RaphiMC", "Exterminate5573"], "baseBranch": "main", - "baseSha": "da6391a7b8714673331cee1d54b8dc9aae0460e5", + "baseSha": "c6f58812fbdff5322bc1f4a7be37ba2513a794fb", "java": "25", "publication": "MavenJava", "buildTask": "build"