From cd0e602746229253cc3b0dec2fcd8b1525588d87 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:56:05 +0100 Subject: [PATCH 1/7] docs(compatibility): record current Lifeboat comparison limits Retain the reproduced spawn obstruction as an open rendering defect. An unchanged official-client scene reproduces it in Java, while the native lobby view remains unobstructed. Clarify that the exact captured Navigator payload resolves in authored Java UI and projects clean dialog labels and response indices. This selected control does not close the original unmatched dialog report or any full compatibility area. --- docs/bedrock-coverage.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index d01ccd84..c2254a7c 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -39,7 +39,8 @@ No complete area has verified Bedrock parity. Retain these reports until a matching reproduction establishes the result: -- Lifeboat dialog text was reported as untranslated. A generic [action-form dialog label defect](../patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md) is fixed. The original report still needs a matching form capture. +- Lifeboat dialog text was reported as untranslated. For the captured Navigator form, [authored presentation](../patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md) produces the correct title, labels, and response indices. Native and Java routes received identical form bytes. The reported dialog still needs a matching reproduction. +- Lifeboat's Java spawn view can be obscured by nearby geometry. A complete replay of an official 1.26.60.5 capture reproduces the obstruction with unchanged scene packets. The native lobby view is unobstructed. The cause remains unverified. - WildFFA inventory opened with E shows items without the expected background. - An update from 0.3.61 to 0.4.2 caused reported inventory lag spikes and overlapping menus. - The oversized shield and distorted custom-skin pose in the confirmed 0.4.3 screenshots remain unresolved. From e8e5c6e1ec6ac6489993d610b384db0ce328c285 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 3 Nov 2026 03:39:10 +0100 Subject: [PATCH 2/7] fix(render): fade nearby Bedrock player surfaces Reproduce the ordered camera-depth fade observed in official Bedrock 1.26.60.5. Separate world-player materials keep shared hands and GUI portraits intact, with matching coverage through actor and Iris shaders. Fold the implementation and its feature consumers into their owning patches. GPU controls verify fixed coverage rows and discarded color/depth. The full dependency build and an unchanged 150-second Lifeboat replay pass; the lobby is visible through nearby players. Broader rendering and skin parity remain open. --- docs/bedrock-coverage.md | 2 +- ...w-confirmed-blacklisted-server-joins.patch | 6 +- .../0002-native-entities-and-skins.patch | 539 ++++++++++++++++-- .../0002-native-entities-and-skins.pr.md | 14 +- ...creator-geometry-and-face-animations.patch | 27 +- ...re-native-actor-and-resource-runtime.patch | 4 +- ...acter-creator-accounts-and-wardrobes.patch | 4 +- ...-actors-with-shared-animation-graphs.patch | 4 +- .../0016-render-server-player-costumes.patch | 24 +- ...7-render-equipped-native-pack-models.patch | 14 +- ...0029-share-native-ui-scene-rendering.patch | 73 +-- 11 files changed, 573 insertions(+), 138 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index c2254a7c..dccd0805 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -40,7 +40,7 @@ No complete area has verified Bedrock parity. Retain these reports until a matching reproduction establishes the result: - Lifeboat dialog text was reported as untranslated. For the captured Navigator form, [authored presentation](../patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md) produces the correct title, labels, and response indices. Native and Java routes received identical form bytes. The reported dialog still needs a matching reproduction. -- Lifeboat's Java spawn view can be obscured by nearby geometry. A complete replay of an official 1.26.60.5 capture reproduces the obstruction with unchanged scene packets. The native lobby view is unobstructed. The cause remains unverified. +- Lifeboat's opaque player-head obstruction matches a missing native near-camera fade. World player materials now use the [native ordered fade](../patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.pr.md). GPU checks cover color and depth through native and Iris programs. A matching offline scene reveals the lobby through nearby players. Broader spawn and rendering parity remain open. - WildFFA inventory opened with E shows items without the expected background. - An update from 0.3.61 to 0.4.2 caused reported inventory lag spikes and overlapping menus. - The oversized shield and distorted custom-skin pose in the confirmed 0.4.3 screenshots remain unresolved. diff --git a/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch b/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch index 184c02c9..e30640a6 100644 --- a/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch +++ b/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch @@ -1,4 +1,4 @@ -From 24a18417c6941d1921b5eb6c6a67bc7d9d182c89 Mon Sep 17 00:00:00 2001 +From 671d083f0b1f9fbe359a81b06cc5ff13475cb665 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:06:07 +0100 Subject: [PATCH] feat(connection): allow confirmed blacklisted server joins @@ -247,10 +247,10 @@ index f345d7c..40e7ae9 100644 "command_block.viafabricplus.first_tick": "Execute on first tick", "base.viafabricplus.accept": "Accept", diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index ac083ae..8777544 100644 +index 65afd38..c073c0e 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -48,6 +48,8 @@ +@@ -49,6 +49,8 @@ "core.access.MixinEventLoopGroupHolder", "core.access.MixinServerAddress", "core.connection.MixinConnectScreen_1", diff --git a/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.patch b/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.patch index af24dec5..687b5ead 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.patch @@ -1,4 +1,4 @@ -From c1ce6257302505e0c708407f4337806b2c432cbe Mon Sep 17 00:00:00 2001 +From bf59659f819670153883df9b55c52b3e0f8434f0 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(bedrock): render custom entities and player skins @@ -761,6 +761,30 @@ Compare transformed pack and fallback programs against 672 original 26.60 fragment controls each, in addition to the native shader GPU controls. Shader-pack lighting remains pack-defined; these controls establish material composition rather than complete shader-pack visual parity. + +Reproduce the player near-camera fade from official Bedrock 1.26.60.5 +(protocol 2225). A complete Lifeboat capture places several players at +the local camera. The official view remains visible through their +surfaces. The matching Java scene preserves all clientbound scene bytes +but fills the view with an opaque player head. + +Native normal alpha-test draws remain two-sided. They use signed +camera-view depth, smoothstep from zero to one block, and an ordered +8x8 dither with a 1/128 threshold offset. Discarded fragments write +neither color nor depth. Other nearby actors can disable this fade, +so do not apply it to every world actor. + +Select the fade through a separate cached world-player material. +Feature consumers opt in for world surfaces. Shared hands, other actors, +and GUI portraits retain their own materials. Native and Iris programs +use the same coverage function, including split emission passes. +Keep the existing material alpha cutoff and other material predicates. + +GPU tests check fixed native coverage rows at zero, quarter, half, +three-quarter, and full depth. They also check negative depth, +transparent texels, untouched discarded depth, and ordinary draws. +Transformed Iris and fallback programs retain the same coverage and +restore full coverage when the shared program draws another material. --- build.gradle.kts | 6 + settings.gradle.kts | 3 + @@ -788,8 +812,8 @@ composition rather than complete shader-pack visual parity. .../iris/MixinIrisNativeMaterialBinding.java | 44 ++ .../MixinIrisNativeSamplerReservation.java | 30 ++ .../mixin/compat/iris/MixinIrisPipelines.java | 23 ++ - .../compat/iris/MixinNativeShaderBinding.java | 34 ++ - .../iris/MixinNativeShaderCompilation.java | 29 ++ + .../compat/iris/MixinNativeShaderBinding.java | 38 ++ + .../iris/MixinNativeShaderCompilation.java | 30 ++ .../render/MixinAbstractClientPlayer.java | 38 ++ .../render/MixinBedrockActorDynamicData.java | 64 +++ .../render/MixinBedrockActorGlintFrame.java | 33 ++ @@ -815,7 +839,7 @@ composition rather than complete shader-pack visual parity. .../render/BedrockActorGlintFrames.java | 37 ++ .../BedrockActorMaterialDefinitions.java | 211 ++++++++++ .../bedrock/render/BedrockActorMaterials.java | 56 +++ - .../render/BedrockActorRenderTypes.java | 180 ++++++++ + .../render/BedrockActorRenderTypes.java | 215 ++++++++++ .../bedrock/render/BedrockActorResources.java | 43 ++ .../bedrock/render/BedrockActorTint.java | 34 ++ .../render/BedrockActorUvTransform.java | 18 + @@ -826,12 +850,12 @@ composition rather than complete shader-pack visual parity. .../bedrock/render/BedrockNameTagAnchors.java | 75 ++++ .../bedrock/render/BedrockNameTags.java | 76 ++++ .../render/BedrockPlayerLayerModels.java | 57 +++ - .../bedrock/render/BedrockPlayerRenderer.java | 53 +++ + .../bedrock/render/BedrockPlayerRenderer.java | 54 +++ .../bedrock/render/BedrockPlayerSkins.java | 289 +++++++++++++ .../render/BedrockRenderSnapshotCache.java | 48 +++ .../bedrock/render/BedrockRidingAnchors.java | 73 ++++ - .../bedrock/render/BedrockShaderAlpha.java | 43 ++ - .../bedrock/render/BedrockShaderEmission.java | 206 +++++++++ + .../bedrock/render/BedrockShaderAlpha.java | 52 +++ + .../bedrock/render/BedrockShaderEmission.java | 208 ++++++++++ .../render/BedrockShaderPipelines.java | 55 +++ .../bedrock/render/BedrockShaderTint.java | 26 ++ .../bedrock/render/BedrockSkinImages.java | 113 +++++ @@ -839,16 +863,17 @@ composition rather than complete shader-pack visual parity. .../render/NativeCustomEntityPayload.java | 24 ++ .../NativeCustomEntitySamplerPayload.java | 24 ++ .../bedrock/render/NativeSkinPayload.java | 22 + - .../render/compat/iris/BedrockIrisAlpha.java | 69 ++++ - .../compat/iris/BedrockIrisAlphaUniforms.java | 48 +++ + .../render/compat/iris/BedrockIrisAlpha.java | 106 +++++ + .../compat/iris/BedrockIrisAlphaUniforms.java | 51 +++ .../compat/iris/BedrockIrisEmission.java | 16 + .../render/compat/iris/BedrockIrisGlint.java | 143 +++++++ .../compat/iris/BedrockIrisGlintPrograms.java | 16 + .../compat/iris/BedrockIrisMaskedTexture.java | 153 +++++++ .../compat/iris/BedrockIrisSamplers.java | 18 + - .../shaders/core/bedrock_actor.fsh | 92 +++++ - .../shaders/core/bedrock_actor.vsh | 69 ++++ + .../shaders/core/bedrock_actor.fsh | 100 +++++ + .../shaders/core/bedrock_actor.vsh | 77 ++++ .../shaders/include/bedrock_glint.glsl | 14 + + .../shaders/include/bedrock_player_fade.glsl | 15 + .../viafabricplus-bedrock.accesswidener | 6 + .../viafabricplus-bedrock.mixins.json | 39 ++ .../render/BedrockAcceptedResourcesTest.java | 118 ++++++ @@ -868,11 +893,12 @@ composition rather than complete shader-pack visual parity. .../bedrock/render/BedrockModelCubeTest.java | 47 +++ .../render/BedrockNameTagAnchorsTest.java | 120 ++++++ .../bedrock/render/BedrockNameTagsTest.java | 127 ++++++ + .../render/BedrockPlayerFadeGpuTest.java | 245 +++++++++++ .../render/BedrockPlayerLayerModelsTest.java | 103 +++++ .../render/BedrockPlayerMaterialsTest.java | 42 ++ .../BedrockRenderSnapshotCacheTest.java | 123 ++++++ .../render/BedrockRidingAnchorsTest.java | 149 +++++++ - .../render/BedrockShaderAlphaTest.java | 38 ++ + .../render/BedrockShaderAlphaTest.java | 43 ++ .../render/BedrockShaderPipelinesTest.java | 119 ++++++ .../bedrock/render/BedrockShaderTintTest.java | 31 ++ .../bedrock/render/BedrockSkinImagesTest.java | 82 ++++ @@ -880,7 +906,7 @@ composition rather than complete shader-pack visual parity. .../bedrock/render/EglTestContext.java | 52 +++ .../compat/iris/BedrockIrisAlphaGpuTest.java | 391 ++++++++++++++++++ .../compat/iris/BedrockIrisGlintGpuTest.java | 253 ++++++++++++ - 118 files changed, 8251 insertions(+) + 120 files changed, 8624 insertions(+) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/BedrockMixinPlugin.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockActorDynamicData.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockActorPreparedRenderType.java @@ -965,6 +991,7 @@ composition rather than complete shader-pack visual parity. create mode 100644 src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.fsh create mode 100644 src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.vsh create mode 100644 src/main/resources/assets/viafabricplus-bedrock/shaders/include/bedrock_glint.glsl + create mode 100644 src/main/resources/assets/viafabricplus-bedrock/shaders/include/bedrock_player_fade.glsl create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockAcceptedResourcesTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorDepthGpuTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorGlintFramesTest.java @@ -982,6 +1009,7 @@ composition rather than complete shader-pack visual parity. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockModelCubeTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockNameTagAnchorsTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockNameTagsTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerFadeGpuTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerLayerModelsTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerMaterialsTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockRenderSnapshotCacheTest.java @@ -1192,7 +1220,7 @@ index 0000000..50fc1c5 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockShaderPipeline.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockShaderPipeline.java new file mode 100644 -index 0000000..c976e95 +index 0000000..caa5b55 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockShaderPipeline.java @@ -0,0 +1,10 @@ @@ -1204,7 +1232,7 @@ index 0000000..c976e95 +package com.viaversion.viafabricplus.bedrock.injection.access; + +public interface IBedrockShaderPipeline { -+ void bedrock$material(float reference, boolean masked); ++ void bedrock$material(float reference, boolean masked, boolean playerFade); +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/emf/MixinEmfModelRoots.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/emf/MixinEmfModelRoots.java new file mode 100644 @@ -1958,10 +1986,10 @@ index 0000000..0e2a633 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/iris/MixinNativeShaderBinding.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/iris/MixinNativeShaderBinding.java new file mode 100644 -index 0000000..bcaf1e5 +index 0000000..0bf43ac --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/iris/MixinNativeShaderBinding.java -@@ -0,0 +1,34 @@ +@@ -0,0 +1,38 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1985,23 +2013,27 @@ index 0000000..bcaf1e5 + @Unique + private boolean bedrock$masked; + ++ @Unique ++ private boolean bedrock$playerFade; ++ + @Override -+ public void bedrock$material(final float reference, final boolean masked) { ++ public void bedrock$material(final float reference, final boolean masked, final boolean playerFade) { + this.bedrock$alphaReference = reference; + this.bedrock$masked = masked; ++ this.bedrock$playerFade = playerFade; + } + + @WrapMethod(method = "bind") + private void bedrock$material(final Operation original) { -+ BedrockShaderAlpha.bind(this.bedrock$alphaReference, this.bedrock$masked, () -> original.call()); ++ BedrockShaderAlpha.bind(this.bedrock$alphaReference, this.bedrock$masked, this.bedrock$playerFade, () -> original.call()); + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/iris/MixinNativeShaderCompilation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/iris/MixinNativeShaderCompilation.java new file mode 100644 -index 0000000..3ea92d6 +index 0000000..f2534d4 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/iris/MixinNativeShaderCompilation.java -@@ -0,0 +1,29 @@ +@@ -0,0 +1,30 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -2026,7 +2058,8 @@ index 0000000..3ea92d6 + // Wrap the complete method so this sees Iris's cached override, including cancelled returns. + final CompiledRenderPipeline compiled = BedrockShaderPipelines.compiling(pipeline, () -> original.call(pipeline)); + if (compiled instanceof FrontendRenderPipeline frontend && frontend.backendRenderPipeline() instanceof IBedrockShaderPipeline material) { -+ material.bedrock$material(BedrockShaderPipelines.alphaReference(pipeline), pipeline.getShaderDefines().flags().contains("MASKED_MULTITEXTURE")); ++ material.bedrock$material(BedrockShaderPipelines.alphaReference(pipeline), pipeline.getShaderDefines().flags().contains("MASKED_MULTITEXTURE"), ++ pipeline.getShaderDefines().flags().contains("PLAYER_NEAR_FADE")); + } + return compiled; + } @@ -3486,10 +3519,10 @@ index 0000000..8e46c00 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorRenderTypes.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorRenderTypes.java new file mode 100644 -index 0000000..db83832 +index 0000000..516fdbf --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorRenderTypes.java -@@ -0,0 +1,180 @@ +@@ -0,0 +1,215 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3503,6 +3536,10 @@ index 0000000..db83832 +import com.mojang.renderpearl.api.pipeline.UniformType; +import com.viaversion.viafabricplus.ViaFabricPlus; +import com.viaversion.viafabricplus.bedrock.injection.access.IBedrockActorRenderType; ++import com.google.common.base.Suppliers; ++import java.util.Map; ++import java.util.WeakHashMap; ++import java.util.function.Supplier; +import java.util.List; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.BindGroupLayouts; @@ -3518,6 +3555,8 @@ index 0000000..db83832 + private static final java.util.Map MATERIAL_PIPELINES = pipelines(); + public static final List PIPELINES = List.copyOf(MATERIAL_PIPELINES.values()); + ++ private static final Map> PLAYER_TYPES = new WeakHashMap<>(); ++ + private BedrockActorRenderTypes() {} + + private static java.util.Map pipelines() { @@ -3529,7 +3568,7 @@ index 0000000..db83832 + for (final boolean nether : new boolean[] {false, true}) + for (final boolean masked : new boolean[] {false, true}) { + if (unlit && nether || masked && (!alphaTest || emissive)) continue; -+ final var key = new PipelineKey(alphaTest, cull, unlit, emissive, nether, masked, false, GlintLighting.NONE, BedrockActorDepth.DEFAULT, true); ++ final var key = new PipelineKey(alphaTest, cull, unlit, emissive, nether, masked, false, GlintLighting.NONE, BedrockActorDepth.DEFAULT, true, false); + result.put(key, pipeline(key)); + } + return result; @@ -3537,7 +3576,7 @@ index 0000000..db83832 + + private static RenderPipeline pipeline(final PipelineKey key) { + final var builder = RenderPipeline.builder(RenderPipelines.ENTITY_SNIPPET) -+ .withLocation(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "pipeline/actor_" + key.alphaTest + "_" + key.cull + "_" + key.unlit + "_" + key.emissive + "_" + key.nether + (key.masked ? "_masked" : "") + (key.colorMask ? "_tint" : "") + (key.alphaWrite ? "" : "_no_alpha") + (key.glint == GlintLighting.NONE ? "" : key.glint == GlintLighting.LIGHTMAP ? "_glint_lightmap" : "_glint") + (key.depth.equals(BedrockActorDepth.DEFAULT) ? "" : "_depth_" + key.depth.suffix()))) ++ .withLocation(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "pipeline/actor_" + key.alphaTest + "_" + key.cull + "_" + key.unlit + "_" + key.emissive + "_" + key.nether + (key.playerFade ? "_player_fade" : "") + (key.masked ? "_masked" : "") + (key.colorMask ? "_tint" : "") + (key.alphaWrite ? "" : "_no_alpha") + (key.glint == GlintLighting.NONE ? "" : key.glint == GlintLighting.LIGHTMAP ? "_glint_lightmap" : "_glint") + (key.depth.equals(BedrockActorDepth.DEFAULT) ? "" : "_depth_" + key.depth.suffix()))) + .withVertexShader(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "core/bedrock_actor")) + .withFragmentShader(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "core/bedrock_actor")) + .withBindGroupLayout(BindGroupLayouts.SAMPLER1) @@ -3554,6 +3593,7 @@ index 0000000..db83832 + .withUniform("BedrockGlintInfo", UniformType.UNIFORM_BUFFER) + .withUniform("BedrockGlintTexture", UniformType.COMBINED_IMAGE_SAMPLER).build()); + if (key.glint == GlintLighting.LIGHTMAP) builder.withShaderDefine("GLINT_LIGHTMAP"); ++ if (key.playerFade) builder.withShaderDefine("PLAYER_NEAR_FADE"); + if (key.alphaTest) builder.withShaderDefine("ALPHA_TEST"); + if (key.unlit) builder.withShaderDefine("IGNORE_LIGHTING"); + if (key.nether) builder.withShaderDefine("NETHER_LIGHTING"); @@ -3608,9 +3648,16 @@ index 0000000..db83832 + private static RenderType create(final List textures, final BedrockActorMaterialDefinitions.Material material, + final boolean ignoreLighting, final float multiplier, final BedrockActorUvTransform uv, + final boolean netherLighting, final BedrockActorGlint glint, final GlintLighting lighting, final BedrockActorTint tint) { ++ return create(textures, material, ignoreLighting, multiplier, uv, netherLighting, glint, lighting, tint, false); ++ } ++ ++ private static RenderType create(final List textures, final BedrockActorMaterialDefinitions.Material material, ++ final boolean ignoreLighting, final float multiplier, final BedrockActorUvTransform uv, ++ final boolean netherLighting, final BedrockActorGlint glint, final GlintLighting lighting, ++ final BedrockActorTint tint, final boolean playerFade) { + if (material.colorMask() && tint == null) throw new IllegalArgumentException("Native tint input is unavailable"); -+ final var pipeline = glint == null ? selectPipeline(material, ignoreLighting, netherLighting) -+ : selectGlintPipeline(material, ignoreLighting, netherLighting, lighting); ++ final var pipeline = glint == null ? selectPipeline(material, ignoreLighting, netherLighting, playerFade) ++ : selectGlintPipeline(material, ignoreLighting, netherLighting, lighting, playerFade); + final var setup = textures(RenderSetup.builder(pipeline), textures, material); + if (glint != null) { + if (textures.size() < 2) throw new IllegalArgumentException("Missing native glint sampler binding"); @@ -3621,23 +3668,44 @@ index 0000000..db83832 + ((IBedrockActorRenderType) type).bedrock$uvTransform(uv); + ((IBedrockActorRenderType) type).bedrock$glint(glint); + ((IBedrockActorRenderType) type).bedrock$tint(material.colorMask() ? tint : null); -+ if (material.emissive()) BedrockShaderEmission.register(type, textures.getFirst(), material, ignoreLighting, multiplier, uv, netherLighting); ++ if (material.emissive()) BedrockShaderEmission.register(type, textures.getFirst(), material, ignoreLighting, multiplier, uv, netherLighting, playerFade); ++ if (!playerFade) { ++ final var bindings = List.copyOf(textures); ++ PLAYER_TYPES.put(type, Suppliers.memoize(() -> create(bindings, material, ignoreLighting, ++ multiplier, uv, netherLighting, glint, lighting, tint, true))); ++ } + return type; + } + ++ /** World player surfaces fade without changing a cached hand or unrelated actor material. */ ++ public static RenderType forPlayer(final RenderType type) { ++ final var player = PLAYER_TYPES.get(type); ++ return player == null ? type : player.get(); ++ } ++ + static RenderPipeline selectPipeline(final BedrockActorMaterialDefinitions.Material material, + final boolean ignoreLighting, final boolean netherLighting) { ++ return selectPipeline(material, ignoreLighting, netherLighting, false); ++ } ++ ++ static RenderPipeline selectPipeline(final BedrockActorMaterialDefinitions.Material material, ++ final boolean ignoreLighting, final boolean netherLighting, final boolean playerFade) { + if (material.glint()) throw new IllegalArgumentException("Native glint requires caller-owned frame inputs"); + return MATERIAL_PIPELINES.computeIfAbsent(new PipelineKey(material.alphaTest(), material.cull(), ignoreLighting, -+ material.emissive(), netherLighting && !ignoreLighting, material.masked(), material.colorMask(), GlintLighting.NONE, material.depth(), material.alphaWrite()), BedrockActorRenderTypes::pipeline); ++ material.emissive(), netherLighting && !ignoreLighting, material.masked(), material.colorMask(), GlintLighting.NONE, material.depth(), material.alphaWrite(), playerFade), BedrockActorRenderTypes::pipeline); + } + + static RenderPipeline selectGlintPipeline(final BedrockActorMaterialDefinitions.Material material, + final boolean ignoreLighting, final boolean netherLighting, final GlintLighting lighting) { ++ return selectGlintPipeline(material, ignoreLighting, netherLighting, lighting, false); ++ } ++ ++ private static RenderPipeline selectGlintPipeline(final BedrockActorMaterialDefinitions.Material material, ++ final boolean ignoreLighting, final boolean netherLighting, final GlintLighting lighting, final boolean playerFade) { + if (lighting == GlintLighting.NONE) throw new IllegalArgumentException("Missing native glint lighting source"); + if (material.emissive() || material.masked() || material.colorMask()) throw new IllegalArgumentException("Unsupported native glint material combination"); + return MATERIAL_PIPELINES.computeIfAbsent(new PipelineKey(material.alphaTest(), material.cull(), ignoreLighting, -+ false, netherLighting && !ignoreLighting, false, false, lighting, material.depth(), material.alphaWrite()), BedrockActorRenderTypes::pipeline); ++ false, netherLighting && !ignoreLighting, false, false, lighting, material.depth(), material.alphaWrite(), playerFade), BedrockActorRenderTypes::pipeline); + } + + private static com.mojang.renderpearl.api.textures.GpuSampler repeatedSampler(final Identifier texture) { @@ -3668,7 +3736,7 @@ index 0000000..db83832 + + enum GlintLighting { NONE, EXPLICIT, LIGHTMAP } + -+ private record PipelineKey(boolean alphaTest, boolean cull, boolean unlit, boolean emissive, boolean nether, boolean masked, boolean colorMask, GlintLighting glint, BedrockActorDepth depth, boolean alphaWrite) {} ++ private record PipelineKey(boolean alphaTest, boolean cull, boolean unlit, boolean emissive, boolean nether, boolean masked, boolean colorMask, GlintLighting glint, BedrockActorDepth depth, boolean alphaWrite, boolean playerFade) {} +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorResources.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorResources.java new file mode 100644 @@ -4519,10 +4587,10 @@ index 0000000..d7b1e7a +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java new file mode 100644 -index 0000000..146f9a2 +index 0000000..4797e13 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java -@@ -0,0 +1,53 @@ +@@ -0,0 +1,54 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -4545,6 +4613,7 @@ index 0000000..146f9a2 + +import java.util.HashMap; +import java.util.Map; ++import java.util.List; +import net.minecraft.client.model.player.PlayerModel; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; @@ -4565,8 +4634,8 @@ index 0000000..146f9a2 + protected RenderType getRenderType(final AvatarRenderState state, final boolean visible, final boolean translucent, final boolean glowing) { + if (usesNativeMaterial(state, visible, translucent)) { + final boolean netherLighting = BedrockActorRenderTypes.usesNetherLighting(); -+ return this.nativeMaterials.computeIfAbsent(netherLighting, _ -> BedrockActorRenderTypes.create( -+ this.getTextureLocation(state), BedrockActorMaterialDefinitions.Material.CUTOUT, false, 1F, BedrockActorUvTransform.IDENTITY, netherLighting)); ++ return BedrockActorRenderTypes.forPlayer(this.nativeMaterials.computeIfAbsent(netherLighting, _ -> BedrockActorRenderTypes.create( ++ List.of(this.getTextureLocation(state)), BedrockActorMaterialDefinitions.Material.CUTOUT, false, 1F, BedrockActorUvTransform.IDENTITY, netherLighting))); + } + return super.getRenderType(state, visible, translucent, glowing); + } @@ -5006,10 +5075,10 @@ index 0000000..79525c0 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockShaderAlpha.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockShaderAlpha.java new file mode 100644 -index 0000000..d2a721c +index 0000000..3b9b045 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockShaderAlpha.java -@@ -0,0 +1,43 @@ +@@ -0,0 +1,52 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5034,31 +5103,40 @@ index 0000000..d2a721c + } + + public static boolean masked() { return REFERENCE.get().masked; } ++ public static boolean playerFade() { return REFERENCE.get().playerFade; } + + public static void bind(final float reference, final boolean masked, final Runnable action) { ++ bind(reference, masked, false, action); ++ } ++ ++ public static void bind(final float reference, final boolean masked, final boolean playerFade, final Runnable action) { + final State current = REFERENCE.get(); + final float previous = current.reference; + final boolean previousMask = current.masked; ++ final boolean previousFade = current.playerFade; + current.reference = reference; + current.masked = masked; ++ current.playerFade = playerFade; + try { + action.run(); + } finally { + current.reference = previous; + current.masked = previousMask; ++ current.playerFade = previousFade; + } + } + private static final class State { + private float reference = Float.NaN; + private boolean masked; ++ private boolean playerFade; + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockShaderEmission.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockShaderEmission.java new file mode 100644 -index 0000000..4e3d77f +index 0000000..7551b96 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockShaderEmission.java -@@ -0,0 +1,206 @@ +@@ -0,0 +1,208 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5120,13 +5198,14 @@ index 0000000..4e3d77f + + private static RenderPipeline createEmissionPipeline(final RenderPipeline body) { + final var depth = body.getDepthStencilState(); -+ return BedrockShaderPipelines.register(RenderPipeline.builder(RenderPipelines.ENTITY_SNIPPET, RenderPipelines.EYES_SNIPPET) ++ final var builder = RenderPipeline.builder(RenderPipelines.ENTITY_SNIPPET, RenderPipelines.EYES_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", body.getLocation().getPath() + "_emission")) + .withBindGroupLayout(net.minecraft.client.renderer.BindGroupLayouts.SAMPLER1) + .withCull(body.isCull()).withColorTargetState(new ColorTargetState(BlendFunction.ADDITIVE)) + .withDepthStencilState(depth == null ? java.util.Optional.empty() -+ : java.util.Optional.of(new com.mojang.renderpearl.api.pipeline.DepthStencilState(depth.depthTest(), false))) -+ .build(), RenderPipelines.EYES); ++ : java.util.Optional.of(new com.mojang.renderpearl.api.pipeline.DepthStencilState(depth.depthTest(), false))); ++ if (body.getShaderDefines().flags().contains("PLAYER_NEAR_FADE")) builder.withShaderDefine("PLAYER_NEAR_FADE"); ++ return BedrockShaderPipelines.register(builder.build(), RenderPipelines.EYES); + } + + /** The additive draw must use the same pack vertex stages as its depth-writing body. */ @@ -5138,8 +5217,8 @@ index 0000000..4e3d77f + + static void register(final RenderType type, final Identifier texture, final BedrockActorMaterialDefinitions.Material material, + final boolean unlit, final float multiplier, final BedrockActorUvTransform uv, -+ final boolean nether) { -+ MATERIALS.put(type, new EmissionKey(texture, material, unlit, multiplier, uv, nether)); ++ final boolean nether, final boolean playerFade) { ++ MATERIALS.put(type, new EmissionKey(texture, material, unlit, multiplier, uv, nether, playerFade)); + } + + public static List passes(final RenderType type) { @@ -5148,7 +5227,8 @@ index 0000000..4e3d77f + try { + final Textures textures = textures(material.texture()); + return textures.passes.computeIfAbsent(material, key -> { -+ final RenderType lit = BedrockActorRenderTypes.create(List.of(textures.litId), key.material().withoutEmission(), key.unlit(), key.multiplier(), key.uv(), key.nether()); ++ final RenderType body = BedrockActorRenderTypes.create(List.of(textures.litId), key.material().withoutEmission(), key.unlit(), key.multiplier(), key.uv(), key.nether()); ++ final RenderType lit = key.playerFade() ? BedrockActorRenderTypes.forPlayer(body) : body; + final RenderType emission = RenderType.create("bedrock_emission", RenderSetup.builder(emissionPipeline(lit.pipeline())) + .withTexture("Sampler0", textures.emissionId).useLightmap().useOverlay().createRenderSetup()); + // Radiance bypasses the native lighting multiplier but shares its texture coordinates. @@ -5233,7 +5313,7 @@ index 0000000..4e3d77f + + public record Pass(RenderType type, boolean emission) {} + private record EmissionKey(Identifier texture, BedrockActorMaterialDefinitions.Material material, boolean unlit, float multiplier, -+ BedrockActorUvTransform uv, boolean nether) {} ++ BedrockActorUvTransform uv, boolean nether, boolean playerFade) {} + + private static final class Textures { + private AbstractTexture source; @@ -5644,10 +5724,10 @@ index 0000000..5b31c39 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/compat/iris/BedrockIrisAlpha.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/compat/iris/BedrockIrisAlpha.java new file mode 100644 -index 0000000..21560be +index 0000000..e19a59f --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/compat/iris/BedrockIrisAlpha.java -@@ -0,0 +1,69 @@ +@@ -0,0 +1,106 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5656,10 +5736,14 @@ index 0000000..21560be +package com.viaversion.viafabricplus.bedrock.render.compat.iris; + +import io.github.douira.glsl_transformer.ast.node.TranslationUnit; ++import io.github.douira.glsl_transformer.ast.node.abstract_node.ASTNode; +import io.github.douira.glsl_transformer.ast.transform.ASTInjectionPoint; +import io.github.douira.glsl_transformer.ast.transform.ASTParser; +import io.github.douira.glsl_transformer.ast.transform.JobParameters; +import io.github.douira.glsl_transformer.ast.transform.SingleASTTransformer; ++import java.io.IOException; ++import java.nio.charset.StandardCharsets; ++import java.util.ArrayList; +import net.irisshaders.iris.pipeline.transform.PatchShaderType; +import net.irisshaders.iris.pipeline.transform.parameter.Parameters; +import net.irisshaders.iris.pipeline.transform.parameter.VanillaParameters; @@ -5669,6 +5753,9 @@ index 0000000..21560be + public static final String ENABLED = "iris_bedrock_AlphaEnabled"; + public static final String REFERENCE = "iris_bedrock_AlphaReference"; + public static final String SAMPLER = "iris_bedrock_SourceTexture"; ++ public static final String PLAYER_FADE = "iris_bedrock_PlayerFade"; ++ private static final String DEPTH = "iris_bedrock_PlayerDepth"; ++ private static final String FADE_LIBRARY = fadeLibrary(); + private static final String UV = "iris_bedrock_SourceUv"; + + private BedrockIrisAlpha() {} @@ -5686,7 +5773,7 @@ index 0000000..21560be + public static void transform(final ASTParser parser, final TranslationUnit tree, final Parameters parameters) { + if (!supports(parameters)) return; + if (parameters.type == PatchShaderType.VERTEX) { -+ vertex(parser, tree, "iris_UV0"); ++ vertex(parser, tree, "iris_UV0", "iris_Position", "iris_transforms.ModelViewMat"); + } else if (parameters.type == PatchShaderType.FRAGMENT) { + fragment(parser, tree); + } @@ -5695,14 +5782,28 @@ index 0000000..21560be + public static String fallback(final String source, final boolean vertex) { + final var transformer = new SingleASTTransformer(); + transformer.setTransformation(tree -> { -+ if (vertex) vertex(transformer, tree, "UV0"); ++ if (vertex) vertex(transformer, tree, "UV0", "Position", "ModelViewMat"); + else fragment(transformer, tree); + }); + return transformer.transform(source); + } + -+ private static void vertex(final ASTParser parser, final TranslationUnit tree, final String attribute) { -+ tree.parseAndInjectNode(parser, ASTInjectionPoint.BEFORE_DECLARATIONS, "out vec2 " + UV + ";"); ++ private static void vertex(final ASTParser parser, final TranslationUnit tree, final String attribute, ++ final String position, final String matrix) { ++ if (!tree.getRoot().identifierIndex.has(position)) ++ tree.parseAndInjectNode(parser, ASTInjectionPoint.BEFORE_DECLARATIONS, "in vec3 " + position + ";"); ++ if (matrix.startsWith("iris_transforms.") && !tree.getRoot().identifierIndex.has("iris_transforms")) { ++ tree.parseAndInjectNode(parser, ASTInjectionPoint.BEFORE_DECLARATIONS, """ ++ layout(std140) uniform iris_DynamicTransforms { ++ mat4 ModelViewMat; mat4 TextureMat; vec4 ColorModulator; vec3 ModelOffset; ++ } iris_transforms; ++ """); ++ } else if (matrix.equals("ModelViewMat") && !tree.getRoot().identifierIndex.has(matrix)) { ++ tree.parseAndInjectNode(parser, ASTInjectionPoint.BEFORE_DECLARATIONS, "uniform mat4 ModelViewMat;"); ++ } ++ tree.parseAndInjectNodes(parser, ASTInjectionPoint.BEFORE_DECLARATIONS, ++ "out vec2 " + UV + ";", "out float " + DEPTH + ";"); ++ tree.prependMainFunctionBody(parser, DEPTH + " = -(" + matrix + " * vec4(" + position + ", 1.0)).z;"); + tree.prependMainFunctionBody(parser, UV + " = " + attribute + ";"); + } + @@ -5710,19 +5811,35 @@ index 0000000..21560be + BedrockIrisMaskedTexture.transform(parser, tree); + tree.parseAndInjectNodes(parser, ASTInjectionPoint.BEFORE_DECLARATIONS, + "in vec2 " + UV + ";", ++ "in float " + DEPTH + ";", ++ "uniform int " + PLAYER_FADE + ";", + "uniform int " + ENABLED + ";", + "uniform float " + REFERENCE + ";", + "uniform sampler2D " + SAMPLER + ";"); ++ final var library = parser.parseTranslationUnit(tree.getRoot(), "#version 330 core\n" + FADE_LIBRARY); ++ final var declarations = new ArrayList<>(library.getChildren()); ++ declarations.forEach(ASTNode::detach); ++ tree.injectNodes(ASTInjectionPoint.BEFORE_DECLARATIONS, declarations); ++ tree.prependMainFunctionBody(parser, "if (" + PLAYER_FADE ++ + " != 0 && !bedrock_player_visible(" + DEPTH + ", gl_FragCoord.xy)) discard;"); + tree.prependMainFunctionBody(parser, "if (" + ENABLED + " != 0 && !(iris_bedrock_Compose(texture(" + + SAMPLER + ", " + UV + "), texture(" + BedrockIrisMaskedTexture.SAMPLER + ", " + UV + ")).a > " + REFERENCE + ")) { discard; }"); + } ++ ++ private static String fadeLibrary() { ++ try (final var input = BedrockIrisAlpha.class.getResourceAsStream( ++ "/assets/viafabricplus-bedrock/shaders/include/bedrock_player_fade.glsl")) { ++ if (input == null) throw new IllegalStateException("Missing native player fade shader"); ++ return new String(input.readAllBytes(), StandardCharsets.UTF_8); ++ } catch (final IOException failure) { throw new IllegalStateException(failure); } ++ } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/compat/iris/BedrockIrisAlphaUniforms.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/compat/iris/BedrockIrisAlphaUniforms.java new file mode 100644 -index 0000000..4a0ccce +index 0000000..3a71efd --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/compat/iris/BedrockIrisAlphaUniforms.java -@@ -0,0 +1,48 @@ +@@ -0,0 +1,51 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5737,6 +5854,7 @@ index 0000000..4a0ccce +/** Locations belong to one program; values belong to each pipeline bind. */ +public final class BedrockIrisAlphaUniforms { + private final int enabled; ++ private final int playerFade; + private final int reference; + private final int sampler; + private final int opaque; @@ -5747,6 +5865,7 @@ index 0000000..4a0ccce + + public BedrockIrisAlphaUniforms(final int program) { + this.enabled = GL46C.glGetUniformLocation(program, BedrockIrisAlpha.ENABLED); ++ this.playerFade = GL46C.glGetUniformLocation(program, BedrockIrisAlpha.PLAYER_FADE); + this.reference = GL46C.glGetUniformLocation(program, BedrockIrisAlpha.REFERENCE); + this.sampler = GL46C.glGetUniformLocation(program, BedrockIrisAlpha.SAMPLER); + this.opaque = GL46C.glGetUniformLocation(program, BedrockIrisMaskedTexture.OPAQUE); @@ -5760,6 +5879,7 @@ index 0000000..4a0ccce + public void bind() { + final float nativeReference = BedrockShaderAlpha.reference(Float.NaN); + GL46C.glUniform1i(this.enabled, Float.isNaN(nativeReference) ? 0 : 1); ++ GL46C.glUniform1i(this.playerFade, BedrockShaderAlpha.playerFade() ? 1 : 0); + GL46C.glUniform1f(this.reference, Float.isNaN(nativeReference) ? 0F : nativeReference); + GL46C.glUniform1i(this.opaque, nativeReference == BedrockShaderAlpha.OPAQUE_REFERENCE ? 1 : 0); + // A pack may replace gtexture. This separate sampler always reads the actor's unit zero. @@ -6149,10 +6269,10 @@ index 0000000..c6a7061 +} diff --git a/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.fsh b/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.fsh new file mode 100644 -index 0000000..88f77b0 +index 0000000..a744fb8 --- /dev/null +++ b/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.fsh -@@ -0,0 +1,92 @@ +@@ -0,0 +1,100 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : require + @@ -6173,6 +6293,11 @@ index 0000000..88f77b0 + vec4 BedrockTint; +}; +#endif ++#ifdef PLAYER_NEAR_FADE ++#include ++layout(location = 9) in float bedrockPlayerViewDepth; ++#endif ++ +uniform sampler2D Sampler0; +#ifdef MASKED_MULTITEXTURE +uniform sampler2D BedrockMaskTexture; @@ -6188,6 +6313,9 @@ index 0000000..88f77b0 +layout(location = 0) out vec4 fragColor; + +void main() { ++#ifdef PLAYER_NEAR_FADE ++ if (!bedrock_player_visible(bedrockPlayerViewDepth, gl_FragCoord.xy)) discard; ++#endif + vec4 color = texture(Sampler0, texCoord0); +#ifdef COLOR_MASK +#ifdef ALPHA_TEST @@ -6247,10 +6375,10 @@ index 0000000..88f77b0 +} diff --git a/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.vsh b/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.vsh new file mode 100644 -index 0000000..35b584b +index 0000000..9f8ae53 --- /dev/null +++ b/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.vsh -@@ -0,0 +1,69 @@ +@@ -0,0 +1,77 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : require + @@ -6270,6 +6398,10 @@ index 0000000..35b584b +#endif +#endif + ++#ifdef PLAYER_NEAR_FADE ++layout(location = 9) out float bedrockPlayerViewDepth; ++#endif ++ +uniform sampler2D Sampler1; +layout(location = 0) in vec3 Position; +layout(location = 1) in vec4 Color; @@ -6288,7 +6420,11 @@ index 0000000..35b584b +layout(location = 6) out vec2 texCoord0; + +void main() { -+ gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); ++ vec4 view = ModelViewMat * vec4(Position, 1.0); ++ gl_Position = ProjMat * view; ++#ifdef PLAYER_NEAR_FADE ++ bedrockPlayerViewDepth = -view.z; ++#endif + sphericalVertexDistance = fog_spherical_distance(Position); + cylindricalVertexDistance = fog_cylindrical_distance(Position); + overlayColor = texelFetch(Sampler1, UV1, 0); @@ -6340,6 +6476,27 @@ index 0000000..32cda1c + return (vec2(c * centered.x + s * centered.y + offset, + c * centered.y - s * centered.x) + 0.5) * BedrockGlintScale.xy; +} +diff --git a/src/main/resources/assets/viafabricplus-bedrock/shaders/include/bedrock_player_fade.glsl b/src/main/resources/assets/viafabricplus-bedrock/shaders/include/bedrock_player_fade.glsl +new file mode 100644 +index 0000000..915b6a5 +--- /dev/null ++++ b/src/main/resources/assets/viafabricplus-bedrock/shaders/include/bedrock_player_fade.glsl +@@ -0,0 +1,15 @@ ++float bedrock_player_bayer(vec2 cell) { ++ vec2 whole = floor(cell); ++ return fract(whole.x * 0.5 + whole.y * whole.y * 0.75); ++} ++ ++bool bedrock_player_visible(float viewDepth, vec2 pixel) { ++ float distance = clamp(viewDepth, 0.0, 1.0); ++ float coverage = distance * distance * (3.0 - distance * 2.0); ++ vec2 cell = floor(pixel); ++ float coarse = bedrock_player_bayer(cell * 0.25); ++ float medium = bedrock_player_bayer(cell * 0.5); ++ float fine = bedrock_player_bayer(cell); ++ float threshold = (medium + coarse * 0.25) * 0.25 + 0.0078125 + fine; ++ return coverage > threshold; ++} diff --git a/src/main/resources/viafabricplus-bedrock.accesswidener b/src/main/resources/viafabricplus-bedrock.accesswidener index 35145bc..f42de8f 100644 --- a/src/main/resources/viafabricplus-bedrock.accesswidener @@ -8455,6 +8612,257 @@ index 0000000..7ca90b8 + return styles; + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerFadeGpuTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerFadeGpuTest.java +new file mode 100644 +index 0000000..94e05d5 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerFadeGpuTest.java +@@ -0,0 +1,245 @@ ++package com.viaversion.viafabricplus.bedrock.render; ++ ++import com.mojang.renderpearl.api.pipeline.RenderPipeline; ++import com.viaversion.viafabricplus.bedrock.render.compat.iris.BedrockIrisAlpha; ++import com.viaversion.viafabricplus.bedrock.render.compat.iris.BedrockIrisAlphaUniforms; ++import io.github.douira.glsl_transformer.ast.transform.JobParameters; ++import io.github.douira.glsl_transformer.ast.transform.SingleASTTransformer; ++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; ++import java.util.Set; ++import net.irisshaders.iris.gl.blending.AlphaTest; ++import net.irisshaders.iris.gl.state.ShaderAttributeInputs; ++import net.irisshaders.iris.pipeline.transform.Patch; ++import net.irisshaders.iris.pipeline.transform.PatchShaderType; ++import net.irisshaders.iris.pipeline.transform.parameter.VanillaParameters; ++import java.io.IOException; ++import java.nio.charset.StandardCharsets; ++import java.nio.FloatBuffer; ++import java.util.regex.Pattern; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; ++import org.lwjgl.system.MemoryStack; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.lwjgl.opengl.GL33C.*; ++ ++/** Native 1.26.60 normal alpha-test near-fade controls, with fixed eight-pixel coverage rows. */ ++@EnabledIfEnvironmentVariable(named = "STACKANVIL_IRIS_ALPHA_GPU_TESTS", matches = "1") ++final class BedrockPlayerFadeGpuTest { ++ private static final int[] EMPTY = new int[8]; ++ private static final int[] FULL = {255, 255, 255, 255, 255, 255, 255, 255}; ++ private static final int[] QUARTER_DEPTH = {0x15, 0, 0x44, 0, 0x51, 0, 0x44, 0}; ++ private static final int[] HALF_DEPTH = {0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA}; ++ private static final int[] THREE_QUARTER_DEPTH = {255, 0xBB, 255, 0xAE, 255, 0xBB, 255, 0xEA}; ++ ++ @Test ++ void fadesBySignedViewDepthAndLeavesDiscardedPixelsAndDepthUntouched() { ++ EglTestContext.run(() -> { ++ final int program = program(true); ++ try (final var fixture = new Fixture(program)) { ++ fixture.draw(-.25F, 1, EMPTY); ++ fixture.draw(0, 1, EMPTY); ++ fixture.draw(.25F, 1, QUARTER_DEPTH); ++ fixture.draw(.5F, 1, HALF_DEPTH); ++ fixture.draw(.75F, 1, THREE_QUARTER_DEPTH); ++ fixture.draw(1, 1, FULL); ++ fixture.draw(2, 1, FULL); ++ fixture.draw(2, .25F, EMPTY); ++ } finally { glDeleteProgram(program); } ++ }); ++ } ++ ++ @Test ++ void theSharedHandAndOtherActorMaterialKeepsItsOriginalCoverage() { ++ EglTestContext.run(() -> { ++ final int program = program(false); ++ try (final var fixture = new Fixture(program)) { ++ fixture.draw(0, 1, FULL); ++ fixture.draw(.25F, 1, FULL); ++ fixture.draw(2, .25F, EMPTY); ++ } finally { glDeleteProgram(program); } ++ }); ++ } ++ ++ @Test ++ void sharedIrisProgramsFadeOnlyPlayerDrawsAndPreserveTheSameDepthCoverage() { ++ EglTestContext.run(() -> { ++ for (final boolean fallback : new boolean[] {false, true}) { ++ final int program = irisProgram(fallback); ++ try (final var fixture = new Fixture(program, fallback)) { ++ fixture.draw(0, 1, EMPTY); ++ fixture.draw(.25F, 1, QUARTER_DEPTH); ++ fixture.draw(.5F, 1, HALF_DEPTH); ++ fixture.draw(.75F, 1, THREE_QUARTER_DEPTH); ++ fixture.draw(1, 1, FULL); ++ fixture.draw(.5F, 1, FULL, false); ++ fixture.draw(.5F, 1, HALF_DEPTH); ++ fixture.draw(2, .25F, EMPTY); ++ } finally { glDeleteProgram(program); } ++ } ++ }); ++ } ++ ++ private static final class Fixture implements AutoCloseable { ++ private final Boolean irisFallback; ++ private final int program, vao = glGenVertexArrays(), vertices = glGenBuffers(); ++ private final int framebuffer = glGenFramebuffers(), color = glGenTextures(), depth = glGenTextures(); ++ private final int texture = glGenTextures(), overlay = glGenTextures(); ++ private final int fog = glGenBuffers(), dynamic = glGenBuffers(), projection = glGenBuffers(); ++ ++ private Fixture(final int program) { this(program, null); } ++ ++ private Fixture(final int program, final Boolean irisFallback) { ++ this.program = program; ++ this.irisFallback = irisFallback; ++ glBindVertexArray(this.vao); ++ glBindBuffer(GL_ARRAY_BUFFER, this.vertices); ++ try (final var stack = MemoryStack.stackPush()) { ++ glBufferData(GL_ARRAY_BUFFER, stack.floats(-1, -1, 0, 3, -1, 0, -1, 3, 0), GL_STATIC_DRAW); ++ glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, false, 12, 0); ++ glVertexAttrib4f(1, 1, 1, 1, 1); glVertexAttrib2f(2, .5F, .5F); ++ glVertexAttribI4i(3, 0, 0, 0, 1); glVertexAttribI4i(4, 0, 0, 0, 1); glVertexAttrib3f(5, 0, 1, 0); ++ glBindTexture(GL_TEXTURE_2D, this.color); ++ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 8, 8, 0, GL_RGBA, GL_FLOAT, 0L); ++ glBindTexture(GL_TEXTURE_2D, this.depth); ++ glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, 8, 8, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0L); ++ glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer); ++ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this.color, 0); ++ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, this.depth, 0); ++ assertEquals(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_FRAMEBUFFER)); ++ glViewport(0, 0, 8, 8); glDisable(GL_BLEND); glDisable(GL_CULL_FACE); ++ glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDepthMask(true); ++ glUseProgram(program); ++ final var fogInput = stack.callocFloat(12); ++ fogInput.put(4, 1000).put(5, 2000).put(6, 1000).put(7, 2000); ++ buffer(program, "Fog", this.fog, 0, fogInput); ++ final var matrix = stack.callocFloat(16); ++ matrix.put(0, 1).put(5, 1).put(15, 1); ++ buffer(program, "Projection", this.projection, 2, matrix); ++ sample(program, "Sampler1", this.overlay, 1, stack.floats(0, 0, 0, 1)); ++ } ++ } ++ ++ private void draw(final float distance, final float alpha, final int[] rows) { ++ this.draw(distance, alpha, rows, true); ++ } ++ ++ private void draw(final float distance, final float alpha, final int[] rows, final boolean playerFade) { ++ try (final var stack = MemoryStack.stackPush()) { ++ final var transforms = stack.callocFloat(40); ++ for (final int index : new int[]{0, 5, 10, 15, 16, 21, 26, 31, 32, 33, 34, 35}) transforms.put(index, 1); ++ transforms.put(14, -distance); ++ if (Boolean.TRUE.equals(this.irisFallback)) { ++ glUniformMatrix4fv(glGetUniformLocation(this.program, "ModelViewMat"), false, transforms.limit(16)); ++ } else { ++ buffer(this.program, this.irisFallback == null ? "DynamicTransforms" : "iris_DynamicTransforms", this.dynamic, 1, transforms); ++ } ++ if (this.irisFallback != null) { ++ final var uniforms = new BedrockIrisAlphaUniforms(this.program); ++ BedrockShaderAlpha.bind(.5F, false, playerFade, uniforms::bind); ++ } ++ sample(this.program, "Sampler0", this.texture, 0, stack.floats(0, 1, 0, alpha)); ++ glClearColor(1, 0, 0, 1); glClearDepth(1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); ++ glDrawArrays(GL_TRIANGLES, 0, 3); ++ final var pixels = stack.mallocFloat(8 * 8 * 4); ++ final var depths = stack.mallocFloat(8 * 8); ++ glReadPixels(0, 0, 8, 8, GL_RGBA, GL_FLOAT, pixels); ++ glReadPixels(0, 0, 8, 8, GL_DEPTH_COMPONENT, GL_FLOAT, depths); ++ for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) { ++ final boolean kept = (rows[y] & 1 << x) != 0; ++ final String context = "iris=" + this.irisFallback + ", depth=" + distance + ", alpha=" + alpha + ", pixel=" + x + "," + y; ++ assertEquals(kept ? 0F : 1F, pixels.get((y * 8 + x) * 4), .00001F, context); ++ assertEquals(kept ? 1F : 0F, pixels.get((y * 8 + x) * 4 + 1), .00001F, context); ++ assertEquals(kept ? .5F : 1F, depths.get(y * 8 + x), .00001F, context); ++ } ++ } ++ } ++ ++ @Override public void close() { ++ glUseProgram(0); glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindVertexArray(0); ++ glDeleteFramebuffers(this.framebuffer); glDeleteVertexArrays(this.vao); glDeleteBuffers(this.vertices); ++ glDeleteBuffers(this.fog); glDeleteBuffers(this.dynamic); glDeleteBuffers(this.projection); ++ glDeleteTextures(this.color); glDeleteTextures(this.depth); glDeleteTextures(this.texture); glDeleteTextures(this.overlay); ++ } ++ } ++ ++ private static void sample(final int program, final String name, final int texture, final int unit, final FloatBuffer pixels) { ++ glActiveTexture(GL_TEXTURE0 + unit); glBindTexture(GL_TEXTURE_2D, texture); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); ++ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 1, 1, 0, GL_RGBA, GL_FLOAT, pixels); ++ glUniform1i(glGetUniformLocation(program, name), unit); ++ } ++ ++ private static void buffer(final int program, final String name, final int buffer, final int binding, final FloatBuffer data) { ++ final int block = glGetUniformBlockIndex(program, name); ++ if (block == GL_INVALID_INDEX) return; ++ glBindBuffer(GL_UNIFORM_BUFFER, buffer); glBufferData(GL_UNIFORM_BUFFER, data, GL_STATIC_DRAW); ++ glBindBufferBase(GL_UNIFORM_BUFFER, binding, buffer); glUniformBlockBinding(program, block, binding); ++ } ++ ++ private static int program(final boolean fade) { ++ final RenderPipeline pipeline = BedrockActorRenderTypes.selectPipeline(BedrockActorMaterialDefinitions.Material.CUTOUT, true, false, fade); ++ final String defines = pipeline.getShaderDefines().asSourceDirectives(); ++ final int vertex = shader(GL_VERTEX_SHADER, expand(resource("core/bedrock_actor.vsh")).replace("#version 330", "#version 330\n" + defines)); ++ final int fragment = shader(GL_FRAGMENT_SHADER, expand(resource("core/bedrock_actor.fsh")).replace("#version 330", "#version 330\n" + defines)); ++ final int program = glCreateProgram(); ++ try { ++ glAttachShader(program, vertex); glAttachShader(program, fragment); glLinkProgram(program); ++ assertEquals(GL_TRUE, glGetProgrami(program, GL_LINK_STATUS), glGetProgramInfoLog(program)); ++ return program; ++ } finally { glDeleteShader(vertex); glDeleteShader(fragment); } ++ } ++ ++ private static int irisProgram(final boolean fallback) { ++ final String position = fallback ? "Position" : "iris_Position"; ++ final String uv = fallback ? "UV0" : "iris_UV0"; ++ final String vertexSource = """ ++ #version 330 core ++ layout(location=0) in vec3 %s; ++ layout(location=2) in vec2 %s; ++ void main() { gl_Position=vec4(%s.xy,0,1); } ++ """.formatted(position, uv, position); ++ final String fragmentSource = """ ++ #version 330 core ++ uniform sampler2D gtexture; ++ out vec4 fragColor; ++ void main() { fragColor=texture(gtexture,vec2(.5)); } ++ """; ++ final String transformedVertex = irisTransform(vertexSource, PatchShaderType.VERTEX, fallback); ++ final int vertex = shader(GL_VERTEX_SHADER, transformedVertex); ++ final int fragment = shader(GL_FRAGMENT_SHADER, irisTransform(fragmentSource, PatchShaderType.FRAGMENT, fallback)); ++ final int program = glCreateProgram(); ++ try { ++ glAttachShader(program, vertex); glAttachShader(program, fragment); glLinkProgram(program); ++ assertEquals(GL_TRUE, glGetProgrami(program, GL_LINK_STATUS), glGetProgramInfoLog(program)); ++ return program; ++ } finally { glDeleteShader(vertex); glDeleteShader(fragment); } ++ } ++ ++ private static String irisTransform(final String source, final PatchShaderType type, final boolean fallback) { ++ if (fallback) return BedrockIrisAlpha.fallback(source, type == PatchShaderType.VERTEX); ++ final var parameters = new VanillaParameters(Patch.VANILLA, new Object2ObjectOpenHashMap<>(), Set.of(), ++ AlphaTest.ALWAYS, false, false, false, new ShaderAttributeInputs(true, true, false, false, false), false, false); ++ parameters.type = type; ++ final var transformer = new SingleASTTransformer(); ++ transformer.setTransformation(tree -> BedrockIrisAlpha.transform(transformer, tree, parameters)); ++ return transformer.transform(source); ++ } ++ ++ private static int shader(final int type, final String source) { ++ final int shader = glCreateShader(type); glShaderSource(shader, source); glCompileShader(shader); ++ assertEquals(GL_TRUE, glGetShaderi(shader, GL_COMPILE_STATUS), glGetShaderInfoLog(shader)); return shader; ++ } ++ ++ private static String expand(final String source) { ++ final var matcher = Pattern.compile("#include <([^:>]+):([^>]+)>").matcher(source); ++ return matcher.replaceAll(match -> java.util.regex.Matcher.quoteReplacement(expand(read("/assets/" + match.group(1) + "/shaders/include/" + match.group(2))))); ++ } ++ ++ private static String resource(final String path) { return read("/assets/viafabricplus-bedrock/shaders/" + path); } ++ private static String read(final String path) { ++ try (final var input = BedrockPlayerFadeGpuTest.class.getResourceAsStream(path)) { ++ assertNotNull(input, path); return new String(input.readAllBytes(), StandardCharsets.UTF_8); ++ } catch (final IOException error) { throw new IllegalStateException(error); } ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerLayerModelsTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerLayerModelsTest.java new file mode 100644 index 0000000..d1c2b75 @@ -8898,10 +9306,10 @@ index 0000000..e3eadc3 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockShaderAlphaTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockShaderAlphaTest.java new file mode 100644 -index 0000000..6360b94 +index 0000000..5be0afd --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockShaderAlphaTest.java -@@ -0,0 +1,38 @@ +@@ -0,0 +1,43 @@ +package com.viaversion.viafabricplus.bedrock.render; + +import java.util.concurrent.CompletableFuture; @@ -8913,18 +9321,22 @@ index 0000000..6360b94 + @Test + void scopesTheReferenceToEachBindingAndRestoresNestedJavaAndNativeDraws() { + assertEquals(0.1F, BedrockShaderAlpha.reference(0.1F)); -+ BedrockShaderAlpha.bind(0.5F, true, () -> { ++ BedrockShaderAlpha.bind(0.5F, true, true, () -> { + assertTrue(BedrockShaderAlpha.masked()); ++ assertTrue(BedrockShaderAlpha.playerFade()); + assertEquals(0.5F, BedrockShaderAlpha.reference(0.1F)); + BedrockShaderAlpha.bind(Float.NaN, () -> { ++ assertFalse(BedrockShaderAlpha.playerFade()); + assertFalse(BedrockShaderAlpha.masked()); + assertEquals(0.1F, BedrockShaderAlpha.reference(0.1F)); + }); + assertTrue(BedrockShaderAlpha.masked()); ++ assertTrue(BedrockShaderAlpha.playerFade()); + assertEquals(0.5F, BedrockShaderAlpha.reference(0.1F)); + BedrockShaderAlpha.bind(0F, () -> assertEquals(0F, BedrockShaderAlpha.reference(0.1F))); + assertEquals(0.5F, BedrockShaderAlpha.reference(0.1F)); + }); ++ assertFalse(BedrockShaderAlpha.playerFade()); + assertFalse(BedrockShaderAlpha.masked()); + assertEquals(0.1F, BedrockShaderAlpha.reference(0.1F)); + } @@ -8936,6 +9348,7 @@ index 0000000..6360b94 + assertEquals(0.1F, CompletableFuture.supplyAsync(() -> BedrockShaderAlpha.reference(0.1F)).join()); + throw failure; + }))); ++ assertFalse(BedrockShaderAlpha.playerFade()); + assertFalse(BedrockShaderAlpha.masked()); + assertEquals(0.1F, BedrockShaderAlpha.reference(0.1F)); + } 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 7b9d317f..c3e2493a 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 @@ -8,6 +8,9 @@ Import native Bedrock entity and skin rendering from viafabricplus-bedrock PR #9 Keep per-player appearance and animation state separate from server costumes and equipment. Preserve native geometry, UVs, materials, animated surfaces, and connection-owned resources. +World player surfaces use separate cached materials for native near-camera fading. Shared hands, unrelated actors, and GUI portraits retain their own materials. +The native and Iris programs use the same ordered coverage function. Surviving pixels retain the material depth policy. + Prerequisites: - viabedrock/0064-evaluate-named-actor-properties.patch: Publish the versioned custom-actor sampler snapshot and immutable evaluated texture bindings. @@ -16,7 +19,16 @@ Prerequisites: ## Validation and remaining limits -Regression coverage includes `MixinIrisJavaAlphaTest`, `BedrockAcceptedResourcesTest`, `BedrockActorDepthGpuTest`, `BedrockActorGlintFramesTest`. +Regression coverage includes `MixinIrisJavaAlphaTest`, `BedrockAcceptedResourcesTest`, `BedrockActorDepthGpuTest`, and `BedrockActorGlintFramesTest`. + +Official Bedrock 1.26.60.5 (protocol 2225) GPU draws establish the player fade across zero to one block of signed view depth. +The native near-player draws remain two-sided. A matching Java replay initially obscures the lobby with an opaque player head. +`BedrockPlayerFadeGpuTest` checks fixed 8×8 coverage patterns and color/depth preservation through actor, Iris, and fallback shaders. +The shared Iris program also restores full coverage for ordinary draws. These controls preserve the existing material alpha cutoff. + +A complete 150-second add-on replay preserves the official scene hash and installs the captured skins unchanged. +The lobby remains visible through nearby players, and the first-person compass remains visible. +This selected scene does not establish complete visual parity or a fresh public-server result. Native controls establish selected appearances, classic armor masks, and one owned outerwear/chest case. One update after disabling the trusted-skins filter replaces the body and face atlases at five camera blocks. Those bindings remain at 120 camera blocks. Both players stay five blocks apart, so this does not establish distance-driven LOD. diff --git a/patches/viafabricplus-bedrock/upstreamable/0006-render-character-creator-geometry-and-face-animations.patch b/patches/viafabricplus-bedrock/upstreamable/0006-render-character-creator-geometry-and-face-animations.patch index 5df0110e..fd37c5a8 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0006-render-character-creator-geometry-and-face-animations.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0006-render-character-creator-geometry-and-face-animations.patch @@ -1,4 +1,4 @@ -From a2431d3c3a168cd20e85a43cacf2ba8b3cfdd5e6 Mon Sep 17 00:00:00 2001 +From f93f231d83e7592f8e6a4d7c4a393a91dc326a49 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(bedrock): render Character Creator geometry and face @@ -65,7 +65,7 @@ one-element binding list and unchanged ordinary material flags. .../features/render/MixinGuiSkinRenderer.java | 32 ++ .../bedrock/render/BedrockGeometry.java | 105 ++++++- .../bedrock/render/BedrockPersonaLayer.java | 71 +++++ - .../bedrock/render/BedrockPlayerRenderer.java | 52 +++- + .../bedrock/render/BedrockPlayerRenderer.java | 53 +++- .../bedrock/render/BedrockPlayerSkins.java | 273 ++++++++++++++++-- .../bedrock/render/BedrockSkinAnimation.java | 220 ++++++++++++++ .../light/BedrockRenderingMetadata.java | 75 +++++ @@ -75,7 +75,7 @@ one-element binding list and unchanged ordinary material flags. .../render/BedrockPersonaHandsTest.java | 75 +++++ .../render/BedrockSkinAnimationTest.java | 157 ++++++++++ .../light/BedrockRenderingMetadataTest.java | 107 +++++++ - 15 files changed, 1456 insertions(+), 28 deletions(-) + 15 files changed, 1456 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinGuiSkinRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPersonaLayer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockSkinAnimation.java @@ -302,7 +302,7 @@ index 4667641..8da79f3 100644 final float pivotY = cube.getPivot().getY(); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPersonaLayer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPersonaLayer.java new file mode 100644 -index 0000000..7346924 +index 0000000..7bf73b9 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPersonaLayer.java @@ -0,0 +1,71 @@ @@ -345,7 +345,7 @@ index 0000000..7346924 + this.surfaceModel.setupAnim(state); + final RenderType renderType; + if (BedrockPlayerRenderer.usesNativeMaterial(state, !state.isInvisible, false)) { -+ renderType = this.nativeMaterial(); ++ renderType = BedrockActorRenderTypes.forPlayer(this.nativeMaterial()); + } else { + renderType = RenderTypes.entityTranslucent(this.surfaceTexture); + } @@ -378,18 +378,19 @@ index 0000000..7346924 + +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java -index 146f9a2..a1e0f55 100644 +index 4797e13..6363d97 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java -@@ -18,30 +18,41 @@ +@@ -18,31 +18,41 @@ package com.viaversion.viafabricplus.bedrock.render; +import com.mojang.blaze3d.vertex.PoseStack; +import java.util.ArrayList; import java.util.HashMap; -+import java.util.List; - import java.util.Map; +-import java.util.Map; + import java.util.List; ++import java.util.Map; import net.minecraft.client.model.player.PlayerModel; import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.client.renderer.SubmitNodeCollector; @@ -422,13 +423,13 @@ index 146f9a2..a1e0f55 100644 protected RenderType getRenderType(final AvatarRenderState state, final boolean visible, final boolean translucent, final boolean glowing) { if (usesNativeMaterial(state, visible, translucent)) { - final boolean netherLighting = BedrockActorRenderTypes.usesNetherLighting(); -- return this.nativeMaterials.computeIfAbsent(netherLighting, _ -> BedrockActorRenderTypes.create( -- this.getTextureLocation(state), BedrockActorMaterialDefinitions.Material.CUTOUT, false, 1F, BedrockActorUvTransform.IDENTITY, netherLighting)); -+ return this.nativeMaterial(this.getTextureLocation(state)); +- return BedrockActorRenderTypes.forPlayer(this.nativeMaterials.computeIfAbsent(netherLighting, _ -> BedrockActorRenderTypes.create( +- List.of(this.getTextureLocation(state)), BedrockActorMaterialDefinitions.Material.CUTOUT, false, 1F, BedrockActorUvTransform.IDENTITY, netherLighting))); ++ return BedrockActorRenderTypes.forPlayer(this.nativeMaterial(this.getTextureLocation(state))); } return super.getRenderType(state, visible, translucent, glowing); } -@@ -50,4 +61,35 @@ final class BedrockPlayerRenderer extends AvatarRenderer { +@@ -51,4 +61,35 @@ final class BedrockPlayerRenderer extends AvatarRenderer { return visible && !translucent && !state.isSpectator; } 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 23238aa2..df539a23 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 42ee24005f515c2e7608ca92bb4ba8198a047d77 Mon Sep 17 00:00:00 2001 +From 262c26dbca8c630ced6c6709586b3bd4ef84c780 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 @@ -14105,7 +14105,7 @@ index 0000000..4bedd7f + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java -index a1e0f55..a4e6f37 100644 +index 6363d97..7068808 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java @@ -32,6 +32,7 @@ import net.minecraft.client.renderer.entity.state.AvatarRenderState; diff --git a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch index e9f5fe77..17a83822 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch @@ -1,4 +1,4 @@ -From 45bfe83f0b10b347602a3a5c4084ca8163402f3f Mon Sep 17 00:00:00 2001 +From 15e37ab2524497f42fc7b053b4885d9fa4d6edcf 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(appearance): manage Character Creator accounts and @@ -6875,7 +6875,7 @@ index 0000000..3dcee8a + float previousPitch(); +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java -index a4e6f37..385eb7b 100644 +index 7068808..82d1d27 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java @@ -87,10 +87,13 @@ final class BedrockPlayerRenderer extends AvatarRenderer { diff --git a/patches/viafabricplus-bedrock/upstreamable/0014-evaluate-custom-actors-with-shared-animation-graphs.patch b/patches/viafabricplus-bedrock/upstreamable/0014-evaluate-custom-actors-with-shared-animation-graphs.patch index d0277a96..3c33e58b 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0014-evaluate-custom-actors-with-shared-animation-graphs.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0014-evaluate-custom-actors-with-shared-animation-graphs.patch @@ -1,4 +1,4 @@ -From 128b805279485f660b521103dce94babdb3951d4 Mon Sep 17 00:00:00 2001 +From 1bdb8ea0cba32d170b8058e453f9d3475cb242f6 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(entities): evaluate custom actors with shared animation @@ -698,7 +698,7 @@ index 73a8827..0e8d0d3 100644 if (!model.has("bones")) return Map.of(); for (final var entry : model.getAsJsonArray("bones")) { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java -index db50887..b6713a5 100644 +index 6d6692e..999258c 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java @@ -339,6 +339,7 @@ public final class BedrockPlayerSkins { diff --git a/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.patch b/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.patch index c3f95ebe..3aa15dd4 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.patch @@ -1,4 +1,4 @@ -From 523906b82cb74b20b5e07d8d4c240b6a82ca6076 Mon Sep 17 00:00:00 2001 +From 8050f107a4b921ed7e6ab1669621a68a55f3ab0a 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): render server player controllers and costumes @@ -489,7 +489,7 @@ index d302d89..9a394b9 100644 static boolean packName(final String name) { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPersonaLayer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPersonaLayer.java -index 7346924..5a33ceb 100644 +index 7bf73b9..0a9ebae 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPersonaLayer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPersonaLayer.java @@ -34,6 +34,7 @@ final class BedrockPersonaLayer extends RenderLayer { @@ -618,10 +618,10 @@ index 385eb7b..d3a9eb6 100644 protected RenderType getRenderType(final AvatarRenderState state, final boolean visible, final boolean translucent, final boolean glowing) { + if (!BedrockPlayerSkins.serverBodyVisible(state)) return null; if (usesNativeMaterial(state, visible, translucent)) { - return this.nativeMaterial(this.getTextureLocation(state)); + return BedrockActorRenderTypes.forPlayer(this.nativeMaterial(this.getTextureLocation(state))); } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java -index b4b7070..9f62bbb 100644 +index 999258c..90c2541 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java @@ -34,9 +34,10 @@ import com.viaversion.viafabricplus.bedrock.appearance.BedrockPlayerAnimations; @@ -1060,7 +1060,7 @@ index b4b7070..9f62bbb 100644 } } finally { poses.popPose(); -@@ -593,6 +933,7 @@ public final class BedrockPlayerSkins { +@@ -604,6 +944,7 @@ public final class BedrockPlayerSkins { queries.put("blocking", BedrockActorState.isBlocking(entity.getUUID(), entity.isBlocking())); queries.put("shield_blocking_bob", BedrockActorState.shieldBlockingBob(entity.getUUID(), partialTick)); queries.put("target_x_rotation", state.xRot); @@ -1068,7 +1068,7 @@ index b4b7070..9f62bbb 100644 queries.put("target_y_rotation", targetYaw(state.yRot)); queries.put("body_y_rotation", state.bodyRot); queries.put("walk_distance", entity instanceof BedrockWalkingAnimation.Provider walking -@@ -829,6 +1170,8 @@ public final class BedrockPlayerSkins { +@@ -840,6 +1181,8 @@ public final class BedrockPlayerSkins { BedrockItemMetadata.clear(); SKIN_ASSEMBLER.clear(); skinConnection = null; @@ -1077,7 +1077,7 @@ index b4b7070..9f62bbb 100644 } private static void release(final Appearance appearance) { -@@ -841,6 +1184,7 @@ public final class BedrockPlayerSkins { +@@ -852,6 +1195,7 @@ public final class BedrockPlayerSkins { for (final AnimatedSurface surface : appearance.surfaces) { textureManager.release(surface.texture()); } @@ -1085,7 +1085,7 @@ index b4b7070..9f62bbb 100644 } private static Identifier textureId(final UUID playerUuid, final String kind) { -@@ -892,11 +1236,15 @@ public final class BedrockPlayerSkins { +@@ -903,11 +1247,15 @@ public final class BedrockPlayerSkins { 3, geometryName(skin.skinResourcePatch(), "animated_128x128")), skin.geometryData(), skin.skinData(), skin.animations()); } @@ -1103,7 +1103,7 @@ index b4b7070..9f62bbb 100644 if (parsed.isJsonNull()) { return null; } -@@ -1117,6 +1465,31 @@ public final class BedrockPlayerSkins { +@@ -1128,6 +1476,31 @@ public final class BedrockPlayerSkins { private Map previewPose = Map.of(); private List previewSurfaces; private long rendererEpoch; diff --git a/patches/viafabricplus-bedrock/upstreamable/0017-render-equipped-native-pack-models.patch b/patches/viafabricplus-bedrock/upstreamable/0017-render-equipped-native-pack-models.patch index 7f5f310e..ad789ce1 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0017-render-equipped-native-pack-models.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0017-render-equipped-native-pack-models.patch @@ -1,4 +1,4 @@ -From 053906cbf63c22e3db5f415bd714c9f1ae1e934d Mon Sep 17 00:00:00 2001 +From e8cc7bd00ecfdb403e9a5360434363f92308ae32 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(attachables): render equipped native pack models @@ -773,7 +773,7 @@ index 830e1d9..962abaf 100644 Selection select(final BedrockMolang molang, final boolean includeBody) throws IOException { molang.binding("geometry", this.entity.getGeometries()); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java -index 5f01d8a..36e46c4 100644 +index d591a84..f0c921e 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java @@ -5,9 +5,14 @@ @@ -810,7 +810,7 @@ index 5f01d8a..36e46c4 100644 @Inject( method = "extractRenderState(Lnet/minecraft/world/entity/Avatar;Lnet/minecraft/client/renderer/entity/state/AvatarRenderState;F)V", -@@ -31,5 +46,6 @@ public abstract class MixinAvatarAnimationRenderer { +@@ -32,5 +47,6 @@ public abstract class MixinAvatarAnimationRenderer { ((BedrockPlayerAnimationState) state) .bedrockAnimationPose(BedrockPlayerSkins.pose(entity, state, partialTick)); ((BedrockPlayerAnimationState) state).bedrockBodyVisibility(BedrockPlayerSkins.bodyVisibility(entity.getUUID())); @@ -1095,7 +1095,7 @@ index 0000000..ec577f7 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockEquippedAttachableLayer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockEquippedAttachableLayer.java new file mode 100644 -index 0000000..79ea084 +index 0000000..90f5549 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockEquippedAttachableLayer.java @@ -0,0 +1,36 @@ @@ -1130,7 +1130,7 @@ index 0000000..79ea084 + collector.order(1).submitModel(surface.model(), + new BedrockPlayerAttachables.Draw(deformations, BedrockAnimatedModel.boneMatrices(owner), + surface.bindings(), surface.selection(), surface.pose()), poses, -+ surface.renderType(), BedrockActorRenderTypes.lightCoords(light, surface.selection().ignoreLighting()), ++ BedrockActorRenderTypes.forPlayer(surface.renderType()), BedrockActorRenderTypes.lightCoords(light, surface.selection().ignoreLighting()), + OverlayTexture.NO_OVERLAY, state.outlineColor); + } + } @@ -1710,7 +1710,7 @@ index 4bedd7f..acf0f1b 100644 + } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java -index 74b22b4..f1a0f99 100644 +index 90c2541..7b3e97c 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java @@ -474,6 +474,17 @@ public final class BedrockPlayerSkins { @@ -1801,7 +1801,7 @@ index 74b22b4..f1a0f99 100644 static Map geometryQueries(final AvatarRenderState state) { final Geometry geometry = geometry(appearance(state.skin), true); -@@ -1152,10 +1179,12 @@ public final class BedrockPlayerSkins { +@@ -1171,10 +1198,12 @@ public final class BedrockPlayerSkins { final var client = Minecraft.getInstance(); rendererContext = BedrockPlayerLayerModels.context(context, client.getAtlasManager(), client.getPalettedTextureManager()); rendererEpoch++; 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 0b072338..13057be3 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 b0099742c74c4c44bf92e90ff04fe4f0ee88f054 Mon Sep 17 00:00:00 2001 +From 5ed6fe3614f5ee59287aee0701dc47a2e28216a8 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 @@ -794,7 +794,7 @@ image tint and native frame timing parity remain separate requirements. .../render/MixinNativeUiViewport.java | 25 + .../render/MixinPictureInPictureRenderer.java | 41 + .../BedrockActorMaterialDefinitions.java | 4 +- - .../render/BedrockActorRenderTypes.java | 64 +- + .../render/BedrockActorRenderTypes.java | 65 +- .../render/BedrockAnimatedEntityState.java | 16 + .../bedrock/render/BedrockEntityRenderer.java | 6 +- .../bedrock/render/BedrockGeometry.java | 4 +- @@ -893,7 +893,7 @@ image tint and native frame timing parity remain separate requirements. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ - 119 files changed, 13574 insertions(+), 36 deletions(-) + 119 files changed, 13574 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/mixin/features/input/MixinNativeUiInput.java @@ -1844,7 +1844,7 @@ index e851321..74bf1c7 100644 final JsonElement materials = compiled.get("materials"); if (materials == null || !materials.isJsonObject()) return compiled; diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorRenderTypes.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorRenderTypes.java -index db83832..b4c063a 100644 +index 516fdbf..6129085 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorRenderTypes.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorRenderTypes.java @@ -7,6 +7,8 @@ package com.viaversion.viafabricplus.bedrock.render; @@ -1856,29 +1856,29 @@ index db83832..b4c063a 100644 import com.mojang.renderpearl.api.pipeline.RenderPipeline; import com.mojang.renderpearl.api.pipeline.UniformType; import com.viaversion.viafabricplus.ViaFabricPlus; -@@ -25,6 +27,7 @@ import net.raphimc.viabedrock.protocol.storage.ChunkTracker; +@@ -29,6 +31,7 @@ import net.raphimc.viabedrock.protocol.storage.ChunkTracker; public final class BedrockActorRenderTypes { private static final java.util.Map MATERIAL_PIPELINES = pipelines(); public static final List PIPELINES = List.copyOf(MATERIAL_PIPELINES.values()); + private static final java.util.Map> GUI_TYPES = new java.util.WeakHashMap<>(); - private BedrockActorRenderTypes() {} + private static final Map> PLAYER_TYPES = new WeakHashMap<>(); -@@ -34,18 +37,19 @@ public final class BedrockActorRenderTypes { +@@ -40,18 +43,19 @@ public final class BedrockActorRenderTypes { for (final boolean cull : new boolean[] {false, true}) for (final boolean unlit : new boolean[] {false, true}) for (final boolean emissive : new boolean[] {false, true}) - for (final boolean nether : new boolean[] {false, true}) - for (final boolean masked : new boolean[] {false, true}) { - if (unlit && nether || masked && (!alphaTest || emissive)) continue; -- final var key = new PipelineKey(alphaTest, cull, unlit, emissive, nether, masked, false, GlintLighting.NONE, BedrockActorDepth.DEFAULT, true); +- final var key = new PipelineKey(alphaTest, cull, unlit, emissive, nether, masked, false, GlintLighting.NONE, BedrockActorDepth.DEFAULT, true, false); - result.put(key, pipeline(key)); - } + for (final boolean gui : new boolean[] {false, true}) + for (final boolean nether : new boolean[] {false, true}) + for (final boolean masked : new boolean[] {false, true}) { + if ((unlit || gui) && nether || masked && (!alphaTest || emissive)) continue; -+ final var key = new PipelineKey(alphaTest, cull, unlit, emissive, nether, gui, masked, false, GlintLighting.NONE, BedrockActorDepth.DEFAULT, true); ++ final var key = new PipelineKey(alphaTest, cull, unlit, emissive, nether, gui, masked, false, GlintLighting.NONE, BedrockActorDepth.DEFAULT, true, false); + result.put(key, pipeline(key)); + } return result; @@ -1886,12 +1886,12 @@ index db83832..b4c063a 100644 private static RenderPipeline pipeline(final PipelineKey key) { final var builder = RenderPipeline.builder(RenderPipelines.ENTITY_SNIPPET) -- .withLocation(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "pipeline/actor_" + key.alphaTest + "_" + key.cull + "_" + key.unlit + "_" + key.emissive + "_" + key.nether + (key.masked ? "_masked" : "") + (key.colorMask ? "_tint" : "") + (key.alphaWrite ? "" : "_no_alpha") + (key.glint == GlintLighting.NONE ? "" : key.glint == GlintLighting.LIGHTMAP ? "_glint_lightmap" : "_glint") + (key.depth.equals(BedrockActorDepth.DEFAULT) ? "" : "_depth_" + key.depth.suffix()))) -+ .withLocation(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "pipeline/actor_" + key.alphaTest + "_" + key.cull + "_" + key.unlit + "_" + key.emissive + "_" + key.nether + (key.gui ? "_gui" : "") + (key.masked ? "_masked" : "") + (key.colorMask ? "_tint" : "") + (key.alphaWrite ? "" : "_no_alpha") + (key.glint == GlintLighting.NONE ? "" : key.glint == GlintLighting.LIGHTMAP ? "_glint_lightmap" : "_glint") + (key.depth.equals(BedrockActorDepth.DEFAULT) ? "" : "_depth_" + key.depth.suffix()))) +- .withLocation(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "pipeline/actor_" + key.alphaTest + "_" + key.cull + "_" + key.unlit + "_" + key.emissive + "_" + key.nether + (key.playerFade ? "_player_fade" : "") + (key.masked ? "_masked" : "") + (key.colorMask ? "_tint" : "") + (key.alphaWrite ? "" : "_no_alpha") + (key.glint == GlintLighting.NONE ? "" : key.glint == GlintLighting.LIGHTMAP ? "_glint_lightmap" : "_glint") + (key.depth.equals(BedrockActorDepth.DEFAULT) ? "" : "_depth_" + key.depth.suffix()))) ++ .withLocation(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "pipeline/actor_" + key.alphaTest + "_" + key.cull + "_" + key.unlit + "_" + key.emissive + "_" + key.nether + (key.gui ? "_gui" : "") + (key.playerFade ? "_player_fade" : "") + (key.masked ? "_masked" : "") + (key.colorMask ? "_tint" : "") + (key.alphaWrite ? "" : "_no_alpha") + (key.glint == GlintLighting.NONE ? "" : key.glint == GlintLighting.LIGHTMAP ? "_glint_lightmap" : "_glint") + (key.depth.equals(BedrockActorDepth.DEFAULT) ? "" : "_depth_" + key.depth.suffix()))) .withVertexShader(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "core/bedrock_actor")) .withFragmentShader(Identifier.fromNamespaceAndPath("viafabricplus-bedrock", "core/bedrock_actor")) .withBindGroupLayout(BindGroupLayouts.SAMPLER1) -@@ -53,6 +57,9 @@ public final class BedrockActorRenderTypes { +@@ -59,6 +63,9 @@ public final class BedrockActorRenderTypes { ColorTargetState.DEFAULT.format(), key.alphaWrite ? ColorTargetState.WRITE_ALL : ColorTargetState.WRITE_COLOR)) .withCull(key.cull) .withDepthStencilState(key.depth.state()); @@ -1901,7 +1901,7 @@ index db83832..b4c063a 100644 if (key.colorMask) builder.withShaderDefine("COLOR_MASK") .withBindGroupLayout(BindGroupLayout.builder().withUniform("BedrockTintInfo", UniformType.UNIFORM_BUFFER).build()); if (key.masked) builder.withShaderDefine("MASKED_MULTITEXTURE") -@@ -66,7 +73,8 @@ public final class BedrockActorRenderTypes { +@@ -73,7 +80,8 @@ public final class BedrockActorRenderTypes { if (key.unlit) builder.withShaderDefine("IGNORE_LIGHTING"); if (key.nether) builder.withShaderDefine("NETHER_LIGHTING"); if (key.emissive) builder.withShaderDefine("USE_EMISSIVE"); @@ -1911,13 +1911,19 @@ index db83832..b4c063a 100644 : !key.alphaTest ? RenderPipelines.ENTITY_SOLID : key.unlit ? RenderPipelines.ENTITY_CUTOUT_Z_OFFSET : key.cull ? RenderPipelines.ENTITY_CUTOUT_CULL : RenderPipelines.ENTITY_CUTOUT; -@@ -130,14 +138,48 @@ public final class BedrockActorRenderTypes { +@@ -144,8 +152,9 @@ public final class BedrockActorRenderTypes { ((IBedrockActorRenderType) type).bedrock$glint(glint); ((IBedrockActorRenderType) type).bedrock$tint(material.colorMask() ? tint : null); - if (material.emissive()) BedrockShaderEmission.register(type, textures.getFirst(), material, ignoreLighting, multiplier, uv, netherLighting); + if (material.emissive()) BedrockShaderEmission.register(type, textures.getFirst(), material, ignoreLighting, multiplier, uv, netherLighting, playerFade); + final var bindings = List.copyOf(textures); + GUI_TYPES.put(type, com.google.common.base.Suppliers.memoize(() -> createGui(bindings, material, ignoreLighting, multiplier, uv, glint, tint))); - return type; + if (!playerFade) { +- final var bindings = List.copyOf(textures); + PLAYER_TYPES.put(type, Suppliers.memoize(() -> create(bindings, material, ignoreLighting, + multiplier, uv, netherLighting, glint, lighting, tint, true))); + } +@@ -158,6 +167,38 @@ public final class BedrockActorRenderTypes { + return player == null ? type : player.get(); } + /** A queued GUI draw must retain its own pipeline without changing the cached world material. */ @@ -1936,7 +1942,7 @@ index db83832..b4c063a 100644 + if (material.colorMask() && tint == null) throw new IllegalArgumentException("Native tint input is unavailable"); + final var pipeline = glint == null ? selectGuiPipeline(material, ignoreLighting) + : MATERIAL_PIPELINES.computeIfAbsent(new PipelineKey(material.alphaTest(), material.cull(), ignoreLighting, -+ false, false, true, false, false, GlintLighting.EXPLICIT, material.depth(), true), BedrockActorRenderTypes::pipeline); ++ false, false, true, false, false, GlintLighting.EXPLICIT, material.depth(), true, false), BedrockActorRenderTypes::pipeline); + final var setup = textures(RenderSetup.builder(pipeline), textures, material); + if (glint != null) setup.withTexture("BedrockGlintTexture", textures.get(1), () -> repeatedSampler(textures.get(1))); + final var type = RenderType.create("bedrock_gui_actor", setup.useLightmap().useOverlay().createRenderSetup()); @@ -1949,33 +1955,36 @@ index db83832..b4c063a 100644 + + static RenderPipeline selectGuiPipeline(final BedrockActorMaterialDefinitions.Material material, final boolean ignoreLighting) { + if (material.glint()) throw new IllegalArgumentException("Native glint requires caller-owned frame inputs"); -+ return MATERIAL_PIPELINES.computeIfAbsent(new PipelineKey(material.alphaTest(), material.cull(), ignoreLighting, material.emissive(), false, true, material.masked(), material.colorMask(), GlintLighting.NONE, material.depth(), true), BedrockActorRenderTypes::pipeline); ++ return MATERIAL_PIPELINES.computeIfAbsent(new PipelineKey(material.alphaTest(), material.cull(), ignoreLighting, material.emissive(), false, true, material.masked(), material.colorMask(), GlintLighting.NONE, material.depth(), true, false), BedrockActorRenderTypes::pipeline); + } + static RenderPipeline selectPipeline(final BedrockActorMaterialDefinitions.Material material, final boolean ignoreLighting, final boolean netherLighting) { + return selectPipeline(material, ignoreLighting, netherLighting, false); +@@ -167,7 +208,7 @@ public final class BedrockActorRenderTypes { + final boolean ignoreLighting, final boolean netherLighting, final boolean playerFade) { if (material.glint()) throw new IllegalArgumentException("Native glint requires caller-owned frame inputs"); return MATERIAL_PIPELINES.computeIfAbsent(new PipelineKey(material.alphaTest(), material.cull(), ignoreLighting, -- material.emissive(), netherLighting && !ignoreLighting, material.masked(), material.colorMask(), GlintLighting.NONE, material.depth(), material.alphaWrite()), BedrockActorRenderTypes::pipeline); -+ material.emissive(), netherLighting && !ignoreLighting, false, material.masked(), material.colorMask(), GlintLighting.NONE, material.depth(), material.alphaWrite()), BedrockActorRenderTypes::pipeline); +- material.emissive(), netherLighting && !ignoreLighting, material.masked(), material.colorMask(), GlintLighting.NONE, material.depth(), material.alphaWrite(), playerFade), BedrockActorRenderTypes::pipeline); ++ material.emissive(), netherLighting && !ignoreLighting, false, material.masked(), material.colorMask(), GlintLighting.NONE, material.depth(), material.alphaWrite(), playerFade), BedrockActorRenderTypes::pipeline); } static RenderPipeline selectGlintPipeline(final BedrockActorMaterialDefinitions.Material material, -@@ -145,7 +187,7 @@ public final class BedrockActorRenderTypes { +@@ -180,7 +221,7 @@ public final class BedrockActorRenderTypes { if (lighting == GlintLighting.NONE) throw new IllegalArgumentException("Missing native glint lighting source"); if (material.emissive() || material.masked() || material.colorMask()) throw new IllegalArgumentException("Unsupported native glint material combination"); return MATERIAL_PIPELINES.computeIfAbsent(new PipelineKey(material.alphaTest(), material.cull(), ignoreLighting, -- false, netherLighting && !ignoreLighting, false, false, lighting, material.depth(), material.alphaWrite()), BedrockActorRenderTypes::pipeline); -+ false, netherLighting && !ignoreLighting, false, false, false, lighting, material.depth(), material.alphaWrite()), BedrockActorRenderTypes::pipeline); +- false, netherLighting && !ignoreLighting, false, false, lighting, material.depth(), material.alphaWrite(), playerFade), BedrockActorRenderTypes::pipeline); ++ false, netherLighting && !ignoreLighting, false, false, false, lighting, material.depth(), material.alphaWrite(), playerFade), BedrockActorRenderTypes::pipeline); } private static com.mojang.renderpearl.api.textures.GpuSampler repeatedSampler(final Identifier texture) { -@@ -176,5 +218,5 @@ public final class BedrockActorRenderTypes { +@@ -211,5 +252,5 @@ public final class BedrockActorRenderTypes { enum GlintLighting { NONE, EXPLICIT, LIGHTMAP } -- private record PipelineKey(boolean alphaTest, boolean cull, boolean unlit, boolean emissive, boolean nether, boolean masked, boolean colorMask, GlintLighting glint, BedrockActorDepth depth, boolean alphaWrite) {} -+ private record PipelineKey(boolean alphaTest, boolean cull, boolean unlit, boolean emissive, boolean nether, boolean gui, boolean masked, boolean colorMask, GlintLighting glint, BedrockActorDepth depth, boolean alphaWrite) {} +- private record PipelineKey(boolean alphaTest, boolean cull, boolean unlit, boolean emissive, boolean nether, boolean masked, boolean colorMask, GlintLighting glint, BedrockActorDepth depth, boolean alphaWrite, boolean playerFade) {} ++ private record PipelineKey(boolean alphaTest, boolean cull, boolean unlit, boolean emissive, boolean nether, boolean gui, boolean masked, boolean colorMask, GlintLighting glint, BedrockActorDepth depth, boolean alphaWrite, boolean playerFade) {} } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockAnimatedEntityState.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockAnimatedEntityState.java new file mode 100644 @@ -2434,7 +2443,7 @@ index 9c823f1..4accc01 100644 final List textures = new ArrayList<>(); for (int index = 0; index < material.textureCount(); index++) { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java -index c07211c..1af8113 100644 +index 7b3e97c..81950c2 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java @@ -373,7 +373,10 @@ public final class BedrockPlayerSkins { @@ -2524,7 +2533,7 @@ index c07211c..1af8113 100644 variables.put("gliding_speed_value", Math.max(1, state.speedValue)); variables.put( "is_holding_right", -@@ -950,6 +979,7 @@ public final class BedrockPlayerSkins { +@@ -951,6 +980,7 @@ public final class BedrockPlayerSkins { queries.putAll(BedrockActorQueries.nativeState(actorState)); queries.putAll(BedrockActorQueries.equipmentAndVehicle(entity)); queries.put("is_emoting", BedrockActorQueries.isEmoting(actorId, actorState)); @@ -2532,7 +2541,7 @@ index c07211c..1af8113 100644 queries.put("life_time", state.ageInTicks / 20F); queries.put("frame_alpha", partialTick); queries.put("is_alive", entity.isAlive()); -@@ -971,7 +1001,7 @@ public final class BedrockPlayerSkins { +@@ -972,7 +1002,7 @@ public final class BedrockPlayerSkins { queries.put("shield_blocking_bob", BedrockActorState.shieldBlockingBob(entity.getUUID(), partialTick)); queries.put("target_x_rotation", state.xRot); queries.put("head_x_rotation", BedrockPlayerQueries.headPitch(state.xRot)); @@ -9044,10 +9053,10 @@ index 545727e..d0fac3c 100644 "bedrock_settings.viafabricplus.particle_view_distance": "Particle render distance", "bedrock_settings.viafabricplus.captions": "Bedrock closed captions", diff --git a/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.fsh b/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.fsh -index 88f77b0..2628d69 100644 +index a744fb8..df8abcd 100644 --- a/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.fsh +++ b/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_actor.fsh -@@ -74,6 +74,11 @@ void main() { +@@ -82,6 +82,11 @@ void main() { #ifndef IGNORE_LIGHTING color.rgb *= lightMapColor.rgb; #endif @@ -9059,7 +9068,7 @@ index 88f77b0..2628d69 100644 #endif fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, FogEnvironmentalStart, FogEnvironmentalEnd, FogRenderDistanceStart, -@@ -88,5 +93,8 @@ void main() { +@@ -96,5 +101,8 @@ void main() { vec3 glint = (firstGlint + secondGlint) * BedrockGlintColor.rgb * tileLight; fragColor.rgb += glint * glint; fragColor.a = abs((firstGlint.r + secondGlint.r) * BedrockGlintColor.a * BedrockGlintTileLight.a); From d7e48fc78469709bc737c380612b18143f88eeec Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 3 Nov 2026 03:57:18 +0100 Subject: [PATCH 3/7] fix(replay): observe current native form screens Read controller-owned form identity and shared-screen geometry instead of recognizing only the removed form screen. Distinguish authored scenes, add-on fallback forms, and Java dialogs without changing interaction. Keep bounded private metadata and test reflection against the current screen shape, non-form controllers, and fallback paths. The rebuilt Lifeboat replay identifies form 0 and retains all nine visible response indices. --- docs/server-replay.md | 6 +++ .../replay/fabric/NativeFormAudit.java | 54 +++++++++++++++---- test/fixtures/NativeFormAuditSelfTest.java | 24 +++++++++ .../BedrockFallbackFormScreen.java | 6 +++ .../BedrockFormController.java | 6 +++ .../native-form-audit/DialogScreen.java | 3 ++ .../NativeUiSceneScreen.java | 9 ++++ test/replay-native-form-audit.test.ts | 6 ++- 8 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 test/fixtures/native-form-audit/BedrockFallbackFormScreen.java create mode 100644 test/fixtures/native-form-audit/BedrockFormController.java create mode 100644 test/fixtures/native-form-audit/DialogScreen.java create mode 100644 test/fixtures/native-form-audit/NativeUiSceneScreen.java diff --git a/docs/server-replay.md b/docs/server-replay.md index 6a25cc14..48f0a473 100644 --- a/docs/server-replay.md +++ b/docs/server-replay.md @@ -172,6 +172,12 @@ Private `camera-audit.jsonl` observations include frame time, world clocks, play Private `held-item-light-audit.jsonl` observations record actual first-person submissions. They include packed light, quad normals, tint layers, material emission, and the selected pipeline. Observations do not change the item or camera. Compare them with the screenshot timestamp; startup light values can change while chunks load. +Private `native-form-audit.json` observations distinguish authored forms, shared scenes, add-on fallback forms, and standard Java dialogs. +For authored forms, the observer reads identity from the form controller and geometry from the shared screen. +The `presentation` field identifies the path. `nativeForm` is true only for an authored form. +Shared scenes can belong to another domain. A Java dialog classification does not prove that Bedrock supplied it. +The bounded geometry report excludes authored labels, URLs, and input text. + Private `movement-audit.jsonl` observations record position, velocity, item use, held item, and selected hotbar slot at each observed movement phase. The `epochMillis` timestamp aligns those observations with external input markers. Use the item and slot fields to distinguish a changed selection from a missing movement impulse. diff --git a/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/NativeFormAudit.java b/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/NativeFormAudit.java index ee8ea53f..f2b44ce0 100644 --- a/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/NativeFormAudit.java +++ b/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/NativeFormAudit.java @@ -17,7 +17,11 @@ /** Read-only, bounded form geometry observation on the client tick thread. */ public final class NativeFormAudit { - private static final String SCREEN = "com.viaversion.viafabricplus.bedrock.screen.form.BedrockNativeFormScreen"; + private static final String LEGACY_SCREEN = "com.viaversion.viafabricplus.bedrock.screen.form.BedrockNativeFormScreen"; + private static final String SCENE_SCREEN = "com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiSceneScreen"; + private static final String FORM_CONTROLLER = "com.viaversion.viafabricplus.bedrock.screen.form.BedrockFormController"; + private static final String FALLBACK_SCREEN = "com.viaversion.viafabricplus.bedrock.screen.form.BedrockFallbackFormScreen"; + private static final String JAVA_DIALOG = "net.minecraft.client.gui.screens.dialog.DialogScreen"; private static final int MAX_NODES = 8192; private static final int MAX_ROWS = 512; private static final int MAX_SCREENS = 32; @@ -35,20 +39,12 @@ public static void tick(Object minecraft) { Object gui = minecraft.getClass().getField("gui").get(minecraft); Object screen = call(gui, "screen"); Object window = call(minecraft, "getWindow"); - Map report = new LinkedHashMap<>(); - report.put("screenClass", screen == null ? "none" : screen.getClass().getName()); - report.put("nativeForm", screen != null && screen.getClass().getName().equals(SCREEN)); + Map report = observe(screen); report.put("guiScale", finite(call(window, "getGuiScale"))); report.put("pixelWidth", call(window, "getWidth")); report.put("pixelHeight", call(window, "getHeight")); report.put("guiWidth", call(window, "getGuiScaledWidth")); report.put("guiHeight", call(window, "getGuiScaledHeight")); - if (Boolean.TRUE.equals(report.get("nativeForm"))) { - report.put("formId", field(screen, "formId")); - report.put("pointer", List.of(finite(field(screen, "pointerX")), finite(field(screen, "pointerY")))); - Object root = field(screen, "root"); - if (root != null) report.putAll(project(root)); - } Path directory = Path.of(Files.readString(Path.of("stackanvil-replay-directory.txt")).trim()); byte[] bytes = new Gson().toJson(report).getBytes(StandardCharsets.UTF_8); if (bytes.length > 1024 * 1024) throw new IllegalStateException("Form audit exceeds output limit"); @@ -68,6 +64,44 @@ public static void tick(Object minecraft) { } } + static Map observe(Object screen) throws ReflectiveOperationException { + Map report = new LinkedHashMap<>(); + String screenClass = screen == null ? "none" : screen.getClass().getName(); + report.put("screenClass", screenClass); + report.put("nativeForm", false); + report.put("nativeScene", false); + report.put("presentation", "other"); + if (screenClass.equals(SCENE_SCREEN) || screenClass.equals(LEGACY_SCREEN)) { + boolean form = screenClass.equals(LEGACY_SCREEN); + if (!form) { + Object controller = field(screen, "controller"); + String controllerClass = controller.getClass().getName(); + report.put("controllerClass", controllerClass); + form = controllerClass.equals(FORM_CONTROLLER); + if (form) report.put("formId", field(controller, "formId")); + } else { + report.put("formId", field(screen, "formId")); + } + report.put("nativeForm", form); + report.put("nativeScene", true); + report.put("presentation", form ? "authored-form" : "authored-scene"); + report.put("pointer", List.of(finite(field(screen, "pointerX")), finite(field(screen, "pointerY")))); + Object root = field(screen, "root"); + if (root != null) report.putAll(project(root)); + } else if (screenClass.equals(FALLBACK_SCREEN)) { + report.put("presentation", "addon-fallback-form"); + report.put("formId", field(screen, "formId")); + } else if (screen != null) { + for (Class type = screen.getClass(); type != null; type = type.getSuperclass()) { + if (type.getName().equals(JAVA_DIALOG)) { + report.put("presentation", "java-dialog"); + break; + } + } + } + return report; + } + static Map project(Object root) throws ReflectiveOperationException { List> controls = new ArrayList<>(); List> other = new ArrayList<>(); diff --git a/test/fixtures/NativeFormAuditSelfTest.java b/test/fixtures/NativeFormAuditSelfTest.java index 2b9040ed..7bc2f0d7 100644 --- a/test/fixtures/NativeFormAuditSelfTest.java +++ b/test/fixtures/NativeFormAuditSelfTest.java @@ -1,6 +1,9 @@ package net.pistonmaster.stackanvil.replay.fabric; import com.viaversion.viaversion.libs.gson.JsonObject; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiSceneScreen; +import com.viaversion.viafabricplus.bedrock.screen.form.BedrockFormController; +import com.viaversion.viafabricplus.bedrock.screen.form.BedrockFallbackFormScreen; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -33,6 +36,25 @@ public static void main(String[] arguments) throws Exception { require(!report.toString().contains("private")); require(NativeFormAudit.lineBreaks("\\n\r\n\r\n") == 2); + Map authored = NativeFormAudit.observe(new NativeUiSceneScreen(new BedrockFormController(37), root)); + require(authored.get("nativeForm").equals(true)); + require(authored.get("presentation").equals("authored-form")); + require(authored.get("formId").equals(37)); + require(authored.get("rows").equals(rows)); + require(authored.get("pointer").equals(List.of(12d, 24d))); + Map shared = NativeFormAudit.observe(new NativeUiSceneScreen(new Object(), root)); + require(shared.get("nativeForm").equals(false)); + require(shared.get("nativeScene").equals(true)); + require(shared.get("presentation").equals("authored-scene")); + require(!shared.containsKey("formId")); + require(shared.get("rows").equals(rows)); + Map fallback = NativeFormAudit.observe(new BedrockFallbackFormScreen(37)); + require(fallback.get("nativeForm").equals(false)); + require(fallback.get("presentation").equals("addon-fallback-form")); + require(fallback.get("formId").equals(37)); + require(NativeFormAudit.observe(new OrdinaryDialog()).get("presentation").equals("java-dialog")); + require(NativeFormAudit.observe(null).get("presentation").equals("other")); + List many = new ArrayList<>(); for (int index = 0; index < 600; index++) many.add(child); Box large = new Box(new Node("panel", new JsonObject(), false, Integer.MIN_VALUE), bounds, clip, clip, many); @@ -50,6 +72,8 @@ public static void main(String[] arguments) throws Exception { } catch (IllegalStateException expected) { } } + private static final class OrdinaryDialog extends net.minecraft.client.gui.screens.dialog.DialogScreen { } + public record Rect(double x, double y, double width, double height) { } public record Box(Node node, Rect bounds, Rect clip, Rect visibleBounds, List children) { public float layer() { return 0; } diff --git a/test/fixtures/native-form-audit/BedrockFallbackFormScreen.java b/test/fixtures/native-form-audit/BedrockFallbackFormScreen.java new file mode 100644 index 00000000..4cd6b72f --- /dev/null +++ b/test/fixtures/native-form-audit/BedrockFallbackFormScreen.java @@ -0,0 +1,6 @@ +package com.viaversion.viafabricplus.bedrock.screen.form; + +public final class BedrockFallbackFormScreen { + private final int formId; + public BedrockFallbackFormScreen(int formId) { this.formId = formId; } +} diff --git a/test/fixtures/native-form-audit/BedrockFormController.java b/test/fixtures/native-form-audit/BedrockFormController.java new file mode 100644 index 00000000..ad51d5b2 --- /dev/null +++ b/test/fixtures/native-form-audit/BedrockFormController.java @@ -0,0 +1,6 @@ +package com.viaversion.viafabricplus.bedrock.screen.form; + +public final class BedrockFormController { + private final int formId; + public BedrockFormController(int formId) { this.formId = formId; } +} diff --git a/test/fixtures/native-form-audit/DialogScreen.java b/test/fixtures/native-form-audit/DialogScreen.java new file mode 100644 index 00000000..7d43d5fd --- /dev/null +++ b/test/fixtures/native-form-audit/DialogScreen.java @@ -0,0 +1,3 @@ +package net.minecraft.client.gui.screens.dialog; + +public abstract class DialogScreen { } diff --git a/test/fixtures/native-form-audit/NativeUiSceneScreen.java b/test/fixtures/native-form-audit/NativeUiSceneScreen.java new file mode 100644 index 00000000..7ee706ca --- /dev/null +++ b/test/fixtures/native-form-audit/NativeUiSceneScreen.java @@ -0,0 +1,9 @@ +package com.viaversion.viafabricplus.bedrock.screen.ui; + +/** The current renderer retains geometry on the screen and form identity on its controller. */ +public final class NativeUiSceneScreen { + private final Object controller; + private final Object root; + private final double pointerX = 12, pointerY = 24; + public NativeUiSceneScreen(Object controller, Object root) { this.controller = controller; this.root = root; } +} diff --git a/test/replay-native-form-audit.test.ts b/test/replay-native-form-audit.test.ts index 0afc8de9..4fdc065b 100644 --- a/test/replay-native-form-audit.test.ts +++ b/test/replay-native-form-audit.test.ts @@ -11,14 +11,16 @@ import { artifact } from "../src/client-artifacts.ts"; const execute = promisify(execFile); const built = existsSync(join(root, "dist/viaproxy/manifest.json")); -test.skipIf(!built)("observes clipped form controls with inherited visibility and bounded private metadata", async () => { +test.skipIf(!built)("distinguishes authored forms, shared scenes and fallback dialogs while retaining bounded geometry", async () => { const directory = await mkdtemp(join(tmpdir(), "stackanvil-native-form-audit-")); try { const jar = await artifact("viaproxy"); await execute("javac", ["-cp", jar, "-d", directory, join(root, "src/replay/java/net/pistonmaster/stackanvil/replay/PrivateFiles.java"), join(root, "src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/NativeFormAudit.java"), - join(root, "test/fixtures/NativeFormAuditSelfTest.java")]); + join(root, "test/fixtures/NativeFormAuditSelfTest.java"), + ...["NativeUiSceneScreen", "BedrockFormController", "BedrockFallbackFormScreen", "DialogScreen"] + .map((name) => join(root, "test/fixtures/native-form-audit", `${name}.java`))]); const result = await execute("java", ["-cp", `${directory}${delimiter}${jar}`, "net.pistonmaster.stackanvil.replay.fabric.NativeFormAuditSelfTest", directory], { cwd: directory }); expect(result.stderr).toBeEmpty(); From 4534f0531cc7cec6d2697738b1389a36dce32db0 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 3 Nov 2026 04:03:49 +0100 Subject: [PATCH 4/7] fix(ui): preserve captions for image-only form actions The captured Bedrock 1.26.60.5 Lifeboat Navigator uses a narration key on its image-only Close action. Use the translated source caption when authored controls have no visible label, preserving the original response index and omitting hidden actions. Fold the fix and regression into the owning shared UI patch. Both complete 240-second scene replays retain their source hash. A diagnostic Java-dialog control displays Close and sends form 0 response 8; all owned processes stop normally. --- docs/bedrock-coverage.md | 2 +- ...are-native-ui-programs-and-resources.patch | 54 +++++++++++++++---- ...are-native-ui-programs-and-resources.pr.md | 8 +++ 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index dccd0805..9d5af4df 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -39,7 +39,7 @@ No complete area has verified Bedrock parity. Retain these reports until a matching reproduction establishes the result: -- Lifeboat dialog text was reported as untranslated. For the captured Navigator form, [authored presentation](../patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md) produces the correct title, labels, and response indices. Native and Java routes received identical form bytes. The reported dialog still needs a matching reproduction. +- Lifeboat dialog text was reported as untranslated. For the captured Navigator form, [authored presentation](../patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md) produces the correct title, labels, and response indices. Native and Java routes received identical form bytes. Image-only dialog actions retain their source captions instead of narration keys. The reported dialog still needs a matching reproduction. - Lifeboat's opaque player-head obstruction matches a missing native near-camera fade. World player materials now use the [native ordered fade](../patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.pr.md). GPU checks cover color and depth through native and Iris programs. A matching offline scene reveals the lobby through nearby players. Broader spawn and rendering parity remain open. - WildFFA inventory opened with E shows items without the expected background. - An update from 0.3.61 to 0.4.2 caused reported inventory lag spikes and overlapping menus. 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 f765e75d..d1d7cc37 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 4848b046be5a57d29dd4cdeace1a429f07317806 Mon Sep 17 00:00:00 2001 +From 1a91bd3056e5b8eeab17d84988dc219e63170c50 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 @@ -481,6 +481,17 @@ program and the host policy. Synthetic fixtures now declare real screen content controls rather than relying on the old content-only resolver. A contract boundary control rejects unknown policy versions and prevents legacy programs from claiming the new host behavior. + +Keep narration metadata out of visible Java dialog captions. The official +Bedrock 1.26.60.5 Lifeboat Navigator renders an image-only Close action. +Its authored scene uses accessibility.button.tts.title for narration, +while the form supplies Close at response index 8. Projecting tts_name +as visible text leaks that key into the Java fallback dialog. + +Use visible authored labels when available. For image-only actions, +retain the translated source form caption at the original response index. +Hidden actions remain omitted. A focused presentation regression covers +image-only actions, resolved labels, and sparse response identities. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -521,7 +532,7 @@ legacy programs from claiming the new host behavior. .../resourcepack/ui/NpcEditorBindings.java | 329 ++++ .../api/resourcepack/ui/NpcEditorState.java | 322 +++ .../api/resourcepack/ui/RecipeCollection.java | 266 +++ - .../api/resourcepack/ui/ServerFormUi.java | 253 +++ + .../api/resourcepack/ui/ServerFormUi.java | 256 +++ .../resourcepack/ui/UiAsepriteMetadata.java | 95 + .../api/resourcepack/ui/UiBindings.java | 79 + .../api/resourcepack/ui/UiBitmapFonts.java | 282 +++ @@ -569,7 +580,7 @@ legacy programs from claiming the new host behavior. .../resourcepack/ui/RecipeCollectionTest.java | 227 +++ .../ui/ServerCustomFormUiTest.java | 146 ++ .../ui/ServerFormUiSliderStateTest.java | 126 ++ - .../api/resourcepack/ui/ServerFormUiTest.java | 427 ++++ + .../api/resourcepack/ui/ServerFormUiTest.java | 445 +++++ .../ui/UiAsepriteMetadataTest.java | 112 ++ .../resourcepack/ui/UiBitmapFontsTest.java | 285 +++ .../ui/UiCollectionWindowTest.java | 131 ++ @@ -578,7 +589,7 @@ legacy programs from claiming the new host behavior. .../api/resourcepack/ui/UiTexturesTest.java | 231 +++ .../ui/native-inventory-item-filter.json | 311 +++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 96 files changed, 14747 insertions(+), 60 deletions(-) + 96 files changed, 14768 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 @@ -671,10 +682,10 @@ legacy programs from claiming the new host behavior. create mode 100644 src/tool/java/net/raphimc/viabedrock/tool/generator/SearchUnicodeDataGenerator.java diff --git a/build.gradle b/build.gradle -index 2fd72ae4..8e9eab8b 100644 +index 8e003758..7d4816d6 100644 --- a/build.gradle +++ b/build.gradle -@@ -238,3 +238,14 @@ class MockitoAgentArgumentProvider implements org.gradle.process.CommandLineArgu +@@ -239,3 +239,14 @@ class MockitoAgentArgumentProvider implements org.gradle.process.CommandLineArgu return ["-javaagent:${this.classpath.singleFile}"] } } @@ -7356,10 +7367,10 @@ index 00000000..cb7249f2 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUi.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUi.java new file mode 100644 -index 00000000..d2ec9215 +index 00000000..232c8e76 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUi.java -@@ -0,0 +1,253 @@ +@@ -0,0 +1,256 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -7561,6 +7572,9 @@ index 00000000..d2ec9215 + final Map labels = new LinkedHashMap<>(); + final List prose = new ArrayList<>(); + project(scene.getAsJsonObject("root"), labels, prose); ++ final JsonArray buttons = scene.getAsJsonObject("fallback").getAsJsonArray("buttons"); ++ labels.replaceAll((index, caption) -> caption.isBlank() && index < buttons.size() ++ ? string(buttons.get(index).getAsJsonObject(), "text") : caption); + final String title = prose.isEmpty() ? string(scene.getAsJsonObject("fallback"), "title") : prose.removeFirst(); + return new Presentation(title, String.join("\n\n", prose), Collections.unmodifiableMap(labels)); + } @@ -7574,7 +7588,7 @@ index 00000000..d2ec9215 + if (index >= 0) { + final List text = new ArrayList<>(); + captions(node, text); -+ final String caption = text.isEmpty() ? string(node, "tts_name") : String.join("\n", text); ++ final String caption = String.join("\n", text); + labels.merge(index, caption, (earlier, later) -> earlier.isBlank() ? later : earlier); + } + return; @@ -14986,10 +15000,10 @@ index 00000000..362380ae +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java new file mode 100644 -index 00000000..056d3123 +index 00000000..6ec93f7f --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java -@@ -0,0 +1,427 @@ +@@ -0,0 +1,445 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -15156,6 +15170,24 @@ index 00000000..056d3123 + } + + @Test ++ void imageOnlyDialogActionsUseTheOriginalCaptionInsteadOfNarrationKeys() { ++ final var scene = JsonParser.parseString(""" ++ {"root":{"type":"panel","children":[ ++ {"type":"label","text":"Navigator"}, ++ {"type":"button","action":2,"tts_name":"accessibility.button.tts.title", ++ "children":[{"type":"image","texture":"textures/ui/close"}]}, ++ {"type":"button","action":0,"tts_name":"narration.choice", ++ "children":[{"type":"label","text":"Resolved choice"}]}, ++ {"type":"button","action":1,"visible":false,"tts_name":"hidden.choice"}]}, ++ "fallback":{"title":"selector_Navigator","buttons":[ ++ {"text":"Source choice"},{"text":"Hidden choice"},{"text":"Close"}]}} ++ """).getAsJsonObject(); ++ final var presentation = ServerFormUi.presentation(scene); ++ assertEquals("Navigator", presentation.title()); ++ assertEquals(Map.of(0, "Resolved choice", 2, "Close"), presentation.buttons()); ++ } ++ ++ @Test + void javaPresentationUsesResolvedCaptionsAndRetainsSparseResponseIndices() { + final var scene = new ServerFormUi(List.of(definitions(UI))).resolve(form("tile:One", "unused", "tile:Three")); + final var presentation = ServerFormUi.presentation(scene); 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 dccaea77..c40bcb7d 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 @@ -85,3 +85,11 @@ Program export preserves the selected format and pack-layer precedence. PNG pref Regressions cover byte preservation, both override directions, program round trips, metadata, and resource export. The rebuilt add-on accepts MegaSMP's converted pack under Java's existing download cap and reaches a visible lobby. A fresh walking control remains grounded. The attempted hotbar menu input opens no screen; complete gameplay remains unverified. + +Java dialog projection uses visible authored labels and the original translated caption for image-only actions. +The captured 1.26.60.5 Navigator Close action previously exposed a narration key. +The corrected projection retains Close at response index 8 and preserves the other eight captions and indices. +A regression covers image-only captions, authored labels, and hidden actions. +A rebuilt Java dialog shows the captured Navigator labels and sends response 8 when the source Close action is clicked. +The local diagnostic control disables authored-form capability without changing the recorded scene packets. +The original reported dialog remains unmatched. From 692dec54bb3b03f5b09264d5cb19b2258b7b1ed5 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 3 Nov 2026 04:45:22 +0100 Subject: [PATCH 5/7] fix(ui): retain resolved form fallbacks after render failures Reuse the shared action-form projection for ordinary client controls. Retain resolved titles, visible captions and original response indices while validating against the full source count. Invalid projections preserve the intact original form, and custom forms keep their typed fallback. Bound the shared projection before caption traversal and fold both changes into their owning patches. Regression tests exercise sparse widget responses, duplicate prevention and rejected scene data. Paired 240-second Lifeboat failure controls preserve scene bytes, remove fourteen placeholders and send the original Close response 8. --- docs/bedrock-coverage.md | 2 +- ...are-native-ui-programs-and-resources.patch | 60 +++++- ...are-native-ui-programs-and-resources.pr.md | 1 + ...ender-authored-forms-and-npc-dialogs.patch | 185 +++++++++++++++--- ...ender-authored-forms-and-npc-dialogs.pr.md | 73 +++---- 5 files changed, 235 insertions(+), 86 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 9d5af4df..8d767ce8 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -39,7 +39,7 @@ No complete area has verified Bedrock parity. Retain these reports until a matching reproduction establishes the result: -- Lifeboat dialog text was reported as untranslated. For the captured Navigator form, [authored presentation](../patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md) produces the correct title, labels, and response indices. Native and Java routes received identical form bytes. Image-only dialog actions retain their source captions instead of narration keys. The reported dialog still needs a matching reproduction. +- Lifeboat dialog text was reported as untranslated. Captured Navigator titles, captions, and response indices survive both [Java dialog projection](../patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md) and [add-on render failures](../patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.pr.md). These controls preserve the official 1.26.60.5 scene bytes. The original reported dialog remains unmatched. - Lifeboat's opaque player-head obstruction matches a missing native near-camera fade. World player materials now use the [native ordered fade](../patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.pr.md). GPU checks cover color and depth through native and Iris programs. A matching offline scene reveals the lobby through nearby players. Broader spawn and rendering parity remain open. - WildFFA inventory opened with E shows items without the expected background. - An update from 0.3.61 to 0.4.2 caused reported inventory lag spikes and overlapping menus. 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 d1d7cc37..8c93f040 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 1a91bd3056e5b8eeab17d84988dc219e63170c50 Mon Sep 17 00:00:00 2001 +From 2b4313d69074e2e1e33e466164f6815c17394e02 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 @@ -492,6 +492,13 @@ Use visible authored labels when available. For image-only actions, retain the translated source form caption at the original response index. Hidden actions remain omitted. A focused presentation regression covers image-only actions, resolved labels, and sparse response identities. + +Bound the public form projection before traversing captions. Client +render-failure fallbacks also consume this API and can receive a scene +that their renderer rejects. Preserve the shared 64-level and 8192-node +scene limits so the client can retain the intact original form when +projection metadata is invalid. A regression rejects an overdeep tree +before caption traversal or fallback-field access. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -532,7 +539,7 @@ image-only actions, resolved labels, and sparse response identities. .../resourcepack/ui/NpcEditorBindings.java | 329 ++++ .../api/resourcepack/ui/NpcEditorState.java | 322 +++ .../api/resourcepack/ui/RecipeCollection.java | 266 +++ - .../api/resourcepack/ui/ServerFormUi.java | 256 +++ + .../api/resourcepack/ui/ServerFormUi.java | 269 +++ .../resourcepack/ui/UiAsepriteMetadata.java | 95 + .../api/resourcepack/ui/UiBindings.java | 79 + .../api/resourcepack/ui/UiBitmapFonts.java | 282 +++ @@ -580,7 +587,7 @@ image-only actions, resolved labels, and sparse response identities. .../resourcepack/ui/RecipeCollectionTest.java | 227 +++ .../ui/ServerCustomFormUiTest.java | 146 ++ .../ui/ServerFormUiSliderStateTest.java | 126 ++ - .../api/resourcepack/ui/ServerFormUiTest.java | 445 +++++ + .../api/resourcepack/ui/ServerFormUiTest.java | 467 +++++ .../ui/UiAsepriteMetadataTest.java | 112 ++ .../resourcepack/ui/UiBitmapFontsTest.java | 285 +++ .../ui/UiCollectionWindowTest.java | 131 ++ @@ -589,7 +596,7 @@ image-only actions, resolved labels, and sparse response identities. .../api/resourcepack/ui/UiTexturesTest.java | 231 +++ .../ui/native-inventory-item-filter.json | 311 +++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 96 files changed, 14768 insertions(+), 60 deletions(-) + 96 files changed, 14803 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 @@ -7367,10 +7374,10 @@ index 00000000..cb7249f2 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUi.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUi.java new file mode 100644 -index 00000000..232c8e76 +index 00000000..668d6c78 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUi.java -@@ -0,0 +1,256 @@ +@@ -0,0 +1,269 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -7571,7 +7578,9 @@ index 00000000..232c8e76 + public static Presentation presentation(final JsonObject scene) { + final Map labels = new LinkedHashMap<>(); + final List prose = new ArrayList<>(); -+ project(scene.getAsJsonObject("root"), labels, prose); ++ final JsonObject root = scene.getAsJsonObject("root"); ++ validateProjection(root, 0, new int[1]); ++ project(root, labels, prose); + final JsonArray buttons = scene.getAsJsonObject("fallback").getAsJsonArray("buttons"); + labels.replaceAll((index, caption) -> caption.isBlank() && index < buttons.size() + ? string(buttons.get(index).getAsJsonObject(), "text") : caption); @@ -7579,6 +7588,17 @@ index 00000000..232c8e76 + return new Presentation(title, String.join("\n\n", prose), Collections.unmodifiableMap(labels)); + } + ++ private static void validateProjection(final JsonObject node, final int depth, final int[] count) { ++ if (depth > 64 || ++count[0] > JsonUiProgram.MAX_NODES) { ++ throw new IllegalArgumentException("Form projection exceeds scene limits"); ++ } ++ if (node.has("children")) { ++ for (final JsonElement child : node.getAsJsonArray("children")) { ++ validateProjection(child.getAsJsonObject(), depth + 1, count); ++ } ++ } ++ } ++ + private static void project(final JsonObject node, final Map labels, final List prose) { + if (node.has("visible") && !UiExpression.truth(node.get("visible"))) { + return; @@ -15000,10 +15020,10 @@ index 00000000..362380ae +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java new file mode 100644 -index 00000000..6ec93f7f +index 00000000..bc7a30e9 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java -@@ -0,0 +1,445 @@ +@@ -0,0 +1,467 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -15170,6 +15190,28 @@ index 00000000..6ec93f7f + } + + @Test ++ void rejectsOverdeepSceneMetadataBeforeProjectingDialogControls() { ++ final JsonObject scene = new JsonObject(), fallback = new JsonObject(); ++ fallback.addProperty("title", "Ordinary form"); ++ fallback.add("buttons", new JsonArray()); ++ scene.add("fallback", fallback); ++ JsonObject root = new JsonObject(); ++ scene.add("root", root); ++ for (int depth = 0; depth < 64; depth++) { ++ final JsonObject child = new JsonObject(); ++ final JsonArray children = new JsonArray(); ++ children.add(child); ++ root.add("children", children); ++ root = child; ++ } ++ assertEquals("Ordinary form", ServerFormUi.presentation(scene).title()); ++ final JsonArray beyondLimit = new JsonArray(); ++ beyondLimit.add(new JsonObject()); ++ root.add("children", beyondLimit); ++ assertThrows(IllegalArgumentException.class, () -> ServerFormUi.presentation(scene)); ++ } ++ ++ @Test + void imageOnlyDialogActionsUseTheOriginalCaptionInsteadOfNarrationKeys() { + final var scene = JsonParser.parseString(""" + {"root":{"type":"panel","children":[ 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 c40bcb7d..3ef0e5aa 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 @@ -90,6 +90,7 @@ Java dialog projection uses visible authored labels and the original translated The captured 1.26.60.5 Navigator Close action previously exposed a narration key. The corrected projection retains Close at response index 8 and preserves the other eight captions and indices. A regression covers image-only captions, authored labels, and hidden actions. +A client-facing projection regression accepts the scene depth boundary and rejects an overdeep tree before caption traversal. A rebuilt Java dialog shows the captured Navigator labels and sends response 8 when the source Close action is clicked. The local diagnostic control disables authored-form capability without changing the recorded scene packets. The original reported dialog remains unmatched. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.patch b/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.patch index f2efb618..7558a051 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.patch @@ -1,4 +1,4 @@ -From 36a5b02df60ad15a78fb9daa3073d1763d49e98b Mon Sep 17 00:00:00 2001 +From b81cd4ed7cd4904976a727742d246ac2b6f5633a 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): render authored Bedrock forms and NPC dialogs @@ -150,12 +150,29 @@ NPC conversations and NPC editors. Retired listeners cannot open a scene, submit a form, close the new conversation or save editor changes over a reused transport. Queued receiver and response tests exercise real client listeners that share one still-connected network connection. + +Use the shared resolved action-form presentation after a client renderer +failure. The official Bedrock 1.26.60.5 Lifeboat Navigator has eight menu +choices and an image-only Close action. Its source title is a selector, +and fourteen source buttons are placeholders. A controlled renderer +exception previously exposed that selector and all empty buttons. + +Keep the resolved title, prose and visible captions in ordinary controls. +Display choices in source order and retain each original response index. +Validate authored actions against the full source button count, rather +than the number of visible fallback choices. Unknown or malformed scene +projections retain the intact original form. Custom forms retain their +typed protocol fallback instead of adopting an action-form projection. + +Regressions cover rejected renderers, sparse response indices, malformed +projection data, custom-form titles, and actual fallback widget clicks. +Filtering rows must never renumber a response or send a second reply. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../screen/form/BedrockCustomFormState.java | 159 +++++++++ - .../form/BedrockFallbackFormScreen.java | 120 +++++++ + .../form/BedrockFallbackFormScreen.java | 119 +++++++ .../screen/form/BedrockFormController.java | 107 ++++++ - .../bedrock/screen/form/BedrockFormScene.java | 60 ++++ + .../bedrock/screen/form/BedrockFormScene.java | 84 +++++ .../screen/form/BedrockNativeForms.java | 89 +++++ .../screen/form/NativeCustomFormPayload.java | 25 ++ .../screen/form/NativeFormPayload.java | 24 ++ @@ -167,12 +184,12 @@ listeners that share one still-connected network connection. .../screen/npc/NativeNpcEditorPayload.java | 22 ++ .../form/BedrockCustomFormEditingTest.java | 110 ++++++ .../BedrockCustomFormInteractionTest.java | 219 ++++++++++++ - .../screen/form/BedrockFormSceneTest.java | 66 ++++ - .../BedrockNativeFormWidgetIdentityTest.java | 105 ++++++ + .../screen/form/BedrockFormSceneTest.java | 124 +++++++ + .../BedrockNativeFormWidgetIdentityTest.java | 138 ++++++++ .../screen/form/BedrockNativeFormsTest.java | 69 ++++ .../screen/npc/BedrockNativeNpcsTest.java | 313 ++++++++++++++++++ .../npc/BedrockNpcConversationTest.java | 106 ++++++ - 21 files changed, 1976 insertions(+) + 21 files changed, 2090 insertions(+) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockCustomFormState.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFallbackFormScreen.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFormController.java @@ -385,10 +402,10 @@ index 0000000..f9c78f5 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFallbackFormScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFallbackFormScreen.java new file mode 100644 -index 0000000..a4509d5 +index 0000000..2085b60 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFallbackFormScreen.java -@@ -0,0 +1,120 @@ +@@ -0,0 +1,119 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -430,10 +447,9 @@ index 0000000..a4509d5 + this.buttons.clear(); + this.panelWidth = Math.max(40, Math.min(360, this.width - 32)); + this.contentHeight = this.font.split(this.content, this.panelWidth).size() * this.font.lineHeight; -+ for (int i = 0; i < this.form.buttons().size(); i++) { -+ final int index = i; -+ this.buttons.add(this.addRenderableWidget(Button.builder(NativeUiText.component(this.form.buttons().get(i)), -+ _ -> this.respond(index)).bounds((this.width - this.panelWidth) / 2, 0, this.panelWidth, 20).build())); ++ for (final var button : this.form.buttons()) { ++ this.buttons.add(this.addRenderableWidget(Button.builder(NativeUiText.component(button.caption()), ++ _ -> this.respond(button.responseIndex())).bounds((this.width - this.panelWidth) / 2, 0, this.panelWidth, 20).build())); + } + this.position(); + } @@ -624,10 +640,10 @@ index 0000000..39e82e7 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFormScene.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFormScene.java new file mode 100644 -index 0000000..12a23dd +index 0000000..26dd934 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFormScene.java -@@ -0,0 +1,60 @@ +@@ -0,0 +1,84 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -635,13 +651,14 @@ index 0000000..12a23dd + */ +package com.viaversion.viafabricplus.bedrock.screen.form; + -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonParser; ++import com.viaversion.viaversion.libs.gson.JsonElement; ++import com.viaversion.viaversion.libs.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonParser; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiScene; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; ++import net.raphimc.viabedrock.api.resourcepack.ui.ServerFormUi; + +/** A bounded, core-resolved scene. Layout never interprets server response indices. */ +public record BedrockFormScene(NativeUiScene.Node root, Fallback fallback) { @@ -651,7 +668,7 @@ index 0000000..12a23dd + final JsonObject scene = JsonParser.parseString(json).getAsJsonObject(); + final Fallback fallback = fallback(scene); + final NativeUiScene.Node root = NativeUiScene.parse(json, RENDERERS).root(); -+ validateActions(root, fallback.buttons.size()); ++ validateActions(root, fallback.sourceButtonCount()); + return new BedrockFormScene(root, fallback); + } + @@ -672,13 +689,32 @@ index 0000000..12a23dd + private static Fallback fallback(final JsonObject scene) { + final JsonObject form = scene.getAsJsonObject("fallback"); + if (form == null) throw new IllegalArgumentException("Missing native form fallback"); -+ final List buttons = new ArrayList<>(); ++ final List buttons = new ArrayList<>(); + if (form.has("buttons")) { + final var entries = form.getAsJsonArray("buttons"); + if (entries.size() > 1024) throw new IllegalArgumentException("Too many form buttons"); -+ for (final var entry : entries) buttons.add(string(entry.getAsJsonObject(), "text", "")); ++ for (int index = 0; index < entries.size(); index++) ++ buttons.add(new FallbackButton(index, string(entries.get(index).getAsJsonObject(), "text", ""))); ++ } ++ final var original = new Fallback(string(form, "title", ""), string(form, "content", ""), buttons.size(), buttons); ++ if (!scene.has("root") || !scene.get("root").isJsonObject() || !scene.has("format")) return original; ++ try { ++ // Custom forms retain their title and typed input fallback in the protocol layer. ++ if (scene.get("format").getAsInt() != 1) return original; ++ final var presentation = ServerFormUi.presentation(scene); ++ if (presentation.buttons().isEmpty() || presentation.buttons().keySet().stream() ++ .anyMatch(index -> index < 0 || index >= original.sourceButtonCount())) return original; ++ final List projected = new ArrayList<>(); ++ for (final var button : original.buttons()) { ++ final String caption = presentation.buttons().get(button.responseIndex()); ++ if (caption != null) projected.add(new FallbackButton(button.responseIndex(), ++ caption.isBlank() ? button.caption() : caption)); ++ } ++ return new Fallback(presentation.title(), presentation.content(), original.sourceButtonCount(), projected); ++ } catch (final RuntimeException unsupported) { ++ // A rejected scene can still carry an intact original form. ++ return original; + } -+ return new Fallback(string(form, "title", ""), string(form, "content", ""), List.copyOf(buttons)); + } + + public static String string(final JsonObject properties, final String key, final String fallback) { @@ -686,7 +722,11 @@ index 0000000..12a23dd + return value == null || !value.isJsonPrimitive() ? fallback : value.getAsString(); + } + -+ public record Fallback(String title, String content, List buttons) {} ++ public record FallbackButton(int responseIndex, String caption) {} ++ ++ public record Fallback(String title, String content, int sourceButtonCount, List buttons) { ++ public Fallback { buttons = List.copyOf(buttons); } ++ } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockNativeForms.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockNativeForms.java new file mode 100644 @@ -1623,10 +1663,10 @@ index 0000000..942e4c6 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFormSceneTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFormSceneTest.java new file mode 100644 -index 0000000..11d2a81 +index 0000000..cea6706 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockFormSceneTest.java -@@ -0,0 +1,66 @@ +@@ -0,0 +1,124 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1638,6 +1678,7 @@ index 0000000..11d2a81 +import com.google.gson.JsonObject; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiLayout; +import java.util.Map; ++import java.util.List; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + @@ -1659,6 +1700,63 @@ index 0000000..11d2a81 + assertThrows(IllegalArgumentException.class, () -> layout(scene(button), 320, 200)); + } + ++ @Test ++ void failedAuthoredControlsKeepResolvedFallbackCaptionsAndSparseResponses() { ++ final JsonObject root = node("panel", 200, 100); ++ final JsonObject title = node("label", 200, 20); ++ title.addProperty("text", "Menu"); ++ final JsonObject choice = node("button", 200, 20); ++ choice.addProperty("action", 0); ++ final JsonObject label = node("label", 200, 20); ++ label.addProperty("text", "Resolved choice"); ++ final JsonArray captions = new JsonArray(); ++ captions.add(label); ++ choice.add("children", captions); ++ final JsonObject close = node("custom", 20, 20); ++ close.addProperty("renderer", "unsupported_renderer"); ++ close.addProperty("action", 8); ++ close.addProperty("tts_name", "accessibility.button.tts.title"); ++ final JsonObject hidden = node("button", 200, 20); ++ hidden.addProperty("action", 1); ++ hidden.addProperty("visible", false); ++ final JsonArray children = new JsonArray(); ++ children.add(title); children.add(close); children.add(choice); children.add(hidden); ++ root.add("children", children); ++ final JsonObject payload = json(root); ++ payload.getAsJsonObject("fallback").addProperty("title", "selector_Menu"); ++ payload.getAsJsonObject("fallback").getAsJsonArray("buttons").get(8).getAsJsonObject().addProperty("text", "Close"); ++ ++ assertThrows(IllegalArgumentException.class, () -> BedrockFormScene.parse(payload.toString())); ++ final var fallback = BedrockFormScene.fallback(payload.toString()); ++ assertEquals("Menu", fallback.title()); ++ assertEquals(10, fallback.sourceButtonCount()); ++ assertEquals(List.of(new BedrockFormScene.FallbackButton(0, "Resolved choice"), ++ new BedrockFormScene.FallbackButton(8, "Close")), fallback.buttons()); ++ // The source index remains valid even when it exceeds the displayed button count. ++ close.addProperty("type", "button"); ++ assertEquals(fallback, BedrockFormScene.parse(payload.toString()).fallback()); ++ } ++ ++ @Test ++ void malformedProjectionKeepsOriginalActionsAndCustomFormsKeepTheirOwnTitle() { ++ final JsonObject root = node("label", 200, 20); ++ root.addProperty("text", "First question"); ++ final JsonObject custom = json(root); ++ custom.addProperty("format", 2); ++ custom.getAsJsonObject("fallback").addProperty("title", "Questionnaire"); ++ custom.getAsJsonObject("fallback").add("buttons", new JsonArray()); ++ assertEquals("Questionnaire", BedrockFormScene.fallback(custom.toString()).title()); ++ final JsonObject malformed = json(root); ++ malformed.getAsJsonObject("fallback").addProperty("title", "Original form"); ++ final JsonArray invalid = new JsonArray(); ++ invalid.add("unsupported child"); ++ root.add("children", invalid); ++ final var fallback = BedrockFormScene.fallback(malformed.toString()); ++ assertEquals("Original form", fallback.title()); ++ assertEquals(10, fallback.buttons().size()); ++ assertEquals(new BedrockFormScene.FallbackButton(9, "9"), fallback.buttons().getLast()); ++ } ++ + private static JsonObject json(final JsonObject root) { + final JsonObject value = new JsonObject(), fallback = new JsonObject(); + value.addProperty("format", 1); @@ -1695,10 +1793,10 @@ index 0000000..11d2a81 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockNativeFormWidgetIdentityTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockNativeFormWidgetIdentityTest.java new file mode 100644 -index 0000000..0ff802f +index 0000000..35b530a --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/form/BedrockNativeFormWidgetIdentityTest.java -@@ -0,0 +1,105 @@ +@@ -0,0 +1,138 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1717,6 +1815,7 @@ index 0000000..0ff802f +import net.minecraft.client.Minecraft; +import net.minecraft.client.StringSplitter; +import net.minecraft.client.gui.Font; ++import net.minecraft.client.gui.components.Button; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.input.MouseButtonInfo; +import net.minecraft.client.sounds.SoundManager; @@ -1748,7 +1847,8 @@ index 0000000..0ff802f + json.addProperty("format", 2); + json.add("root", root); + final var scene = new BedrockFormScene(NativeUiScene.parse(json.toString(), Set.of()).root(), -+ new BedrockFormScene.Fallback("", "", List.of("", "", ""))); ++ new BedrockFormScene.Fallback("", "", 3, List.of(new BedrockFormScene.FallbackButton(0, ""), ++ new BedrockFormScene.FallbackButton(1, ""), new BedrockFormScene.FallbackButton(2, "")))); + final Font font = mock(Font.class); + when(font.getSplitter()).thenReturn(new StringSplitter((c, style) -> 6)); + final Minecraft game = mock(Minecraft.class); @@ -1781,6 +1881,37 @@ index 0000000..0ff802f + } + } + ++ @Test ++ void filteredFallbackButtonsKeepTheirSourceIndexAndRespondOnlyOnce() throws Exception { ++ SharedConstants.tryDetectVersion(); ++ Bootstrap.bootStrap(); ++ final Font font = mock(Font.class); ++ when(font.getSplitter()).thenReturn(new StringSplitter((c, style) -> 6)); ++ final Minecraft game = mock(Minecraft.class); ++ when(game.getSoundManager()).thenReturn(mock(SoundManager.class)); ++ final var fontField = Minecraft.class.getField("font"); ++ fontField.setAccessible(true); ++ fontField.set(game, font); ++ try (final var singleton = mockStatic(Minecraft.class); final var forms = mockStatic(BedrockNativeForms.class)) { ++ singleton.when(Minecraft::getInstance).thenReturn(game); ++ final var fallback = new BedrockFormScene.Fallback("Menu", "", 10, List.of( ++ new BedrockFormScene.FallbackButton(0, "Choice"), new BedrockFormScene.FallbackButton(8, "Close"))); ++ final var screen = new BedrockFallbackFormScreen(null, 27, fallback); ++ screen.width = 320; ++ screen.height = 200; ++ screen.init(); ++ assertEquals(2, screen.children().size()); ++ assertEquals("Close", ((Button) screen.children().get(1)).getMessage().getString()); ++ assertTrue(screen.mouseClicked(new MouseButtonEvent(160, 70, ++ new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT, 0)), false)); ++ forms.verify(() -> BedrockNativeForms.respond(null, 27, 8)); ++ screen.mouseClicked(new MouseButtonEvent(160, 70, ++ new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT, 0)), false); ++ forms.verifyNoMoreInteractions(); ++ verify(game).setScreenAndShow(null); ++ } ++ } ++ + private static void initialize(final NativeUiSceneScreen screen) throws Exception { + final var init = NativeUiSceneScreen.class.getDeclaredMethod("init"); + init.setAccessible(true); diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.pr.md index 039b5198..c2b55d3c 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.pr.md @@ -1,60 +1,35 @@ -Forms and NPC dialogs consume the [shared scene renderer](0029-share-native-ui-scene-rendering.pr.md). Container presentation has its [own patch](0029-present-authored-native-containers.pr.md). Preserve response identities and ordinary form fallback. +# Authored Bedrock forms and NPC dialogs ## Purpose -Render server-authored Bedrock form and container scenes after ViaBedrock resolves pack definitions, variables and bindings. Keep each original form ID and button index. Use the existing Java click-action response path and a negotiated client payload. Negotiate native custom-form support separately, and keep the original content indices and response types. +Present core-resolved forms and NPC dialogs through the [shared scene renderer](0029-share-native-ui-scene-rendering.pr.md). +Keep each original form ID, action index, and typed response. +Container presentation has its [own patch](0029-present-authored-native-containers.pr.md). -The renderer uses accepted pack textures and bundled licensed UI definitions. Pack paths use accepted textures; public HTTPS image URLs load asynchronously without account credentials or another package acquisition. No local Bedrock installation is required. Ordinary Java controls remain the fallback when a visible control or dimension cannot be rendered. Missing or corrupt individual images retain usable captions and responses. +## Design constraints -## Behavior +The core owns pack selection, authored bindings, and scene contracts. +The client owns rendering, input, focus, scrolling, narration, and resource lifetimes. +Shared controls retain their identities across scene refreshes. -- Keep authored control hierarchy, anchors, signed offsets, stack and grid layouts, content dimensions, layers and clipping. -- Default image dimensions inherit the control's owner extent. Use texture aspect sizing only when `default_size_scales_to_ratio` is enabled. Slider skins therefore fill their authored track and thumb bounds. -- Apply native small, normal, large and extra-large font multipliers consistently to label measurements, wrapping and drawing. Multiply by the authored font factor without imposing a minimum size. A zero factor has zero text extent; unknown size names use normal, as the native parser does. Visible negative factors retain ordinary controls until native layout and mirrored drawing are verified. Hidden controls with negative factors remain accepted. -- Measure wrapped labels within their resolved width and clip text to authored bounds. Draw labels, accepted pack images, public HTTPS images and native four-side nine-slice borders. Select authored default, hover and locked visual branches. Support the native `hover_text_renderer` through formatted Java tooltips; other visible custom renderers retain the ordinary-form fallback. -- Play resolved alpha tracks, including initially transparent cards and hover branch reentry. -- Keep hidden controls, authored properties and collection indices. Reset their computed size metrics to zero through inherited visibility, as the original visibility setter and dependency scheduler do. Child, peer, stack cursor and fill calculations consume those computed metrics. Keep disabled controls measured. Default stacks sum their main axis and inherit their cross axis from the parent; explicit content dimensions retain their own dependency rules. Do not prune collection entries or remap grid response indices. -- Use Java focus, narration, tooltips, keyboard navigation and pointer handling. Provide bounded vertical and horizontal scrolling with proportional, draggable thumbs. -- Omit individually missing or corrupt valid images with a readable status, preserving usable captions and responses. Invalid resource identifiers keep the ordinary-form fallback. -- Reject stale-connection responses and send at most one response from a screen. -- Keep original buttons usable while remote images load or fail. Fetch and decode on four workers, validate redirects, share a 20-second deadline, and cap each image at 4 MiB and four million pixels, with a sixteen-million-pixel screen budget including pending uploads. Cache normalized PNGs by target version and full URL hash with 128-entry and 64 MiB limits. Release each screen's dynamic textures when it closes. Do not retain failed requests across reopened menus. +After an action-form renderer fails, ordinary controls use the shared resolved title, prose, and visible captions. +Each displayed button retains its source response index. +Authored action validation uses the full source count, independently of the displayed count. +An unusable projection retains the intact original form. Custom forms retain their typed protocol fallback. -## Custom controls +Prerequisite: [shared scene rendering](0029-share-native-ui-scene-rendering.patch) must land before these controllers. -Resolve labels, headers, dividers, toggles, text inputs, dropdowns, sliders and step sliders through the authored UI program. Keep input widgets stable while dependent controls resolve again, preserving caret, selection and focus. Expanded dropdowns use their authored branches. Responses retain null slots for prose and the original boolean, text, scalar or option values. Unsupported visible controls retain ordinary controls. +## Validation and remaining limits -Apply native slider placement after layout. Keep authored anchors, position the thumb along the full owner extent, and preserve the progress clip ratio and step rounding. Select default, hover, disabled and pressed visual states before measuring their branches. Hidden descendants retain their authored visibility. Use the current Minecraft mouse-button constant for clicks, drags and releases; its SDL left-button value is 1. +`BedrockFormSceneTest` covers rejected controls, invalid actions, resolved fallback captions, sparse indices, malformed projections, and custom-form titles. +`BedrockNativeFormWidgetIdentityTest` exercises repeated controls, retained widget identities, fallback clicks, and duplicate-response prevention. +Other form and NPC regressions cover typed inputs, ownership, dismissal, and conversation lifetimes. -Apply image clipping to each native slice in submission order. Pixel-perfect clipping rounds against source pixels. Keep corner slices and signed interiors, and omit only non-finite output quads. Active clipping bypasses the single-image aspect fit; nine-slice images also bypass that fit. +The official Bedrock 1.26.60.5 Navigator supplies eight menu choices, an image-only Close action, and fourteen placeholder buttons. +Paired 240-second Java renderer-failure controls preserve the recorded scene packets. +The old fallback exposes the selector title and fourteen empty buttons. The corrected fallback shows Navigator and nine intended actions. +Clicking Close sends the original response 8 exactly once. +Private screenshots and traffic evidence remain outside Git. -## Evidence and limits - -Research uses the pinned official 1.26.51.1 native executable and actual captured resource-pack definitions. Original native controls bind scalar/four-side nine-slice parsing, missing-template child omission, grid rescaling ratios, dimension token parsing and child/peer dependency enumeration. The original native image draw consumer also binds left/top/right/bottom sides, corner-edge-center submission order, UV border scaling by both metadata dimensions, and unclamped signed interiors on undersized destinations. The Java submission normalizes inverted destination bounds while swapping float UV endpoints through the existing GUI pipeline. Native GPU winding and culling remain unverified. The original visibility setter, metric constructor, callback, registrar and dependency scheduler also bind fresh and re-hidden size resets, default stack axes, nested hidden-descendant wrappers and header fill allocation. Factory hierarchy and visibility flags are supplied in these controls; collection clone/population and controller notifications remain outside the fixture. The original label loader, enum parser and text-submission consumer bind font-size multipliers of 0.5, 1, 2 and 4, multiplied by the authored factor. The wrapping consumer uses the same product. Named-font lookup is case-sensitive, but the native registry-to-file initialization and glyph renderer are not yet bound. Acquired metadata-selected TTF families now use isolated Java font resources, with native hhea size and baseline normalization and default glyph fallback. Alias/MSDF families and exact native raster pixels remain unverified. These results do not establish full native pixel layout. - -The Java layout solver still uses bounded iterative measurement. Its mixed-template grid extents, zero maximum-size sentinel and proportional scrollbar geometry need wider native comparison. All 32 alpha easing curves and alpha/wait successor graphs now have native execution comparisons, including loops and play/reset timing. Native font-family glyph metrics, baseline and tooltip placement, other animation outputs and complete input-event delivery remain open. Authored final static dimensions remain usable when a size animation is present. - -## Asset pipeline boundary - -The addon provider can supply bundled `ui/*.json` definitions and textures to the core when its optional `builtin_ui.zip` is absent. The five addon asset ZIPs contain zero `ui/` definitions. StackAnvil now tracks a separate versioned core UI bundle with native templates, textures, image metadata, font metadata, font faces, and bitmap font pages. Full stack builds require this bundle and verify its exact bytes in the core, ViaProxy, and the add-on's embedded core JAR. This corrects the default CI asset gap without requiring a local Bedrock installation at runtime. - -The provider reads its packaged, version-checked asset library. It does not acquire missing UI definitions from a Microsoft Store session or a versioned runtime package cache. If the entire addon asset bundle is absent, its loader throws instead of returning an empty layer. That loading behavior remains separate from the corrected build inputs. Native MSDF and alias font rendering remain outside the verified font path. - -## Verification - -- Twenty-six targeted layout and image-region tests cover anchors, sparse original actions, visibility lifecycle, disabled-control geometry, hidden descendant wrappers, default stack axes, wrapping, clipping, grid rescaling, drag clamps, resize, alpha branch reentry and native asymmetric/signed/float-UV regions. Five regression cases fail without the computed-visibility fix and pass with it. -- Three production HTTP fixture tests cover nonblocking deduplication, offline cache reuse, target-version isolation, redirects, stream bounds, corrupt caches and decoded pixel limits, rejected queues and reopening after a transient HTTP failure. All 18 distinct remote images from five actual captured raw forms download and decode through this loader in a private probe. That result does not prove actual client rendering. -- Private probes parse and lay out all four actual captured ActionForm scenes with inferred font and image metrics. Their original button counts are 19, 36, 8 and 25. At two viewports, the repaired header retains positive width and the first captured Wardrobe category enters its viewport. Original visible action sets remain unchanged. A separate audit resolves all 603 image nodes through actual converted PNGs and cached HTTPS images, then verifies all 4,195 production-generated source and destination regions. Six complete native draw controls match 54 Java region quads and 432 coordinates. These are CPU geometry and real-image-dimension checks, not Java GPU drawing. The Wardrobe scene includes sixteen bound native hover-text nodes. Earlier synthetic Game Selector and variant probes remain separate evidence. These checks do not claim native screenshot parity. -- Three additional font regressions exercise native size factors through wrapped stack allocation, explicit clipping, zero and hidden negative factors, original response identity, normal fallback and small unclamped text. All fail with the prior factor-only implementation and pass with the change. The private font/layout suite passes all 26 cases. A separate gate keeps the original action sets and positive sized captions in all four actual captured scenes at two viewports, using inferred Java glyph metrics. The combined replay and full build now include this source change. -- Before the container extension, the complete 31-patch replay and combined Gradle test, check and build gates passed: 674 tests, 556 passed, 118 skipped, no failures or errors. Access-widener validation passes. This project declares no Checkstyle task. -- Four interaction tests exercise the actual SDL left-button value, dependent toggle visibility, dropdown expansion and selection, typed responses with null slots, inverted scalar and stepped sliders, single-option sliders, and validation of newly visible unsupported controls. The click regression fails before the mouse-button fix. -- Native slider execution fixtures cover 184 placement, pointer, state and progress cases. Image-clipping fixtures compare 80 controls and 1,888 coordinates, plus aspect-dispatch and zero-source cases. These are CPU consumer checks with supplied components; native scheduler integration and GPU drawing remain unverified. -- The current immutable core candidate has SHA-256 `2e348845a6cc04732804dec3b555ecbdb57e9175e7324951544a4eb6799ca6ce`. The paired addon candidate has SHA-256 `99e7a8f8f63e85c83ff9527dae6ebf2f2cb275a2c88a3bf9e8caa2322aab0d04`; all 1,323 approved core namespace, asset and license entries match, excluding the manifest and the Loom-added module descriptor. Detailed evidence remains private. No credentials, captures, screenshots, bundled official assets or private paths are added to this patch. - -Actual client menu rendering, resource reloads and Iris routes must be checked with the final candidate before release. - -The image sizing change passes all 27 layout tests. Seven native default, explicit, hidden and min/max sizing controls and four opt-in aspect controls establish the pinned build's behavior. All five captured Settings step sliders pass CPU layout at two viewports using the actual program and PNG dimensions. Live GPU rendering remains pending for this change. - - -## Upstream prerequisites - -- [viafabricplus-bedrock patch](../../viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md): Land the shared renderer before registering form and NPC controllers. +These selected controls do not establish complete form, NPC, font, or rendering parity. +The original reported untranslated dialog remains unmatched. From cd504e2f0aeeb26194df0f359f7c7429993f9e92 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 3 Nov 2026 05:16:24 +0100 Subject: [PATCH 6/7] test(ui): satisfy form regression declaration style --- ...095-share-native-ui-programs-and-resources.patch | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 8c93f040..b0f685e3 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 2b4313d69074e2e1e33e466164f6815c17394e02 Mon Sep 17 00:00:00 2001 +From 18fcb4f8b96a6e0c219743ad468ad2815011e9cb 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 @@ -587,7 +587,7 @@ before caption traversal or fallback-field access. .../resourcepack/ui/RecipeCollectionTest.java | 227 +++ .../ui/ServerCustomFormUiTest.java | 146 ++ .../ui/ServerFormUiSliderStateTest.java | 126 ++ - .../api/resourcepack/ui/ServerFormUiTest.java | 467 +++++ + .../api/resourcepack/ui/ServerFormUiTest.java | 468 +++++ .../ui/UiAsepriteMetadataTest.java | 112 ++ .../resourcepack/ui/UiBitmapFontsTest.java | 285 +++ .../ui/UiCollectionWindowTest.java | 131 ++ @@ -596,7 +596,7 @@ before caption traversal or fallback-field access. .../api/resourcepack/ui/UiTexturesTest.java | 231 +++ .../ui/native-inventory-item-filter.json | 311 +++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 96 files changed, 14803 insertions(+), 60 deletions(-) + 96 files changed, 14804 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 @@ -15020,10 +15020,10 @@ index 00000000..362380ae +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java new file mode 100644 -index 00000000..bc7a30e9 +index 00000000..e615972c --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java -@@ -0,0 +1,467 @@ +@@ -0,0 +1,468 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -15191,7 +15191,8 @@ index 00000000..bc7a30e9 + + @Test + void rejectsOverdeepSceneMetadataBeforeProjectingDialogControls() { -+ final JsonObject scene = new JsonObject(), fallback = new JsonObject(); ++ final JsonObject scene = new JsonObject(); ++ final JsonObject fallback = new JsonObject(); + fallback.addProperty("title", "Ordinary form"); + fallback.add("buttons", new JsonArray()); + scene.add("fallback", fallback); From 7872c18116fa34036c126f60da1c37ba0a93162a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 3 Nov 2026 05:20:17 +0100 Subject: [PATCH 7/7] fix(ui): translate projected captions and tolerate absent input metadata Translate image-only source captions in the fallback copy while retaining raw form strings for authored bindings and sparse response indices. Skip non-object input metadata so one control cannot stop later replay audit observations. Cover translated sparse actions and null, array, boolean, and string input metadata. Correct the renderer regression list. --- ...are-native-ui-programs-and-resources.patch | 38 ++++++++++++++----- ...are-native-ui-programs-and-resources.pr.md | 2 +- .../0002-native-entities-and-skins.pr.md | 2 +- .../replay/fabric/NativeFormAudit.java | 23 ++++++----- test/fixtures/NativeFormAuditSelfTest.java | 13 +++++++ 5 files changed, 57 insertions(+), 21 deletions(-) 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 b0f685e3..cd325d5e 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 18fcb4f8b96a6e0c219743ad468ad2815011e9cb Mon Sep 17 00:00:00 2001 +From 0e0579dd8583d16bf07eeef88308e461676b1aa3 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 @@ -539,7 +539,7 @@ before caption traversal or fallback-field access. .../resourcepack/ui/NpcEditorBindings.java | 329 ++++ .../api/resourcepack/ui/NpcEditorState.java | 322 +++ .../api/resourcepack/ui/RecipeCollection.java | 266 +++ - .../api/resourcepack/ui/ServerFormUi.java | 269 +++ + .../api/resourcepack/ui/ServerFormUi.java | 271 +++ .../resourcepack/ui/UiAsepriteMetadata.java | 95 + .../api/resourcepack/ui/UiBindings.java | 79 + .../api/resourcepack/ui/UiBitmapFonts.java | 282 +++ @@ -587,7 +587,7 @@ before caption traversal or fallback-field access. .../resourcepack/ui/RecipeCollectionTest.java | 227 +++ .../ui/ServerCustomFormUiTest.java | 146 ++ .../ui/ServerFormUiSliderStateTest.java | 126 ++ - .../api/resourcepack/ui/ServerFormUiTest.java | 468 +++++ + .../api/resourcepack/ui/ServerFormUiTest.java | 486 +++++ .../ui/UiAsepriteMetadataTest.java | 112 ++ .../resourcepack/ui/UiBitmapFontsTest.java | 285 +++ .../ui/UiCollectionWindowTest.java | 131 ++ @@ -596,7 +596,7 @@ before caption traversal or fallback-field access. .../api/resourcepack/ui/UiTexturesTest.java | 231 +++ .../ui/native-inventory-item-filter.json | 311 +++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 96 files changed, 14804 insertions(+), 60 deletions(-) + 96 files changed, 14824 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 @@ -7374,10 +7374,10 @@ index 00000000..cb7249f2 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUi.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUi.java new file mode 100644 -index 00000000..668d6c78 +index 00000000..53aa191f --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUi.java -@@ -0,0 +1,269 @@ +@@ -0,0 +1,271 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -7503,7 +7503,9 @@ index 00000000..668d6c78 + for (final JsonElement entry : form.getAsJsonArray(collection)) { + final JsonObject element = entry.getAsJsonObject(); + if ("button".equals(string(element, "type"))) { -+ fallbackButtons.add(element.deepCopy()); ++ final JsonObject button = element.deepCopy(); ++ button.addProperty("text", this.program.translate(string(element, "text"))); ++ fallbackButtons.add(button); + } else if (!string(element, "text").isBlank()) { + content.add(string(element, "text")); + } @@ -15020,10 +15022,10 @@ index 00000000..362380ae +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java new file mode 100644 -index 00000000..e615972c +index 00000000..63bf4b98 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java -@@ -0,0 +1,468 @@ +@@ -0,0 +1,486 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -15231,6 +15233,24 @@ index 00000000..e615972c + } + + @Test ++ void imageOnlyCaptionsTranslateWithoutChangingRawBindingValues() { ++ final String images = """ ++ {"namespace":"server_form", ++ "third_party_server_screen":{"$screen_content":"server_form.main_screen_content"}, ++ "main_screen_content":{"type":"grid","collection_name":"form_buttons","grid_item_template":"server_form.row"}, ++ "row":{"type":"button","$pressed_button_name":"button.form_button_click", ++ "bindings":[{"binding_name":"(#form_button_text = '%probe.choice')","binding_name_override":"#visible"}], ++ "controls":[{"icon":{"type":"image","texture":"textures/ui/close"}}]}} ++ """; ++ final JsonObject form = form("unused", "%probe.choice"); ++ final var ui = new ServerFormUi(List.of(definitions(images)), text -> "%probe.choice".equals(text) ? "Translated choice" : text); ++ final JsonObject scene = ui.resolve(form); ++ assertNotNull(scene); ++ assertEquals(Map.of(1, "Translated choice"), ServerFormUi.presentation(scene).buttons()); ++ assertEquals("%probe.choice", form.getAsJsonArray("elements").get(1).getAsJsonObject().get("text").getAsString()); ++ } ++ ++ @Test + void javaPresentationUsesResolvedCaptionsAndRetainsSparseResponseIndices() { + final var scene = new ServerFormUi(List.of(definitions(UI))).resolve(form("tile:One", "unused", "tile:Three")); + final var presentation = ServerFormUi.presentation(scene); 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 3ef0e5aa..898d012a 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 @@ -89,7 +89,7 @@ A fresh walking control remains grounded. The attempted hotbar menu input opens Java dialog projection uses visible authored labels and the original translated caption for image-only actions. The captured 1.26.60.5 Navigator Close action previously exposed a narration key. The corrected projection retains Close at response index 8 and preserves the other eight captions and indices. -A regression covers image-only captions, authored labels, and hidden actions. +Regressions cover image-only captions, authored labels, hidden actions, and translated source captions without changing raw binding values. A client-facing projection regression accepts the scene depth boundary and rejects an overdeep tree before caption traversal. A rebuilt Java dialog shows the captured Navigator labels and sends response 8 when the source Close action is clicked. The local diagnostic control disables authored-form capability without changing the recorded scene packets. 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 c3e2493a..6bf9191e 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 @@ -19,7 +19,7 @@ Prerequisites: ## Validation and remaining limits -Regression coverage includes `MixinIrisJavaAlphaTest`, `BedrockAcceptedResourcesTest`, `BedrockActorDepthGpuTest`, and `BedrockActorGlintFramesTest`. +Regression coverage includes `BedrockAcceptedResourcesTest`, `BedrockActorDepthGpuTest`, `BedrockActorGlintFramesTest`, `BedrockShaderAlphaTest`, and `BedrockPlayerFadeGpuTest`. Official Bedrock 1.26.60.5 (protocol 2225) GPU draws establish the player fade across zero to one block of signed view depth. The native near-player draws remain two-sided. A matching Java replay initially obscures the lobby with an opaque player head. diff --git a/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/NativeFormAudit.java b/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/NativeFormAudit.java index f2b44ce0..5add2c15 100644 --- a/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/NativeFormAudit.java +++ b/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/NativeFormAudit.java @@ -150,17 +150,20 @@ private static void walk(Object box, String path, boolean inheritedVisible, int Object properties = call(node, "properties"); Object input = call(properties, "get", new Class[] {String.class}, "input"); if (input != null && input.toString().length() <= 4096) { - JsonObject values = JsonParser.parseString(input.toString()).getAsJsonObject(); - Map metadata = new LinkedHashMap<>(); - for (String key : List.of("index", "option")) { - if (values.has(key) && values.get(key).isJsonPrimitive() && values.get(key).getAsJsonPrimitive().isNumber()) - metadata.put(key, values.get(key).getAsInt()); + var parsed = JsonParser.parseString(input.toString()); + if (parsed.isJsonObject()) { + JsonObject values = parsed.getAsJsonObject(); + Map metadata = new LinkedHashMap<>(); + for (String key : List.of("index", "option")) { + if (values.has(key) && values.get(key).isJsonPrimitive() && values.get(key).getAsJsonPrimitive().isNumber()) + metadata.put(key, values.get(key).getAsInt()); + } + if (values.has("kind") && values.get("kind").isJsonPrimitive()) { + String kind = values.get("kind").getAsString(); + if (List.of("toggle", "slider", "step_slider", "input", "dropdown", "multiselect", "submit").contains(kind)) metadata.put("kind", kind); + } + row.put("input", metadata); } - if (values.has("kind") && values.get("kind").isJsonPrimitive()) { - String kind = values.get("kind").getAsString(); - if (List.of("toggle", "slider", "step_slider", "input", "dropdown", "multiselect", "submit").contains(kind)) metadata.put("kind", kind); - } - row.put("input", metadata); } target.add(row); } diff --git a/test/fixtures/NativeFormAuditSelfTest.java b/test/fixtures/NativeFormAuditSelfTest.java index 7bc2f0d7..96d94415 100644 --- a/test/fixtures/NativeFormAuditSelfTest.java +++ b/test/fixtures/NativeFormAuditSelfTest.java @@ -1,6 +1,7 @@ package net.pistonmaster.stackanvil.replay.fabric; import com.viaversion.viaversion.libs.gson.JsonObject; +import com.viaversion.viaversion.libs.gson.JsonParser; import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiSceneScreen; import com.viaversion.viafabricplus.bedrock.screen.form.BedrockFormController; import com.viaversion.viafabricplus.bedrock.screen.form.BedrockFallbackFormScreen; @@ -36,6 +37,18 @@ public static void main(String[] arguments) throws Exception { require(!report.toString().contains("private")); require(NativeFormAudit.lineBreaks("\\n\r\n\r\n") == 2); + for (var malformed : List.of("null", "[]", "false", "\"private\"")) { + JsonObject invalid = new JsonObject(); + invalid.add("input", JsonParser.parseString(malformed)); + Box mixed = new Box(new Node("button", invalid, true, 9), bounds, clip, clip, List.of(child)); + Map observed = NativeFormAudit.project(mixed); + List inputs = (List) observed.get("rows"); + require(inputs.size() == 2); + require(!((Map) inputs.getFirst()).containsKey("input")); + require(((Map) inputs.get(1)).get("input").equals(Map.of("kind", "slider", "index", 4))); + require(!observed.toString().contains("private")); + } + Map authored = NativeFormAudit.observe(new NativeUiSceneScreen(new BedrockFormController(37), root)); require(authored.get("nativeForm").equals(true)); require(authored.get("presentation").equals("authored-form"));