diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 0329421c..b91bf89f 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -23,14 +23,14 @@ No complete area has verified Bedrock parity. | Area | Implemented behavior | Remaining limits | | --- | --- | --- | | Protocol 2225 | Updated packet layouts, explicit enum values, stonecutter notifications, cursor drags, skin identity, and sound unions. | Block seating, signed content playback, matchmaking UI, and environment layers remain partial or unsupported. | -| World resources | World base-game version selects vanilla metadata, textures, actor graphs, and audio before server overrides. The context survives ViaProxy. | Native current/older/current cat and sound controls cover selected paths. Broader pose, audio, and legacy fog behavior need comparisons. | +| World resources | World base-game version selects vanilla metadata, textures, actor graphs, and audio before server overrides. The context survives ViaProxy. Server legacy biome bindings retain unqualified names across versions. | Native current/older/current cat, sound, and remote biome-name controls cover selected paths. Broader pose and audio behavior need comparisons. | | Held trapdoors | Retained facing and half, interaction suppression, use timing, cancellation, rejection, and prediction reconciliation. | Native, direct, and proxy controls cover specific held-placement histories. Other placement and interaction families need separate comparisons. | -| Fonts and UI | Bitmap metadata, extended mapping, same-pack ownership, legacy layouts, authored scenes, and native GUI sampling. | Current font controls match 15/16 labels at native UI scale 1 and 16/16 at scale 2. Legacy scale-2 controls match 16/16. One scale-1 pixel, broader viewports, and TrueType comparisons remain open. | +| Fonts and UI | Bitmap metadata, extended mapping, same-pack ownership, legacy layouts, authored scenes, and native GUI sampling. | At 1280 × 720, current controls match all 16 labels at native UI scales 1 and 2; legacy scale-2 controls match 16/16. Both routes pass Java GUI scale changes and reloads. Broader viewports and TrueType comparisons remain open. | | Player inventory and crafting | Installed-screen lifetimes, request ownership, recipe catalogs, native stack identities, and admitted authored presentation. | Complete E-key presentation, controller publication, recipe admission, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | | Cooking | Snapshot-bound recipe actions, transfer batches, preferences, search, and progress. | Default cooking screens retain ordinary controls until authored bindings are complete. See the [cooking reference](native-cooking-actions.md). | -| Skins and appearance | Complete skin transport, classic changes, persona assembly, animation graphs, costumes, and attachables. | Armor masking, profile/LOD changes, shield rendering, remote appearances, and account/platform flows need further native comparisons. See [skin support](skin-flow.md). | -| Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. | Water-edge falls, combined vision effects, mount variants, network conditions, and broader replay behavior remain open. | -| Fog and picking | Bounded fog-command handling, End dragon distance rules, and native actor picking transport. | Legacy biome fog, combined effects, render modes, block/entity occlusion, and native hit results need focused comparisons. | +| Skins and appearance | Complete skin transport, classic changes, persona assembly, animation graphs, costumes, and attachables. | Broader wardrobe masking, profile/LOD changes, shield rendering, remote appearances, and account/platform flows need further native comparisons. See [skin support](skin-flow.md). | +| Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. Overworld air blindness attenuates night-vision strength and fog color together. | Native Fancy controls establish the tick-based expiry blend and distance curve at noon and midnight. Water-edge falls, other vision combinations and media, mount variants, network conditions, and broader replay behavior remain open. | +| Fog and picking | Fog commands, inherited legacy biome bindings, pack-defined defaults, Overworld clock modulation, End dragon distance rules, and native actor picking transport. Disabling the daylight rule freezes explicit Overworld clocks. | Direct and ViaProxy match native Fancy controls for selected resources and fixed colors. Spatial blending, timed command transitions, directional sunrise tint, sky sampling, other render modes, and broader picking remain open. | | Custom content | Resource-defined actors, materials, particles, sound, block geometry, lighting, and collision. | Unsupported material methods, missing assets, carrier capacity, and visual differences remain. Live-server admission does not establish parity. | | Platforms | Linux build, replay, and selected native comparisons. | Windows and macOS launcher/account/rendering paths need their own results. See [platform testing](platform-testing.md). | diff --git a/docs/skin-flow.md b/docs/skin-flow.md index d38a001f..94ca2f04 100644 --- a/docs/skin-flow.md +++ b/docs/skin-flow.md @@ -54,7 +54,7 @@ The owning patch notes describe their specific validation and limits: ## Remaining limits -Native armor masking and profile/LOD texture changes still need matching fixtures. +Other wardrobe categories, rapid profile changes, and distinct LOD texture selection still need matching fixtures. Enchanted shields, complete first-person appearance, live mounts, remote skins, and motion timing remain partial or unverified. Platform purchases, complete account synchronization, and broader emote parity remain open. Keep current status in the [compatibility page](bedrock-coverage.md), not a chronological skin journal. diff --git a/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.patch b/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.patch index 3f62645d..31380ecb 100644 --- a/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.patch +++ b/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.patch @@ -1,4 +1,4 @@ -From 5267d12c920059ac239972f5dd9967877bcda487 Mon Sep 17 00:00:00 2001 +From 9c60fdf9655d557f771d1f73f81f199186ec4c73 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] fix(world): preserve native Bedrock world clock semantics @@ -19,8 +19,11 @@ changes, resend its current full tick count and paused or running rate. Resuming after a long pause must continue from the paused value rather than resetting to the last server time or counting ticks spent paused. A new legacy SetTime replaces the snapshot. Explicit Overworld SyncWorldClocks -updates clear legacy tracking and retain their own rates; updates to other -clocks remain independent. +updates replace legacy tracking while retaining the wire pause state. +Project the daylight-cycle rule as an additional Overworld pause constraint. +Re-anchor effective time when the rule changes, without counting paused +ticks or resuming an explicitly paused clock. Other named clocks remain +independent. Numerical tests cover long pauses, resume without a jump, new server times, independent explicit clocks, and full-count packet serialization. @@ -35,17 +38,25 @@ clock state sync before StartGame and its initialization registry afterward. Clock handling must therefore remain available during world configuration, including a supplemental custom-block pack load. Numerical tests cover latest updates, elapsed ticks, paused clocks and legacy fallback selection. + +Native 1.26.60.5 Fancy and BDS 1.26.60.6 confirm the explicit-clock rule. +With doDayLightCycle=false, BDS repeatedly sends time 13000 with IsPaused +false. Native remote fog remains at the fixed twilight color, while the +old Java client advances between syncs and drifts by 225 ticks. Apply the +rule at initial clock publication as well as later updates and rule changes. +Focused packet-to-state tests cover this sequence, resume anchoring, wire +pause preservation, and unrelated clock progress. Raw captures stay private. --- .../viabedrock/protocol/BedrockProtocol.java | 2 + .../protocol/packet/JoinPackets.java | 16 +- - .../protocol/packet/WorldPackets.java | 114 ++++++++++++- + .../protocol/packet/WorldPackets.java | 120 +++++++++- .../protocol/storage/GameRulesStorage.java | 3 + .../protocol/storage/JoinGameStorage.java | 2 +- - .../protocol/storage/WorldClockTracker.java | 129 ++++++++++++++ - .../packet/InitialClockStateTest.java | 65 +++++++ - .../protocol/packet/WorldPacketsTest.java | 103 ++++++++++++ - .../storage/WorldClockTrackerTest.java | 158 ++++++++++++++++++ - 9 files changed, 580 insertions(+), 12 deletions(-) + .../protocol/storage/WorldClockTracker.java | 152 +++++++++++++ + .../packet/InitialClockStateTest.java | 74 +++++++ + .../protocol/packet/WorldPacketsTest.java | 103 +++++++++ + .../storage/WorldClockTrackerTest.java | 209 ++++++++++++++++++ + 9 files changed, 669 insertions(+), 12 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/storage/WorldClockTracker.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/InitialClockStateTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/WorldPacketsTest.java @@ -113,7 +124,7 @@ index 866ec9e2b..da0f21713 100644 static DimensionDefinition readDimensionDefinition(final PacketWrapper wrapper) { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/WorldPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/WorldPackets.java -index bc6037b80..36de635c5 100644 +index bc6037b80..3b818fe4a 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/WorldPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/WorldPackets.java @@ -19,14 +19,17 @@ package net.raphimc.viabedrock.protocol.packet; @@ -180,7 +191,7 @@ index bc6037b80..36de635c5 100644 protocol.registerServerbound(ServerboundPackets26_3.SIGN_UPDATE, ServerboundBedrockPackets.BLOCK_ENTITY_DATA, wrapper -> { final ChunkTracker chunkTracker = wrapper.user().get(ChunkTracker.class); -@@ -574,6 +596,83 @@ public final class WorldPackets { +@@ -574,6 +596,89 @@ public final class WorldPackets { }); } @@ -199,6 +210,7 @@ index bc6037b80..36de635c5 100644 + static List initialClockUpdates(final WorldClockTracker tracker, final CompoundTag registry, + final int clientTick, final long gameTime, final long legacyTime, final boolean legacyRunning) { + final List updates = new ArrayList<>(); ++ tracker.updateDayCycle(gameTime, clientTick, legacyRunning); + for (WorldClockTracker.ClockUpdate update : tracker.currentClocks(clientTick)) { + final int id = javaWorldClockId(registry, update.name()); + if (id >= 0) { @@ -219,11 +231,16 @@ index bc6037b80..36de635c5 100644 + return player != null ? player.age() : 0; + } + -+ public static void updateLegacyDayCycle(final UserConnection user, final boolean running) { ++ public static void updateDayCycle(final UserConnection user, final boolean running) { + final long gameTime = currentJavaGameTime(user); -+ user.get(WorldClockTracker.class).updateLegacyDayCycle(gameTime, running).ifPresent(update -> { ++ user.get(WorldClockTracker.class).updateDayCycle(gameTime, currentClientTick(user), running).ifPresent(update -> { ++ final CompoundTag registry = user.get(GameSessionStorage.class).getJavaRegistries().getCompoundTag(RegistryKeys.WORLD_CLOCK); ++ final int id = javaWorldClockId(registry, update.name()); ++ if (id < 0) { ++ return; ++ } + final PacketWrapper setTime = PacketWrapper.create(ClientboundPackets26_3.SET_TIME, user); -+ writeJavaTime(setTime, gameTime, List.of(new JavaClockUpdate(0, update.time(), !update.paused()))); ++ writeJavaTime(setTime, gameTime, List.of(new JavaClockUpdate(id, update.time(), !update.paused()))); + setTime.send(BedrockProtocol.class); + }); + } @@ -264,7 +281,7 @@ index bc6037b80..36de635c5 100644 static long packSectionPosition(final BlockPosition section) { return (section.x() & 0x3FFFFFL) << 42 | (section.z() & 0x3FFFFFL) << 20 | (section.y() & 0xFFFFFL); } -@@ -581,4 +680,7 @@ public final class WorldPackets { +@@ -581,4 +686,7 @@ public final class WorldPackets { private WorldPackets() { } @@ -273,7 +290,7 @@ index bc6037b80..36de635c5 100644 + } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/GameRulesStorage.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/GameRulesStorage.java -index 9dcfed5b7..8f8bd5edc 100644 +index 9dcfed5b7..e1c58934f 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/GameRulesStorage.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/GameRulesStorage.java @@ -27,6 +27,7 @@ import net.raphimc.viabedrock.protocol.BedrockProtocol; @@ -289,7 +306,7 @@ index 9dcfed5b7..8f8bd5edc 100644 } else if (gameRule.name().equalsIgnoreCase("doLimitedCrafting")) { PacketFactory.sendJavaGameEvent(this.user(), GameEventType.LIMITED_CRAFTING, (boolean) gameRule.value() ? 1F : 0F); + } else if (gameRule.name().equalsIgnoreCase("doDayLightCycle")) { -+ WorldPackets.updateLegacyDayCycle(this.user(), (boolean) gameRule.value()); ++ WorldPackets.updateDayCycle(this.user(), (boolean) gameRule.value()); } } } @@ -306,10 +323,10 @@ index 85bfa5862..cdad57cfa 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/WorldClockTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/WorldClockTracker.java new file mode 100644 -index 000000000..2614181c8 +index 000000000..70d00ae8c --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/WorldClockTracker.java -@@ -0,0 +1,129 @@ +@@ -0,0 +1,152 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -344,20 +361,39 @@ index 000000000..2614181c8 + private final Map clockNames = new HashMap<>(); + private final Map clocks = new LinkedHashMap<>(); + private LegacyClock legacyOverworld; ++ private boolean daylightCycle = true; + + public void setLegacyTime(final long gameTime, final long time, final boolean running) { -+ this.clocks.values().removeIf(clock -> Key.namespaced(clock.update().name()).equals("minecraft:overworld")); ++ this.clocks.values().removeIf(clock -> Key.namespaced(clock.wireUpdate().name()).equals("minecraft:overworld")); + this.legacyOverworld = new LegacyClock(gameTime, time, running); ++ this.daylightCycle = running; + } + -+ public Optional updateLegacyDayCycle(final long gameTime, final boolean running) { -+ if (this.legacyOverworld == null || this.legacyOverworld.running() == running) { ++ public Optional updateDayCycle(final long gameTime, final int clientTick, final boolean running) { ++ if (this.legacyOverworld != null) { ++ this.daylightCycle = running; ++ if (this.legacyOverworld.running() == running) { ++ return Optional.empty(); ++ } ++ final long time = this.legacyOverworld.time() + (this.legacyOverworld.running() ++ ? Math.max(0L, gameTime - this.legacyOverworld.gameTime()) : 0L); ++ this.setLegacyTime(gameTime, time, running); ++ return Optional.of(new ClockUpdate("minecraft:overworld", time, !running)); ++ } ++ if (this.daylightCycle == running) { + return Optional.empty(); + } -+ final long time = this.legacyOverworld.time() + (this.legacyOverworld.running() -+ ? Math.max(0L, gameTime - this.legacyOverworld.gameTime()) : 0L); -+ this.setLegacyTime(gameTime, time, running); -+ return Optional.of(new ClockUpdate("minecraft:overworld", time, !running)); ++ ClockUpdate changed = null; ++ for (Map.Entry entry : this.clocks.entrySet()) { ++ final ClockSample sample = entry.getValue(); ++ if (Key.namespaced(sample.wireUpdate().name()).equals("minecraft:overworld")) { ++ final ClockUpdate current = this.project(sample, clientTick); ++ changed = new ClockUpdate(sample.wireUpdate().name(), current.time(), sample.wireUpdate().paused()); ++ entry.setValue(new ClockSample(changed, clientTick)); ++ } ++ } ++ this.daylightCycle = running; ++ return changed != null ? Optional.of(this.resolve(changed, 0L)) : Optional.empty(); + } + + public List readUpdates(final PacketWrapper wrapper) throws InformativeException { @@ -379,7 +415,7 @@ index 000000000..2614181c8 + final ClockUpdate update = new ClockUpdate(name, time, paused); + this.clockNames.put(id, name); + this.clocks.put(id, new ClockSample(update, clientTick)); -+ updates.add(update); ++ updates.add(this.resolve(update, 0L)); + + final int markerCount = readCount(wrapper); + for (int j = 0; j < markerCount; j++) { @@ -401,7 +437,7 @@ index 000000000..2614181c8 + if (name != null) { + final ClockUpdate update = new ClockUpdate(name, time, paused); + this.clocks.put(id, new ClockSample(update, clientTick)); -+ updates.add(update); ++ updates.add(this.resolve(update, 0L)); + } + } + } @@ -412,13 +448,17 @@ index 000000000..2614181c8 + } + + public List currentClocks(final int clientTick) { -+ final List updates = new ArrayList<>(this.clocks.size()); -+ for (ClockSample sample : this.clocks.values()) { -+ final ClockUpdate update = sample.update(); -+ final long elapsed = update.paused() ? 0L : Math.max(0L, (long) clientTick - sample.clientTick()); -+ updates.add(new ClockUpdate(update.name(), update.time() + elapsed, update.paused())); -+ } -+ return List.copyOf(updates); ++ return this.clocks.values().stream().map(sample -> this.project(sample, clientTick)).toList(); ++ } ++ ++ private ClockUpdate project(final ClockSample sample, final int clientTick) { ++ return this.resolve(sample.wireUpdate(), Math.max(0L, (long) clientTick - sample.clientTick())); ++ } ++ ++ private ClockUpdate resolve(final ClockUpdate update, final long elapsed) { ++ // Native Overworld rendering also honors the game rule when the explicit wire clock is unpaused. ++ final boolean paused = update.paused() || (!this.daylightCycle && Key.namespaced(update.name()).equals("minecraft:overworld")); ++ return new ClockUpdate(update.name(), update.time() + (paused ? 0L : elapsed), paused); + } + + private static int readCount(final PacketWrapper wrapper) throws InformativeException { @@ -432,7 +472,7 @@ index 000000000..2614181c8 + public record ClockUpdate(String name, long time, boolean paused) { + } + -+ private record ClockSample(ClockUpdate update, int clientTick) { ++ private record ClockSample(ClockUpdate wireUpdate, int clientTick) { + } + + private record LegacyClock(long gameTime, long time, boolean running) { @@ -441,10 +481,10 @@ index 000000000..2614181c8 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/InitialClockStateTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/InitialClockStateTest.java new file mode 100644 -index 000000000..4306d9ee4 +index 000000000..7e7620224 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/InitialClockStateTest.java -@@ -0,0 +1,65 @@ +@@ -0,0 +1,74 @@ +package net.raphimc.viabedrock.protocol.packet; + +import com.viaversion.nbt.tag.CompoundTag; @@ -463,12 +503,21 @@ index 000000000..4306d9ee4 +final class InitialClockStateTest { + + @Test ++ void initialExplicitOverworldHonorsTheRuleReceivedAfterItsRegistry() throws Exception { ++ final var tracker = new WorldClockTracker(); ++ initialize(tracker, "minecraft:overworld", 13_000, false); ++ assertEquals(List.of(new WorldPackets.JavaClockUpdate(0, 13_000L, false)), ++ WorldPackets.initialClockUpdates(tracker, registry(), 0, 5_000L, 6_000L, false)); ++ assertEquals(List.of(new WorldClockTracker.ClockUpdate("minecraft:overworld", 13_000L, true)), tracker.currentClocks(500)); ++ } ++ ++ @Test + void explicitClockSurvivesJoinAndGameruleChangesWithoutLegacyOverwrite() throws Exception { + final var tracker = new WorldClockTracker(); + initialize(tracker, "overworld", 72_045, true); + assertEquals(List.of(new WorldPackets.JavaClockUpdate(0, 72_045L, false)), + WorldPackets.initialClockUpdates(tracker, registry(), 60, 5_000L, 6_000L, true)); -+ assertTrue(tracker.updateLegacyDayCycle(5_020L, true).isEmpty()); ++ assertTrue(tracker.updateDayCycle(5_020L, 0, true).isEmpty()); + assertEquals(72_045L, tracker.currentClocks(600).get(0).time()); + } + @@ -479,7 +528,7 @@ index 000000000..4306d9ee4 + assertEquals(List.of(new WorldPackets.JavaClockUpdate(0, 6_000L, false), + new WorldPackets.JavaClockUpdate(1, 50_040L, true)), + WorldPackets.initialClockUpdates(tracker, registry(), 40, 5_000L, 6_000L, false)); -+ assertEquals(6_000L, tracker.updateLegacyDayCycle(7_000L, true).orElseThrow().time()); ++ assertEquals(6_000L, tracker.updateDayCycle(7_000L, 0, true).orElseThrow().time()); + initialize(tracker, "minecraft:unmapped_clock", 99_000, true); + assertEquals(List.of(new WorldPackets.JavaClockUpdate(0, 7_000L, true)), + WorldPackets.initialClockUpdates(tracker, registry(), 0, 7_000L, 7_000L, true)); @@ -621,10 +670,10 @@ index 000000000..acb10056f +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/WorldClockTrackerTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/WorldClockTrackerTest.java new file mode 100644 -index 000000000..758654ffa +index 000000000..bc219977c --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/WorldClockTrackerTest.java -@@ -0,0 +1,158 @@ +@@ -0,0 +1,209 @@ +package net.raphimc.viabedrock.protocol.storage; + +import com.viaversion.viaversion.protocol.packet.PacketWrapperImpl; @@ -643,6 +692,54 @@ index 000000000..758654ffa +class WorldClockTrackerTest { + + @Test ++ void daylightRuleFreezesExplicitOverworldAcrossSyncsAndResumesWithoutPausedElapsedTicks() throws Exception { ++ final var tracker = new WorldClockTracker(); ++ final ByteBuf payload = Unpooled.buffer(); ++ try { ++ BedrockTypes.UNSIGNED_VAR_INT.write(payload, 1); ++ BedrockTypes.UNSIGNED_VAR_INT.write(payload, 2); ++ for (int i = 0; i < 2; i++) { ++ BedrockTypes.UNSIGNED_VAR_LONG.write(payload, 42L + i); ++ BedrockTypes.STRING.write(payload, i == 0 ? "minecraft:overworld" : "test:season"); ++ BedrockTypes.VAR_INT.write(payload, i == 0 ? 13_000 : 6_000); ++ Types.BOOLEAN.write(payload, false); ++ BedrockTypes.UNSIGNED_VAR_INT.write(payload, 0); ++ } ++ tracker.readUpdates(new PacketWrapperImpl(ClientboundBedrockPackets.SYNC_WORLD_CLOCKS, payload, null), 100); ++ assertEquals(new WorldClockTracker.ClockUpdate("minecraft:overworld", 13_000L, true), ++ tracker.updateDayCycle(0L, 100, false).orElseThrow()); ++ assertEquals(List.of(new WorldClockTracker.ClockUpdate("minecraft:overworld", 13_000L, true), ++ new WorldClockTracker.ClockUpdate("test:season", 6_400L, false)), tracker.currentClocks(500)); ++ ++ payload.clear(); ++ BedrockTypes.UNSIGNED_VAR_INT.write(payload, 0); ++ BedrockTypes.UNSIGNED_VAR_INT.write(payload, 1); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(payload, 42L); ++ BedrockTypes.VAR_INT.write(payload, 13_000); ++ Types.BOOLEAN.write(payload, false); ++ assertEquals(List.of(new WorldClockTracker.ClockUpdate("minecraft:overworld", 13_000L, true)), ++ tracker.readUpdates(new PacketWrapperImpl(ClientboundBedrockPackets.SYNC_WORLD_CLOCKS, payload, null), 500)); ++ assertEquals(new WorldClockTracker.ClockUpdate("minecraft:overworld", 13_000L, false), ++ tracker.updateDayCycle(0L, 700, true).orElseThrow()); ++ assertEquals(13_010L, tracker.currentClocks(710).getFirst().time()); ++ ++ payload.clear(); ++ BedrockTypes.UNSIGNED_VAR_INT.write(payload, 0); ++ BedrockTypes.UNSIGNED_VAR_INT.write(payload, 1); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(payload, 42L); ++ BedrockTypes.VAR_INT.write(payload, 13_100); ++ Types.BOOLEAN.write(payload, true); ++ tracker.readUpdates(new PacketWrapperImpl(ClientboundBedrockPackets.SYNC_WORLD_CLOCKS, payload, null), 710); ++ tracker.updateDayCycle(0L, 720, false); ++ assertEquals(new WorldClockTracker.ClockUpdate("minecraft:overworld", 13_100L, true), ++ tracker.updateDayCycle(0L, 730, true).orElseThrow()); ++ assertEquals(13_100L, tracker.currentClocks(740).getFirst().time()); ++ } finally { ++ payload.release(); ++ } ++ } ++ ++ @Test + void retainsLatestExplicitStateAcrossJoinAndAdvancesOnlyRunningClocks() throws Exception { + final WorldClockTracker tracker = new WorldClockTracker(); + final ByteBuf initialization = Unpooled.buffer(); @@ -675,9 +772,10 @@ index 000000000..758654ffa + BedrockTypes.UNSIGNED_VAR_LONG.write(sync, 42L); + BedrockTypes.VAR_INT.write(sync, 96_000); + Types.BOOLEAN.write(sync, false); -+ assertEquals(List.of(new WorldClockTracker.ClockUpdate("minecraft:overworld", 96_000L, false)), ++ assertEquals(List.of(new WorldClockTracker.ClockUpdate("minecraft:overworld", 96_000L, true)), + tracker.readUpdates(new PacketWrapperImpl(ClientboundBedrockPackets.SYNC_WORLD_CLOCKS, sync, null), 7_000)); -+ assertTrue(tracker.updateLegacyDayCycle(201L, true).isEmpty()); ++ assertEquals(new WorldClockTracker.ClockUpdate("minecraft:overworld", 96_000L, false), ++ tracker.updateDayCycle(201L, 7_000, true).orElseThrow()); + assertTrue(tracker.currentClocks(7_010).contains(new WorldClockTracker.ClockUpdate("minecraft:overworld", 96_010L, false))); + } finally { + initialization.release(); @@ -690,19 +788,19 @@ index 000000000..758654ffa + final WorldClockTracker tracker = new WorldClockTracker(); + tracker.setLegacyTime(100L, 48_001L, true); + assertEquals(new WorldClockTracker.ClockUpdate("minecraft:overworld", 48_021L, true), -+ tracker.updateLegacyDayCycle(120L, false).orElseThrow()); -+ assertTrue(tracker.updateLegacyDayCycle(10_000L, false).isEmpty()); ++ tracker.updateDayCycle(120L, 0, false).orElseThrow()); ++ assertTrue(tracker.updateDayCycle(10_000L, 0, false).isEmpty()); + assertEquals(new WorldClockTracker.ClockUpdate("minecraft:overworld", 48_021L, false), -+ tracker.updateLegacyDayCycle(20_000L, true).orElseThrow()); ++ tracker.updateDayCycle(20_000L, 0, true).orElseThrow()); + assertEquals(new WorldClockTracker.ClockUpdate("minecraft:overworld", 48_052L, true), -+ tracker.updateLegacyDayCycle(20_031L, false).orElseThrow()); ++ tracker.updateDayCycle(20_031L, 0, false).orElseThrow()); + } + + @Test + void appliesNewServerTimeWhilePausedAndKeepsUnrelatedExplicitClocksIndependent() throws Exception { + final WorldClockTracker tracker = new WorldClockTracker(); + tracker.setLegacyTime(100L, 12_445L, true); -+ assertEquals(12_445L, tracker.updateLegacyDayCycle(100L, false).orElseThrow().time()); ++ assertEquals(12_445L, tracker.updateDayCycle(100L, 0, false).orElseThrow().time()); + tracker.setLegacyTime(500L, -6_000L, false); + final ByteBuf initialization = Unpooled.buffer(); + try { @@ -715,8 +813,8 @@ index 000000000..758654ffa + BedrockTypes.UNSIGNED_VAR_INT.write(initialization, 0); + tracker.readUpdates(new PacketWrapperImpl(ClientboundBedrockPackets.SYNC_WORLD_CLOCKS, initialization, null)); + assertEquals(new WorldClockTracker.ClockUpdate("minecraft:overworld", -6_000L, false), -+ tracker.updateLegacyDayCycle(25_000L, true).orElseThrow()); -+ assertEquals(-5_960L, tracker.updateLegacyDayCycle(25_040L, false).orElseThrow().time()); ++ tracker.updateDayCycle(25_000L, 0, true).orElseThrow()); ++ assertEquals(-5_960L, tracker.updateDayCycle(25_040L, 0, false).orElseThrow().time()); + } finally { + initialization.release(); + } @@ -755,7 +853,8 @@ index 000000000..758654ffa + new WorldClockTracker.ClockUpdate("minecraft:the_end", 6_000, true)), + tracker.readUpdates(new PacketWrapperImpl(ClientboundBedrockPackets.SYNC_WORLD_CLOCKS, initialization, null))); + assertEquals(0, initialization.readableBytes()); -+ assertTrue(tracker.updateLegacyDayCycle(200L, false).isEmpty()); ++ assertEquals(new WorldClockTracker.ClockUpdate("minecraft:overworld", 18_000L, true), ++ tracker.updateDayCycle(200L, 0, false).orElseThrow()); + } finally { + initialization.release(); + } @@ -773,10 +872,11 @@ index 000000000..758654ffa + + assertEquals(List.of( + new WorldClockTracker.ClockUpdate("minecraft:the_end", 6_100, true), -+ new WorldClockTracker.ClockUpdate("minecraft:overworld", 1_000, false)), ++ new WorldClockTracker.ClockUpdate("minecraft:overworld", 1_000, true)), + tracker.readUpdates(new PacketWrapperImpl(ClientboundBedrockPackets.SYNC_WORLD_CLOCKS, sync, null))); + assertEquals(0, sync.readableBytes()); -+ assertTrue(tracker.updateLegacyDayCycle(300L, true).isEmpty()); ++ assertEquals(new WorldClockTracker.ClockUpdate("minecraft:overworld", 1_000L, false), ++ tracker.updateDayCycle(300L, 0, true).orElseThrow()); + } finally { + sync.release(); + } diff --git a/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.pr.md b/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.pr.md index b15bb11d..5906163c 100644 --- a/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.pr.md +++ b/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.pr.md @@ -1,40 +1,28 @@ -Java retained a running day clock when a later Bedrock gamerule update disabled daylight. This made fixed-time lobbies drift into night. +# Native world clock semantics -The saved official Windows Bedrock 1.26.51.1 session uses protocol 2193. It sends `SetTime` 12445, then `GameRulesChanged` with `dodaylightcycle=false`. It sends no `SyncWorldClocks` updates. Native loopback replay preserves the same sky colors after the complete scene arrives. +## Purpose -The translator now snapshots the legacy Overworld clock against game age. Rule changes resend the current full tick count and rate. Resuming preserves the paused time and excludes ticks spent paused. Explicit Overworld clock updates clear legacy tracking, preserving their independent rates. +Initialize full clock counts and preserve paused time across legacy packets, explicit clock snapshots, and daylight-rule changes. +Retain configuration snapshots through join and supplemental resource-pack loading. -The patch also initializes the day clock from level time when no stop time is present. Complete tick counts preserve day counts and moon phases. +## Design constraints -Targeted numerical tests cover pause, long waits, resume, new server times, independent explicit clocks, and packet serialization. All seven targeted tests pass. The full core suite passes with 279 tests and zero skips. `checkstyleMain` and `checkstyleTest` also pass. +Keep game age independent from the day clock. +A legacy update replaces the Overworld snapshot. An explicit update replaces legacy tracking. +For the Overworld, combine the wire pause flag with the daylight-cycle rule. +Keep the wire flag so a rule change cannot resume an explicitly paused clock. +Re-anchor current time on pause and resume. Exclude ticks spent paused. +Other named clocks retain their independent state. -## Configuration and explicit clocks +## Validation and remaining limits -The pinned Bedrock Dedicated Server 1.26.51.1 (build 51061372, protocol 2193) sends a clock state sync before StartGame. -Its registry initialization follows StartGame. -Core now accepts clock packets during configuration and retains the latest state for known IDs. -Java receives those snapshots when the world opens, with elapsed client ticks applied only to running clocks. -An explicit Overworld snapshot takes precedence over the legacy StartGame time. -A legacy update replaces that snapshot without removing the registry ID needed by later explicit sync packets. +Native 1.26.51.1 controls establish legacy pause behavior in fixed-time lobbies. +Native 1.26.60.5 and BDS 1.26.60.6 controls resolve the explicit-clock case: +BDS reports time 13000 with `IsPaused=false` and daylight cycling disabled, while native remote fog stays fixed. +The old Java clock advances between syncs and changes the twilight color. -The native registry capture consumes every byte with the existing optional marker-period decoder. -All six advertised markers have a present period of 24,000 ticks. -The [target packet schema](https://mojang.github.io/bedrock-protocol-docs/1.26.51/packets/sync-world-clocks-packet/) describes the period differently. -The capture establishes the wire layout used by this pinned build. -Production does not read captures or a local game installation. - -Three additional numerical tests cover retained state, running ticks, paused values, independent clocks, registry replacement, and legacy fallback. -They also cover explicit sync after a legacy update. -The complete replayed core suite and Checkstyle pass. - -The live server reports a running explicit Overworld clock even when `doDaylightCycle=false` keeps its queried daytime fixed. -Direct Java follows that explicit rate until the next server correction. -The native client's treatment of these conflicting signals remains unverified. -Explicit paused-clock integration and arbitrary custom-clock registry transport also need further native comparisons. - -Live direct and ViaProxy joins retain the registry during supplemental custom-block pack loading. -The private state timeline records configuration, a populated clock snapshot, buffered world packets, and the transition to play. -Java starts at the captured 72,046 ticks on both routes. -These observations establish clock retention and initial projection through the pack gate. -They do not establish paused-clock or complete visual parity. -The final build passes 413 core tests with one optional fixture skip and no failures or errors. +Packet-to-state regressions cover initialization, repeated syncs, rule changes, resume anchoring, wire pause, and unrelated clocks. +Focused tests and the complete four-project build pass. +Fresh direct and ViaProxy recordings hold the full clock count at dusk and resume without counting paused ticks. +Arbitrary custom-clock transport and broader native clock commands still need comparisons. +The [owning patch](0014-initialize-and-preserve-bedrock-clock-time.patch) retains protocol and implementation evidence. diff --git a/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.patch b/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.patch index 9fa20262..811d3ed0 100644 --- a/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.patch +++ b/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.patch @@ -1,88 +1,56 @@ -From b5049739146de51a6134f0d4610a62fd34682313 Mon Sep 17 00:00:00 2001 +From aa900e1a862b4ded5509220564c6e4e39bf4a21b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] feat(resource-packs): select native resources by world version -Bedrock 1.26.51.1 protocol 2193 accepts Hive item atlas aliases to vanilla images which the server does not resend. The bundled metadata archives intentionally contain no images, leaving these aliases unresolved during Java conversion. +Select cumulative vanilla metadata, licensed images, atlas aliases, actor +dependencies, and audio using the base-game version advertised by +RESOURCE_PACK_STACK. Server packs retain independent priority. Acquire +licensed resources on the existing worker after that version is known. +Keep their provenance outside server advertisements, carry the version +through native archive format 2, and include it in conversion cache +identity even when the selected files match. -Expose a platform provider for licensed image-only resource layers. Acquire them on the existing pack executor and wait alongside the server download without blocking its Netty event loop. Preserve native overlay precedence below bundled metadata and server packs, include their exact pixels in conversion cache fingerprints, and keep them outside server advertisements and pack identity tracking. +Licensed providers and the optional verified classpath bundle supply +resources that servers reference without resending. Validate protocol, +file hashes, bounds, and overlay order. Preserve path case and restrict +accepted documents to the native rendering dependencies. Missing optional +resources preserve existing fallbacks; malformed present bundles fail. -Native equipment can refer to built-in geometry, animations, controllers -and material parents. Permit these actor dependency documents from the -licensed provider while keeping them below server packs. Do not publish -unrelated block or entity definitions. Permit only the exact item and -terrain atlas paths needed for native icon selection. The synthetic shield -regression covers dependencies, server precedence and provenance. +Canonicalize biome identifiers while preserving custom namespaces and +whole-definition priority. Load legacy biomes_client.json fog bindings +separately from modern biome metadata. Honor per-biome inheritance and +data-default clearing without deleting modern definitions or fog assets. +Skip built-in legacy bindings for base versions 1.21.40 and newer, while +continuing to load server legacy bindings, as the Creator reference states: +https://learn.microsoft.com/en-us/minecraft/creator/reference/content/clientbiomesreference/examples/clientbiomesoverview -Permit the exact native textures/item_texture.json and textures/terrain_texture.json atlases alongside licensed images. Its definitions are necessary to select built-in item sprites without server overrides. +Bedrock 1.26.60.5, protocol 2225, native Fancy controls establish lower air +inheritance beneath water-only bindings, replacement falling to the data +default, and legacy precedence over a higher modern biome definition. +Clearing legacy bindings exposes the modern definition again. Preserve +unqualified server legacy biome keys in current and older world contexts. +Native 1.26.60.5 Fancy remote current/1.20.0/current controls admit the same +blue 48-block settings from those keys. Fog identifiers remain qualified +in these controls. Keep spatial blending and timed fog transitions separate +from identifier admission; these comparisons do not establish their parity. -Load an optional verified native-image bundle from the core classpath when -a platform does not supply its own image library. ViaProxy otherwise -converts accepted attachables without their inherited native inventory -images and falls back to held geometry in GUI contexts. The same authored -BDS pack shows blank proxy icons while the direct route has an image -provider. Preserve server priority, atlas aliases, path casing and numeric -vanilla overlay order rather than changing item identity or held models. - -The build derives only images and the two native atlases from the existing -checksum-verified Bedrock 1.26.51.1 source archives. Validate the packaged -manifest, target protocol, per-file hashes, bounds, and layer order before -constructing lower-priority packs. A missing optional bundle preserves the -existing empty provider; a malformed present bundle fails explicitly. -The source images and generated bundle stay outside Git. - -Regression coverage includes default item icon conversion with retained -held selectors, server image and atlas overrides, original bytes and path -case, numeric/base overlay order, invalid manifests and immutable lists. -The StackAnvil build verifies the exact bundle in core and both consumers. -This supplies the same native image fallback on proxy and direct routes; -it does not establish complete native visuals or other actor semantics. - -Select cumulative vanilla metadata and licensed resource layers using the -base-game version advertised by RESOURCE_PACK_STACK. Preserve server pack -priority independently of that vanilla cutoff. Delay licensed acquisition -until preparation on the existing worker after STACK supplies the version. - -Carry the immutable version through native resource archive format2 for -direct and ViaProxy clients, and include it in conversion cache identity -even when selected file bytes happen to match. Reject malformed archive -version data rather than silently selecting current resources. - -Official BDS1.26.60.6, protocol2225, advertises * for an ordinary world and -1.20.0 for an isolated template-marked world with that baseGameVersion. -The executable SHA-256 is -ea16b541a448e07c337848d2c5f37321b706554b5216dd942f3f11e7a8ed2bad. -Microsoft's base-game version guide specifies cumulative vanilla overlay -selection and leaves server resource/skin packs independent of it: -https://learn.microsoft.com/en-us/minecraft/creator/documents/basegameversioning - -Regressions cover numeric cutoffs, newer/older/newer resource selection, -server priority, immutable raw layers, versioned archive/storage transfer, -and cache separation with identical resource bytes. BDS packet evidence -does not establish native client graphical or complete audio parity. - -Verify the cutoff metadata against all55 versioned vanilla manifests in -the matching1.26.60.5 client and generated core metadata: every layer's -minimum engine version matches its numeric overlay version. Archive -validation tests derive count and provenance offsets after the variable -version field, retaining malformed-role and truncated-header coverage. - -Canonicalize native biome identifiers on insertion and lookup. Older -vanilla layers use names such as the_end; 1.21.120 and newer descriptions -qualify the same identifier as minecraft:the_end. Keeping two keys lets -registry lookup retain stale metadata or ignore a qualified server -override even after the correct world-version layers are selected. -Preserve independent custom namespaces and whole-definition pack priority. -Two regressions cover both override directions, lookup aliases, sky/water -colors, fog binding and custom namespace isolation. +BDS 1.26.60.6 advertises * for ordinary worlds and 1.20.0 for an isolated +template. Matching 1.26.60.5 client manifests confirm all 55 numeric +vanilla cutoffs. Tests cover version transitions, server priority, archive +ownership, cache identity, and legacy fog composition. Native captures +and licensed resources remain private; these controls do not establish +complete rendering or audio parity. --- build.gradle | 11 ++ .../api/resourcepack/BaseGameVersion.java | 94 ++++++++++ .../api/resourcepack/NativeActorLibrary.java | 25 ++- - .../definition/BiomeDefinitions.java | 4 +- + .../definition/BiomeDefinitions.java | 61 +++++- + .../definition/FogDefinitions.java | 14 ++ .../http/ConvertedResourcePackCache.java | 1 + .../viabedrock/protocol/BedrockProtocol.java | 2 + + .../protocol/data/JavaRegistries.java | 2 +- .../protocol/packet/ResourcePackPackets.java | 15 +- .../provider/BuiltinResourcePackProvider.java | 27 +++ .../protocol/provider/BundledImagePacks.java | 176 ++++++++++++++++++ @@ -93,12 +61,12 @@ colors, fog binding and custom namespace isolation. .../BuiltinActorResourcesTest.java | 51 +++++ .../resourcepack/NativeActorLibraryTest.java | 45 +++-- .../resourcepack/SoundTranslationsTest.java | 6 +- - .../definition/BiomeDefinitionsTest.java | 78 ++++++++ + .../definition/BiomeDefinitionsTest.java | 157 ++++++++++++++++ .../http/ConvertedResourcePackCacheTest.java | 22 +++ .../http/WorldVersionCacheTest.java | 27 +++ .../provider/BundledImagePacksTest.java | 163 ++++++++++++++++ .../resourcepack/ModelResourceAssetsTest.java | 84 ++++++++- - 21 files changed, 944 insertions(+), 51 deletions(-) + 23 files changed, 1095 insertions(+), 52 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/BaseGameVersion.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/provider/BuiltinResourcePackProvider.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/provider/BundledImagePacks.java @@ -305,10 +273,43 @@ index 7448e5169..c91f7513e 100644 public Layer { resources = Map.copyOf(resources); diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/BiomeDefinitions.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/BiomeDefinitions.java -index 004b5be32..7c049c9b6 100644 +index 004b5be32..01876acf2 100644 --- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/BiomeDefinitions.java +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/BiomeDefinitions.java -@@ -37,7 +37,7 @@ public class BiomeDefinitions { +@@ -17,27 +17,40 @@ + */ + package net.raphimc.viabedrock.api.resourcepack.definition; + ++import com.viaversion.viaversion.libs.gson.JsonElement; + import com.viaversion.viaversion.libs.gson.JsonObject; + import com.viaversion.viaversion.util.Key; + import net.raphimc.viabedrock.ViaBedrock; ++import net.raphimc.viabedrock.api.resourcepack.BaseGameVersion; + import net.raphimc.viabedrock.api.resourcepack.ResourcePack; + import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; + ++import java.util.ArrayList; + import java.util.Collections; + import java.util.HashMap; ++import java.util.List; + import java.util.Map; + import java.util.logging.Level; + + public class BiomeDefinitions { + ++ private static final BaseGameVersion CLIENT_BIOMES_VERSION = new BaseGameVersion(1, 21, 40); ++ + private final Map biomes = new HashMap<>(); ++ private final Map> fogBindings; ++ private final List defaultFog; + + public BiomeDefinitions(final ResourcePackStorage resourcePackStorage) { ++ final Map> legacyFog = new HashMap<>(); + for (ResourcePack pack : resourcePackStorage.getPackStackBottomToTop()) { ++ if (pack.content().contains("biomes_client.json") && (resourcePackStorage.isServerPack(pack) ++ || resourcePackStorage.getBaseGameVersion().compareTo(CLIENT_BIOMES_VERSION) < 0)) { ++ this.loadLegacyFog(pack, legacyFog); ++ } for (String biomePath : pack.content().getFilesDeep("biomes/", ".json")) { try { final JsonObject biome = pack.content().getJson(biomePath).getAsJsonObject("minecraft:client_biome"); @@ -317,7 +318,54 @@ index 004b5be32..7c049c9b6 100644 final BiomeDefinition biomeDefinition = new BiomeDefinition(name); if (biome.has("components")) { final JsonObject components = biome.getAsJsonObject("components"); -@@ -60,7 +60,7 @@ public class BiomeDefinitions { +@@ -57,10 +70,54 @@ public class BiomeDefinitions { + } + } + } ++ this.defaultFog = legacyFog.getOrDefault("default", List.of()); ++ legacyFog.remove("default"); ++ final Map> bindings = new HashMap<>(); ++ for (BiomeDefinition biome : this.biomes.values()) { ++ if (biome.fog() != null) { ++ bindings.put(biome.name(), List.of(biome.fog())); ++ } ++ } ++ // Native legacy fog bindings override modern biome fog, independent of pack order. ++ bindings.putAll(legacyFog); ++ this.fogBindings = Map.copyOf(bindings); ++ } ++ ++ private void loadLegacyFog(final ResourcePack pack, final Map> bindings) { ++ try { ++ final JsonObject biomes = pack.content().getJson("biomes_client.json").getAsJsonObject("biomes"); ++ final JsonObject defaults = biomes.getAsJsonObject("default"); ++ if (defaults != null && defaults.has("remove_all_prior_fog") && defaults.get("remove_all_prior_fog").getAsBoolean()) { ++ bindings.clear(); ++ } ++ for (Map.Entry entry : biomes.entrySet()) { ++ final JsonObject biome = entry.getValue().getAsJsonObject(); ++ if (!biome.has("fog_identifier")) { ++ continue; ++ } ++ final String name = entry.getKey().equals("default") ? "default" : Key.namespaced(entry.getKey()); ++ final List fog = new ArrayList<>(); ++ if (biome.has("inherit_from_prior_fog") && biome.get("inherit_from_prior_fog").getAsBoolean()) { ++ fog.addAll(bindings.getOrDefault(name, List.of())); ++ } ++ fog.add(Key.namespaced(biome.get("fog_identifier").getAsString())); ++ bindings.put(name, List.copyOf(fog)); ++ } ++ } catch (final Throwable e) { ++ ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Failed to parse legacy biome fog in pack " + pack.key(), e); ++ } ++ } ++ ++ public Map> fogBindings() { ++ return this.fogBindings; ++ } ++ ++ public List defaultFog() { ++ return this.defaultFog; } public BiomeDefinition get(final String name) { @@ -326,6 +374,38 @@ index 004b5be32..7c049c9b6 100644 } public Map biomes() { +diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/FogDefinitions.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/FogDefinitions.java +index 6196540c0..00e7f86d3 100644 +--- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/FogDefinitions.java ++++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/FogDefinitions.java +@@ -25,6 +25,7 @@ import net.raphimc.viabedrock.api.resourcepack.ResourcePack; + import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; + + import java.util.HashMap; ++import java.util.List; + import java.util.Map; + import java.util.logging.Level; + +@@ -58,6 +59,19 @@ public class FogDefinitions { + return this.fogs.get(identifier); + } + ++ public FogDefinition resolve(final List identifiers) { ++ final Map colors = new HashMap<>(); ++ String identifier = null; ++ for (String name : identifiers) { ++ final FogDefinition fog = this.get(name); ++ if (fog != null) { ++ identifier = fog.identifier(); ++ colors.putAll(fog.colors()); ++ } ++ } ++ return identifier != null ? new FogDefinition(identifier, Map.copyOf(colors)) : null; ++ } ++ + public record FogDefinition(String identifier, Map colors) { + } + diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/http/ConvertedResourcePackCache.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/http/ConvertedResourcePackCache.java index d9ce5c69d..1cad5c75b 100644 --- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/http/ConvertedResourcePackCache.java @@ -358,6 +438,19 @@ index aa4cef92d..163a5c3a9 100644 providers.register(SkinProvider.class, new SkinProvider()); providers.register(EmoteProvider.class, new EmoteProvider()); +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java b/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java +index d850863cf..a27810aff 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java +@@ -94,7 +94,7 @@ public final class JavaRegistries { + for (String bedrockBiomeName : BedrockProtocol.MAPPINGS.getBedrockBiomes().keySet()) { + final CompoundTag bedrockBiome = biomeDefinitions.getCompoundTag(Key.namespaced(bedrockBiomeName)); + final BiomeDefinitions.BiomeDefinition bedrockBiomeDefinition = resourcePackStorage.getBiomes().get(bedrockBiomeName); +- final FogDefinitions.FogDefinition bedrockFogDefinition = bedrockBiomeDefinition != null ? resourcePackStorage.getFogs().get(bedrockBiomeDefinition.fog()) : null; ++ final FogDefinitions.FogDefinition bedrockFogDefinition = resourcePackStorage.getFogs().resolve(resourcePackStorage.getBiomes().fogBindings().getOrDefault(Key.namespaced(bedrockBiomeName), List.of())); + if (bedrockBiome == null) { + ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Missing biome definition for " + bedrockBiomeName); + continue; diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/ResourcePackPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/ResourcePackPackets.java index 1a6e4d3f1..8454de390 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/ResourcePackPackets.java @@ -1142,10 +1235,10 @@ index de4a1c640..8505d480a 100644 @Test diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/definition/BiomeDefinitionsTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/definition/BiomeDefinitionsTest.java new file mode 100644 -index 000000000..7688eae74 +index 000000000..a059e6fa4 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/definition/BiomeDefinitionsTest.java -@@ -0,0 +1,78 @@ +@@ -0,0 +1,157 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -1154,6 +1247,7 @@ index 000000000..7688eae74 +package net.raphimc.viabedrock.api.resourcepack.definition; + +import com.viaversion.viaversion.libs.gson.JsonObject; ++import net.raphimc.viabedrock.api.resourcepack.BaseGameVersion; +import net.raphimc.viabedrock.api.resourcepack.ResourcePack; +import net.raphimc.viabedrock.api.resourcepack.content.InMemoryContent; +import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; @@ -1191,10 +1285,88 @@ index 000000000..7688eae74 + assertNotSame(definitions.get("the_end"), definitions.get("test:the_end")); + } + ++ @Test ++ void legacyInheritanceMergesMediaWhileReplacementFallsThroughToDataDefault() { ++ final var lower = legacyPack(""" ++ {"default":{"fog_identifier":"test:green","remove_all_prior_fog":true}, ++ "the_end":{"fog_identifier":"test:blue"}, ++ "minecraft:plains":{"fog_identifier":"test:blue"}} ++ """); ++ final var higher = legacyPack(""" ++ {"minecraft:the_end":{"fog_identifier":"test:water","inherit_from_prior_fog":true}, ++ "plains":{"fog_identifier":"test:water"}} ++ """); ++ lower.content().putString("fogs/blue.json", fog("test:blue", "air", "#0000FF")); ++ lower.content().putString("fogs/green.json", fog("test:green", "air", "#00FF00")); ++ higher.content().putString("fogs/water.json", fog("test:water", "water", "#00FF00")); ++ final var storage = storage(lower, higher); ++ final var definitions = new BiomeDefinitions(storage); ++ final var fogs = new FogDefinitions(storage); ++ assertEquals(0x0000FF, fogs.resolve(definitions.fogBindings().get("minecraft:the_end")).colors().get("air")); ++ assertEquals(0x00FF00, fogs.resolve(definitions.fogBindings().get("minecraft:the_end")).colors().get("water")); ++ assertFalse(fogs.resolve(definitions.fogBindings().get("minecraft:plains")).colors().containsKey("air")); ++ assertEquals(0x00FF00, fogs.resolve(definitions.defaultFog()).colors().get("air")); ++ } ++ ++ @Test ++ void clearingLegacyFogExposesModernBindingWithoutDiscardingModernMetadata() { ++ final var lower = legacyPack(""" ++ {"default":{"fog_identifier":"test:default"},"the_end":{"fog_identifier":"test:legacy"}} ++ """); ++ final var modern = pack("minecraft:the_end", "test:modern", 0x112233, 0x445566); ++ assertEquals(List.of("test:legacy"), definitions(lower, modern).fogBindings().get("minecraft:the_end")); ++ final var clear = legacyPack(""" ++ {"minecraft:default":{"fog_identifier":"test:biome"}, ++ "default":{"remove_all_prior_fog":true,"fog_identifier":"test:new_default"}} ++ """); ++ final var cleared = definitions(lower, modern, clear); ++ assertEquals(List.of("test:modern"), cleared.fogBindings().get("minecraft:the_end")); ++ assertEquals(0x112233, cleared.get("the_end").skyColor()); ++ assertEquals(List.of("test:new_default"), cleared.defaultFog()); ++ assertEquals(List.of("test:biome"), cleared.fogBindings().get("minecraft:default")); ++ assertThrows(UnsupportedOperationException.class, () -> cleared.fogBindings().clear()); ++ assertThrows(UnsupportedOperationException.class, () -> cleared.defaultFog().add("test:other")); ++ } ++ ++ @Test ++ void vanillaLegacyFogStopsAtClientBiomeCutoffWhileServerLegacyFogStillLoads() { ++ final var builtin = legacyPack("{\"the_end\":{\"fog_identifier\":\"test:builtin\"}}"); ++ final var storage = storage(builtin); ++ when(storage.isServerPack(builtin)).thenReturn(false); ++ when(storage.getBaseGameVersion()).thenReturn(BaseGameVersion.parse("1.21.39")); ++ assertEquals(List.of("test:builtin"), new BiomeDefinitions(storage).fogBindings().get("minecraft:the_end")); ++ when(storage.getBaseGameVersion()).thenReturn(BaseGameVersion.parse("1.21.40")); ++ assertTrue(new BiomeDefinitions(storage).fogBindings().isEmpty()); ++ when(storage.isServerPack(builtin)).thenReturn(true); ++ assertEquals(List.of("test:builtin"), new BiomeDefinitions(storage).fogBindings().get("minecraft:the_end")); ++ } ++ ++ private static String fog(final String identifier, final String medium, final String color) { ++ return """ ++ {"minecraft:fog_settings":{"description":{"identifier":"%s"},"distance":{"%s":{"fog_color":"%s"}}}} ++ """.formatted(identifier, medium, color); ++ } ++ ++ private static ResourcePack legacyPack(final String biomes) { ++ final var content = new InMemoryContent(); ++ content.putString("manifest.json", "{\"format_version\":2,\"header\":{\"uuid\":\"" + UUID.randomUUID() ++ + "\",\"version\":[1,0,0],\"name\":\"test\"}}"); ++ content.putString("biomes_client.json", "{\"biomes\":" + biomes + "}"); ++ return new ResourcePack(content); ++ } ++ + private static BiomeDefinitions definitions(final ResourcePack... layers) { ++ return new BiomeDefinitions(storage(layers)); ++ } ++ ++ private static ResourcePackStorage storage(final ResourcePack... layers) { + final var storage = mock(ResourcePackStorage.class); + when(storage.getPackStackBottomToTop()).thenReturn(List.of(layers)); -+ return new BiomeDefinitions(storage); ++ when(storage.getBaseGameVersion()).thenReturn(BaseGameVersion.CURRENT); ++ for (ResourcePack layer : layers) { ++ when(storage.isServerPack(layer)).thenReturn(true); ++ } ++ return storage; + } + + private static ResourcePack pack(final String identifier, final String fog, final int sky, final int water) { diff --git a/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.pr.md b/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.pr.md index 2b50202d..28600412 100644 --- a/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.pr.md +++ b/patches/viabedrock/upstreamable/0068-resolve-licensed-native-image-fallbacks.pr.md @@ -2,12 +2,15 @@ ## Purpose -Bedrock 1.26.51.1 protocol 2193 accepts Hive item atlas aliases to vanilla images which the server does not resend. The bundled metadata archives intentionally contain no images, leaving these aliases unresolved during Java conversion. +Select vanilla metadata, licensed images, actor dependencies, and audio by the world's advertised base-game version. +Apply server assets above those selected resources, including image aliases that servers do not resend. ## Design constraints Resolve licensed images beneath server assets from the selected native resource context. Keep acquisition, version validation, cache identity, and fallback ownership explicit. +Legacy fog bindings stay separate from modern biome metadata. Vanilla legacy files stop at base version 1.21.40. Server legacy files still load. + Prerequisites: - viabedrock/0015-transport-native-resources-and-sound-commands.patch: Carry the selected world version through the existing native resource archive and converted-pack transport. @@ -16,8 +19,13 @@ Prerequisites: ## Validation and remaining limits -Regression coverage includes `BaseGameVersionTest`, `BuiltinActorResourcesTest`, `NativeActorLibraryTest`, `SoundTranslationsTest`. +Regression coverage includes `BaseGameVersionTest`, `BuiltinActorResourcesTest`, `NativeActorLibraryTest`, `SoundTranslationsTest`, and `BiomeDefinitionsTest`. -Resource-selection and fallback tests protect known cases. Missing licensed resources and broader native rendering comparisons remain limits. +Native 1.26.60.5 Fancy controls cover legacy fog inheritance, replacement, and clearing over modern definitions. +Remote current/1.20.0/current controls also admit unqualified server legacy biome keys. All fog identifiers in these controls remain qualified. +Homogeneous neighboring bindings separate name admission from spatial blending. +Four direct and ViaProxy recordings confirm the advertised current and older contexts and match the settled blue 48-block binding. +Color differences stay below 0.00001 RGBA and distance differences below 0.001 blocks. All recordings exit normally. +Missing licensed resources and broader rendering comparisons remain limits. The [owning patch](0068-resolve-licensed-native-image-fallbacks.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viabedrock/upstreamable/0070-inherit-native-air-fog-defaults.patch b/patches/viabedrock/upstreamable/0070-inherit-native-air-fog-defaults.patch index e79c3c03..771fc4c3 100644 --- a/patches/viabedrock/upstreamable/0070-inherit-native-air-fog-defaults.patch +++ b/patches/viabedrock/upstreamable/0070-inherit-native-air-fog-defaults.patch @@ -1,28 +1,23 @@ -From d9c622ddafc003c956988ddf544f43ab741fddcd Mon Sep 17 00:00:00 2001 +From 1697035f66cbcc0d13dd8a009db2142b7855940e Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] fix(biomes): inherit native air fog defaults -When a biome fog has no air color, inherit the effective resource-pack -minecraft:fog_default air color before using a last-resort native value. -The synthetic the_void biome uses the same inherited default. This keeps -server overrides effective instead of replacing them with Java's default -#C0D8FF. Limit this change to registry air fog inputs. +Use an explicit biome air color first, then the resource pack's resolved +data-default binding, then native #ABD2FF when both are absent. The +synthetic the_void biome uses the same default. Preserve black colors and +keep registry air fog independent from water fog. -Bedrock 1.26.51.1 (protocol 2193) ships default air #ABD2FF in its licensed -vanilla_1.16.100, vanilla_1.18.0, and vanilla_1.19.0 fog archives. The -vanilla_1.19.0 archive SHA-256 is -74fd138548bcead6bd466ac26a5e10f7ad3988def3dff9277b7aceb94fe59e94; -default_fog_setting.json SHA-256 is -373bd242339101bea36d7f1cc3cbb5b03d8b5a8d23d7759e46d0950d3549f45b. -The existing fog loader already applies pack precedence. Preserve explicit -biome air colors, including black, and use #ABD2FF only when both levels -lack air. Native assets and extracted evidence remain private. +The default binding comes from biomes_client.json, not the conventional +minecraft:fog_default asset name. Native Bedrock 1.26.60.5 controls use a +custom green default beneath a replacement water-only End binding. The +result is green air at the default's 24-block distance. Inherited biome +air takes priority; clearing the binding exposes modern biome resources. -The full native sky shader is still unported; correcting this input does -not establish complete sky or fog pixel parity. Focused numerical tests -cover biome precedence, pack defaults, missing definitions, and water-only -fog definitions. The full core suite and both Checkstyle tasks pass. +Bedrock 1.26.51.1 vanilla archives supply #ABD2FF in default_fog_setting. +The fallback remains for absent data, without inventing a pack binding. +Focused tests cover explicit colors, inherited defaults, missing media, +and the last-resort color. Complete native sky shading remains separate. --- .../protocol/data/JavaRegistries.java | 24 ++++++---- .../protocol/data/JavaRegistriesFogTest.java | 46 +++++++++++++++++++ @@ -30,7 +25,7 @@ fog definitions. The full core suite and both Checkstyle tasks pass. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/data/JavaRegistriesFogTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java b/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java -index ea3cd9011..99801d584 100644 +index a27810aff..a948f481b 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java @@ -40,7 +40,7 @@ import java.util.logging.Level; @@ -42,17 +37,17 @@ index ea3cd9011..99801d584 100644 public static CompoundTag createJavaRegistries(final GameSessionStorage gameSession, final ResourcePackStorage resourcePackStorage) { final CompoundTag registries = BedrockProtocol.MAPPINGS.getJavaRegistries().copy(); -@@ -74,7 +74,8 @@ public final class JavaRegistries { +@@ -85,7 +85,8 @@ public final class JavaRegistries { private static CompoundTag buildJavaBiomeRegistry(final CompoundTag biomeDefinitions, final ResourcePackStorage resourcePackStorage) { final CompoundTag javaBiomes = new CompoundTag(); - javaBiomes.put("minecraft:the_void", getTheVoidBiome()); -+ final FogDefinitions.FogDefinition defaultFogDefinition = resourcePackStorage.getFogs().get("minecraft:fog_default"); ++ final FogDefinitions.FogDefinition defaultFogDefinition = resourcePackStorage.getFogs().resolve(resourcePackStorage.getBiomes().defaultFog()); + javaBiomes.put("minecraft:the_void", getTheVoidBiome(getAirFogColor(null, defaultFogDefinition))); final Map foliageColor = BedrockProtocol.MAPPINGS.getBedrockToJavaBiomeExtraData().get("foliage_color"); final Map grassColor = BedrockProtocol.MAPPINGS.getBedrockToJavaBiomeExtraData().get("grass_color"); -@@ -129,11 +130,7 @@ public final class JavaRegistries { +@@ -140,11 +141,7 @@ public final class JavaRegistries { } else { attributes.putInt("minecraft:visual/sky_color", getSkyColor(bedrockBiome.getDoubleTag("temperature").asFloat())); } @@ -65,7 +60,7 @@ index ea3cd9011..99801d584 100644 if (bedrockFogDefinition != null && bedrockFogDefinition.colors().containsKey("water")) { attributes.putInt("minecraft:visual/water_fog_color", bedrockFogDefinition.colors().get("water")); } else { -@@ -169,7 +166,16 @@ public final class JavaRegistries { +@@ -180,7 +177,16 @@ public final class JavaRegistries { return javaBiomes; } @@ -83,7 +78,7 @@ index ea3cd9011..99801d584 100644 final CompoundTag biome = new CompoundTag(); biome.putFloat("temperature", 0.5F); biome.putFloat("downfall", 0.5F); -@@ -181,7 +187,7 @@ public final class JavaRegistries { +@@ -192,7 +198,7 @@ public final class JavaRegistries { biome.put("attributes", attributes); attributes.putInt("minecraft:visual/sky_color", getSkyColor(0.5F)); attributes.putInt("minecraft:visual/water_fog_color", BEDROCK_DEFAULT_WATER_COLOR); diff --git a/patches/viabedrock/upstreamable/0070-inherit-native-air-fog-defaults.pr.md b/patches/viabedrock/upstreamable/0070-inherit-native-air-fog-defaults.pr.md index 3bde9ce5..946b36db 100644 --- a/patches/viabedrock/upstreamable/0070-inherit-native-air-fog-defaults.pr.md +++ b/patches/viabedrock/upstreamable/0070-inherit-native-air-fog-defaults.pr.md @@ -1,11 +1,20 @@ # Inherit native air fog defaults -Biomes without an air fog color currently receive Java's `#C0D8FF`. Bedrock 1.26.51.1 supplies `#ABD2FF` through `minecraft:fog_default` instead. +## Purpose -Use an explicit biome air color first, then the effective resource pack's default air color, then `#ABD2FF` if both are absent. The synthetic `the_void` biome follows the same default. This preserves server overrides and valid black colors. Water fog behavior stays unchanged. +Biomes without an air color inherit the pack-defined data default before the native `#ABD2FF` fallback. +The synthetic `the_void` biome uses the same default. -The installed 1.26.51.1 vanilla fog archives contain `#ABD2FF` in their default fog settings. The 1.19.0 overlay archive has SHA-256 `74fd138548bcead6bd466ac26a5e10f7ad3988def3dff9277b7aceb94fe59e94`; its `default_fog_setting.json` entry has SHA-256 `373bd242339101bea36d7f1cc3cbb5b03d8b5a8d23d7759e46d0950d3549f45b`. All three bundled overlays defining `minecraft:fog_default` also contain this air color. A private Java probe loads all 60 configured vanilla packs into `ResourcePackStorage`; the effective parsed default air color is `0xABD2FF`, or RGB `(171, 210, 255)`. +## Design constraints -Validation: the full core suite passes with 281 tests executed, none skipped, and no failures. Both main and test Checkstyle tasks pass. Focused tests cover explicit biome precedence, black, inherited defaults, missing air entries, and the final fallback. +Resolve the actual legacy default binding instead of assuming the asset name `minecraft:fog_default`. +Preserve explicit biome colors, including black. Keep water fog separate. -This patch corrects the air fog input. It does not port the complete native sky shader or establish complete sky pixel parity. +## Validation and remaining limits + +`JavaRegistriesFogTest` covers color precedence and missing media. +Native 1.26.60.5 Fancy controls confirm a custom green default beneath a replacement water-only biome binding. +The resource and transport patches cover binding selection and distance composition. +Complete native sky shading remains separate. + +The [owning patch](0070-inherit-native-air-fog-defaults.patch) retains the rationale and native evidence. diff --git a/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.patch b/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.patch index c7f13c4f..a572e043 100644 --- a/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.patch +++ b/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.patch @@ -1,42 +1,47 @@ -From 02d425b55d061714cc4bcb50783f478a1a70e790 Mon Sep 17 00:00:00 2001 +From 36d3d1de3c0c93036215026d40561d77946edd4a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:05:15 +0100 Subject: [PATCH] feat(fog): transport resolved native fog settings -Retain complete PLAYER_FOG snapshots and resolve command, biome and -existing registry-default settings against the connection-selected packs. -Publish one immutable version-2 snapshot after join and channel negotiation, -including empty command resets and late registrations. Preserve distance -units and colors without inferring field-level inheritance. +Retain complete PLAYER_FOG snapshots and resolve commands, biome bindings, +and data defaults against the connection-selected packs. Publish immutable +version-2 snapshots after join and channel negotiation, including empty +command resets and late registrations. Preserve distance units and colors +without inferring partial-field inheritance. -Native Bedrock 1.26.60.5 with BDS 1.26.60.6 (protocol 2225) confirms -unsigned-count string-array snapshots, topmost matching-medium precedence, -and water-only fallback to lower air settings. A qualified End-biome fixture -renders red fog from 0 to 32 blocks without commands or living dragons. -Both prior Java routes read the red registry color but kept 1,024 blocks. +Resolve inherited legacy fog chains per medium before transport. Use the +actual default binding instead of assuming minecraft:fog_default. Clearing +legacy bindings leaves modern biome definitions available. World changes +retire the entire snapshot; no client payload change is necessary for +these resolved resource layers. -Empty command stacks now retain world resources. World replacement retires -the entire snapshot, and payload decoding rejects ambiguous biome aliases -and unsupported versions. Regressions cover medium precedence, immutable -resource ownership, codec bounds, late registration, command removal and -world replacement. Biome-boundary blending, legacy data-default bindings, -and native engine-specific vanilla rules remain separate acceptance work. +Native Bedrock 1.26.60.5 with BDS 1.26.60.6, protocol 2225, confirms the +unsigned-count string array, matching-medium command priority, and red +32-block End fog without commands or living dragons. Native Fancy controls +also establish blue 48-block air beneath an inherited water-only legacy +binding, green 24-block data-default air after replacement, and restoration +of modern red 32-block fog after legacy clearing. Captures stay private. + +Regressions cover composed resource-to-snapshot behavior, replacement, +immutable ownership, command removal, codec bounds, and late registration. +Biome-boundary blending, remote legacy name cutoffs, engine-specific +vanilla rules, and other render modes remain separate acceptance work. BREAKING CHANGE: FogDefinition includes distances; FogState requires biome/default settings and the negotiated fog payload uses version 2. --- .../viabedrock/api/model/FogState.java | 176 ++++++++++++++++++ - .../definition/FogDefinitions.java | 44 +++-- + .../definition/FogDefinitions.java | 48 +++-- .../viabedrock/protocol/BedrockProtocol.java | 2 + .../protocol/packet/FogPackets.java | 32 ++++ .../protocol/packet/JoinPackets.java | 2 + .../protocol/packet/MultiStatePackets.java | 4 + .../protocol/packet/UnhandledPackets.java | 1 - - .../protocol/storage/FogStorage.java | 72 +++++++ + .../protocol/storage/FogStorage.java | 69 +++++++ .../viabedrock/api/model/FogStateTest.java | 144 ++++++++++++++ .../definition/FogDefinitionsTest.java | 73 ++++++++ .../protocol/data/JavaRegistriesFogTest.java | 12 +- - .../protocol/storage/FogStorageTest.java | 117 ++++++++++++ - 12 files changed, 661 insertions(+), 18 deletions(-) + .../protocol/storage/FogStorageTest.java | 145 +++++++++++++++ + 12 files changed, 689 insertions(+), 19 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/FogState.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/packet/FogPackets.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/storage/FogStorage.java @@ -227,7 +232,7 @@ index 000000000..be1a75389 + +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/FogDefinitions.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/FogDefinitions.java -index 6196540c0..a799d2730 100644 +index 00e7f86d3..b3d7c14ed 100644 --- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/FogDefinitions.java +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/definition/FogDefinitions.java @@ -21,6 +21,7 @@ import com.viaversion.viaversion.libs.gson.JsonElement; @@ -238,7 +243,7 @@ index 6196540c0..a799d2730 100644 import net.raphimc.viabedrock.api.resourcepack.ResourcePack; import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; -@@ -37,16 +38,8 @@ public class FogDefinitions { +@@ -38,16 +39,8 @@ public class FogDefinitions { for (String fogPath : pack.content().getFilesDeep("fogs/", ".json")) { try { final JsonObject fog = pack.content().getJson(fogPath).getAsJsonObject("minecraft:fog_settings"); @@ -257,8 +262,22 @@ index 6196540c0..a799d2730 100644 } catch (final Throwable e) { ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Failed to parse fog definition " + fogPath + " in pack " + pack.key(), e); } -@@ -58,7 +51,36 @@ public class FogDefinitions { - return this.fogs.get(identifier); +@@ -61,18 +54,49 @@ public class FogDefinitions { + + public FogDefinition resolve(final List identifiers) { + final Map colors = new HashMap<>(); ++ final Map distances = new HashMap<>(); + String identifier = null; + for (String name : identifiers) { + final FogDefinition fog = this.get(name); + if (fog != null) { + identifier = fog.identifier(); + colors.putAll(fog.colors()); ++ distances.putAll(fog.distances()); + } + } +- return identifier != null ? new FogDefinition(identifier, Map.copyOf(colors)) : null; ++ return identifier != null ? new FogDefinition(identifier, colors, distances) : null; } - public record FogDefinition(String identifier, Map colors) { @@ -367,10 +386,10 @@ index d4cf7a04a..215e2d3c6 100644 user.get(HudVisibilityStorage.class).sendSnapshot(); user.get(CameraEffectsStorage.class).markJoined(); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java -index df24b545b..476b5fc38 100644 +index 9cbf7341a..d41a2a00a 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java -@@ -87,6 +87,7 @@ import net.raphimc.viabedrock.protocol.storage.PlayerInputStorage; +@@ -88,6 +88,7 @@ import net.raphimc.viabedrock.protocol.storage.PlayerInputStorage; import net.raphimc.viabedrock.protocol.storage.PlayerPredictionStorage; import net.raphimc.viabedrock.protocol.storage.VehiclePredictionStorage; import net.raphimc.viabedrock.protocol.storage.HudVisibilityStorage; @@ -378,7 +397,7 @@ index df24b545b..476b5fc38 100644 import net.raphimc.viabedrock.protocol.storage.CameraEffectsStorage; import net.raphimc.viabedrock.protocol.types.BedrockTypes; -@@ -379,6 +380,9 @@ public final class MultiStatePackets { +@@ -390,6 +391,9 @@ public final class MultiStatePackets { HudPackets.sendNativeRoot(wrapper.user()); wrapper.user().get(PlayerInputStorage.class).sendSnapshot(); wrapper.user().get(HudVisibilityStorage.class).sendSnapshot(); @@ -402,10 +421,10 @@ index 0e0fe5920..b8c53fa4f 100644 protocol.cancelClientbound(ClientboundBedrockPackets.LESSON_PROGRESS); // Not relevant (Education Edition) diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/FogStorage.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/FogStorage.java new file mode 100644 -index 000000000..cf1c6e04e +index 000000000..13a918f75 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/FogStorage.java -@@ -0,0 +1,72 @@ +@@ -0,0 +1,69 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -447,16 +466,13 @@ index 000000000..cf1c6e04e + public FogState snapshot(final ResourcePackStorage resources) { + final FogDefinitions definitions = resources.getFogs(); + final Map biomes = new LinkedHashMap<>(); -+ for (final var biome : resources.getBiomes().biomes().values()) { -+ if (biome.fog() == null) { -+ continue; -+ } -+ final FogDefinitions.FogDefinition definition = definitions.get(biome.fog()); ++ for (final var binding : resources.getBiomes().fogBindings().entrySet()) { ++ final FogDefinitions.FogDefinition definition = definitions.resolve(binding.getValue()); + if (definition != null) { -+ biomes.put(biome.name(), new FogState.Layer(definition.identifier(), definition.distances())); ++ biomes.put(binding.getKey(), new FogState.Layer(definition.identifier(), definition.distances())); + } + } -+ final FogDefinitions.FogDefinition defaults = definitions.get("minecraft:fog_default"); ++ final FogDefinitions.FogDefinition defaults = definitions.resolve(resources.getBiomes().defaultFog()); + return new FogState(this.identifiers.stream().map(identifier -> { + final FogDefinitions.FogDefinition definition = definitions.get(identifier); + return new FogState.Layer(identifier, definition == null ? Map.of() : definition.distances()); @@ -739,10 +755,10 @@ index 05383c57d..658e16522 100644 diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/FogStorageTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/FogStorageTest.java new file mode 100644 -index 000000000..7eeaa0355 +index 000000000..228a91e4d --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/FogStorageTest.java -@@ -0,0 +1,117 @@ +@@ -0,0 +1,145 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -756,12 +772,16 @@ index 000000000..7eeaa0355 +import com.viaversion.viaversion.api.protocol.packet.State; +import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets26_3; +import net.raphimc.viabedrock.api.model.FogState; ++import net.raphimc.viabedrock.api.resourcepack.BaseGameVersion; ++import net.raphimc.viabedrock.api.resourcepack.ResourcePack; ++import net.raphimc.viabedrock.api.resourcepack.content.InMemoryContent; +import net.raphimc.viabedrock.api.resourcepack.definition.FogDefinitions; +import net.raphimc.viabedrock.api.resourcepack.definition.BiomeDefinitions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; ++import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; @@ -818,44 +838,68 @@ index 000000000..7eeaa0355 + } + + @Test -+ void worldReplacementAndEmptyCommandsResolveCurrentBiomeResources() { ++ void worldReplacementPublishesInheritedLegacyMediaAndClearedModernFallback() { ++ final var lower = pack(""" ++ {"default":{"fog_identifier":"test:default"},"the_end":{"fog_identifier":"test:end"}} ++ """); ++ lower.content().putString("fogs/default.json", fog("test:default", "air", 24, "#00FF00")); ++ lower.content().putString("fogs/end.json", fog("test:end", "air", 48, "#0000FF")); ++ final var upper = pack(""" ++ {"the_end":{"fog_identifier":"test:water","inherit_from_prior_fog":true}} ++ """); ++ upper.content().putString("fogs/water.json", fog("test:water", "water", 8, "#00FF00")); ++ upper.content().putString("biomes/end.json", """ ++ {"minecraft:client_biome":{"description":{"identifier":"minecraft:the_end"}, ++ "components":{"minecraft:fog_appearance":{"fog_identifier":"test:modern"}}}} ++ """); ++ upper.content().putString("fogs/modern.json", fog("test:modern", "air", 32, "#FF0000")); ++ final var clear = pack("{\"default\":{\"remove_all_prior_fog\":true}}"); + final var storage = new FogStorage(mock(UserConnection.class)); -+ final var definitions = mock(FogDefinitions.class); -+ final var biomes = mock(BiomeDefinitions.class); -+ final var biome = mock(BiomeDefinitions.BiomeDefinition.class); -+ when(biome.name()).thenReturn("minecraft:the_end"); -+ when(biome.fog()).thenReturn("test:end"); -+ when(biomes.biomes()).thenReturn(Map.of("minecraft:the_end", biome)); ++ final var first = storage.snapshot(resources(lower, upper)); ++ assertEquals(new FogState.Distance(0, 48, 0x0000FF, false), first.resolve("air", "the_end")); ++ assertEquals(new FogState.Distance(0, 8, 0x00FF00, false), first.resolve("water", "the_end")); ++ assertEquals(new FogState.Distance(0, 24, 0x00FF00, false), first.resolve("air", "test:unknown")); ++ final var replaced = storage.snapshot(resources(lower, upper, clear)); ++ assertEquals(new FogState.Distance(0, 32, 0xFF0000, false), replaced.resolve("air", "the_end")); ++ assertNull(replaced.resolve("air", "test:unknown")); ++ assertEquals(48, first.resolve("air", "the_end").end()); ++ } ++ ++ private static String fog(final String identifier, final String medium, final int end, final String color) { ++ return """ ++ {"minecraft:fog_settings":{"description":{"identifier":"%s"},"distance":{"%s": ++ {"fog_color":"%s","fog_start":0,"fog_end":%d,"render_distance_type":"fixed"}}}} ++ """.formatted(identifier, medium, color, end); ++ } ++ ++ private static ResourcePack pack(final String biomes) { ++ final var content = new InMemoryContent(); ++ content.putString("manifest.json", "{\"format_version\":2,\"header\":{\"uuid\":\"" + UUID.randomUUID() ++ + "\",\"version\":[1,0,0],\"name\":\"test\"}}"); ++ content.putString("biomes_client.json", "{\"biomes\":" + biomes + "}"); ++ return new ResourcePack(content); ++ } ++ ++ private static ResourcePackStorage resources(final ResourcePack... packs) { + final var resources = mock(ResourcePackStorage.class); -+ when(resources.getFogs()).thenReturn(definitions); ++ when(resources.getPackStackBottomToTop()).thenReturn(List.of(packs)); ++ when(resources.getBaseGameVersion()).thenReturn(BaseGameVersion.CURRENT); ++ for (ResourcePack pack : packs) { ++ when(resources.isServerPack(pack)).thenReturn(true); ++ } ++ final var biomes = new BiomeDefinitions(resources); ++ final var fogs = new FogDefinitions(resources); + when(resources.getBiomes()).thenReturn(biomes); -+ final var current = new FogState.Distance(0, 32, 0xFF0000, false); -+ final var older = new FogState.Distance(0, 64, 0x0000FF, false); -+ final var command = new FogState.Distance(0, 16, 0xFFFFFF, false); -+ when(definitions.get("test:end")).thenReturn(new FogDefinitions.FogDefinition("test:end", Map.of(), Map.of("air", current))); -+ when(definitions.get("test:command")).thenReturn(new FogDefinitions.FogDefinition("test:command", Map.of(), Map.of("air", command))); -+ final var fallback = new FogState.Distance(0.25F, 0.75F, 0x123456, true); -+ when(definitions.get("minecraft:fog_default")).thenReturn(new FogDefinitions.FogDefinition("minecraft:fog_default", Map.of(), Map.of("air", fallback))); -+ final var first = storage.snapshot(resources); -+ assertSame(fallback, first.resolve("air", "test:unknown")); -+ storage.update(new String[]{"test:command"}); -+ assertSame(command, storage.snapshot(resources).resolve("air", "the_end")); -+ when(definitions.get("test:end")).thenReturn(new FogDefinitions.FogDefinition("test:end", Map.of(), Map.of("air", older))); -+ storage.update(new String[0]); -+ assertSame(older, storage.snapshot(resources).resolve("air", "the_end")); -+ assertSame(current, first.resolve("air", "the_end")); -+ when(biomes.biomes()).thenReturn(Map.of()); -+ assertSame(fallback, storage.snapshot(resources).resolve("air", "the_end")); -+ when(definitions.get("minecraft:fog_default")).thenReturn(null); -+ assertEquals(FogState.EMPTY, storage.snapshot(resources)); ++ when(resources.getFogs()).thenReturn(fogs); ++ return resources; + } + -+ private static ResourcePackStorage resources(final FogDefinitions definitions, final Map bindings) { ++ private static ResourcePackStorage resources(final FogDefinitions definitions, final Map> bindings) { + final var resources = mock(ResourcePackStorage.class); + final var biomes = mock(BiomeDefinitions.class); + when(resources.getFogs()).thenReturn(definitions); + when(resources.getBiomes()).thenReturn(biomes); -+ when(biomes.biomes()).thenReturn(bindings); ++ when(biomes.fogBindings()).thenReturn(bindings); + return resources; + } + diff --git a/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.pr.md b/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.pr.md index 719dd930..df418a81 100644 --- a/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.pr.md +++ b/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.pr.md @@ -1,35 +1,24 @@ # World-resolved native fog transport -Retain complete `PLAYER_FOG` command stacks and publish world-selected settings through `viabedrock:fog` payload version 2. -Each immutable snapshot includes command layers, biome bindings, and the existing `minecraft:fog_default` registry fallback. -Commands take priority over the selected biome, then that fallback, independently for each medium. -Empty commands retain base resources; connection replacement retires the entire snapshot. -Publications remain gated by join state and channel negotiation, including late registration. +## Purpose -The [documented native stack](https://learn.microsoft.com/en-us/minecraft/creator/documents/foginresourcepacks?view=minecraft-bedrock-stable) places command settings above biome settings and data defaults. -This implementation preserves the existing registry fallback policy. -Legacy pack-defined default bindings and spatial biome blending still need matching native controls. +Publish commands, biome settings, and pack-defined defaults through `viabedrock:fog` payload version 2. +Empty commands retain world resources. World changes replace the entire snapshot. -The native protocol 2225 captures use Bedrock 1.26.60.5 and BDS 1.26.60.6. -They establish unsigned-count string-array payloads, matching-medium priority, and lower air fallback beneath a water-only entry. -A qualified End-biome fixture binds a unique red fog identifier without commands or living dragons. -Actual native shader uniforms establish red fog from 0 to 32 blocks at effective render distance 192. -The test pack still matches every file hash from that reference. +## Design constraints -Distance settings retain fixed or render-relative units and immutable colors. -Complete settings require finite valid distances; partial color-only definitions retain their existing registry role. -`FogDefinition` takes a distance map in addition to its color map. -`FogState` now requires biome and default settings, and version 2 replaces the earlier payload contract. -Core, proxy, and client distributions must use the matching contract. -Malformed counts, duplicate biome aliases, unsupported versions, and trailing fields fail decoding. +Commands take priority over biome settings, then data defaults, separately for each medium. +Resolve inherited legacy bindings before transport. Clearing legacy bindings exposes modern biome fog again. +Preserve fixed and render-relative distances. Partial color-only definitions retain their registry role. +Publication requires join completion and channel registration, including late registration. +Core, proxy, and client distributions must share the version-2 payload contract. -Regressions cover medium precedence, immutable world ownership, resource replacement, codec bounds, negotiated publication, and empty commands. -The full core build passes 2,071 cases, including 176 optional skips, with no failures or errors. -Both Checkstyle tasks pass. -Fresh direct and ViaProxy renderer controls each verify seven phases: -red 32-block base fog, water-only inheritance, white 16-block commands, removal, empty commands, living-dragon override, and death. -The cap remains 100 blocks while preserving red base color; death restores 32 blocks. -Both 180-second recordings finish normally, without logged errors or GPU cleanup failures. -Native sky sampling, exact transitions, engine-specific vanilla rules, fluids, and weather remain outside this bounded control. -The private journals and licensed assets remain outside Git. -This transport does not establish complete native fog parity. +## Validation and remaining limits + +Native 1.26.60.5 and BDS 1.26.60.6 controls establish command precedence and red 32-block End fog. +Fresh direct and ViaProxy runs match native Fancy air colors and distances for legacy inheritance, replacement, and modern fallback after clearing. +Prior direct and ViaProxy controls cover commands, removal, empty commands, and dragon state changes. +`FogStorageTest` exercises pack-to-snapshot composition and replacement. Codec tests protect malformed payload boundaries. + +Exact sky pixels, biome blending, combined vision effects, other render modes, fluids, and weather remain open. +The [owning patch](0104-transport-resolved-native-fog-stacks.patch) retains the rationale and versioned evidence. 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 507b3198..e651ee30 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 @@ -18,6 +18,8 @@ Prerequisites: Regression coverage includes `MixinIrisJavaAlphaTest`, `BedrockAcceptedResourcesTest`, `BedrockActorDepthGpuTest`, `BedrockActorGlintFramesTest`. -Native and replay controls establish selected appearances. Armor masking, profile/LOD changes, remote skins, first-person composition, and complete rendering parity remain open. +Native and replay controls establish selected appearances, classic armor masks, and one owned outerwear/chest case. A free-camera fixture keeps players five blocks apart while native draws retain the original persona body and face atlases through 120 camera blocks. + +Rapid profile changes in that fixture receive server echoes at Java while native draws retain the previous bindings. Delivery to the native observer remains unverified. Distinct LOD selection, other wardrobe categories, broader remote skins, first-person composition, and complete rendering parity remain open. The [owning patch](0002-native-entities-and-skins.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0014-reproduce-the-native-bedrock-classic-lightmap.patch b/patches/viafabricplus-bedrock/upstreamable/0014-reproduce-the-native-bedrock-classic-lightmap.patch index 47ffb1d5..6d323572 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0014-reproduce-the-native-bedrock-classic-lightmap.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0014-reproduce-the-native-bedrock-classic-lightmap.patch @@ -1,4 +1,4 @@ -From f840c8db2e0fe2ffea5033ddecdb846ec95146b3 Mon Sep 17 00:00:00 2001 +From 0f5d6c3befe2fd1b507ea901e9b89ecdfd84598b 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(render): reproduce native Bedrock classic lighting @@ -23,14 +23,26 @@ Use immutable frame inputs to preserve Java refresh behavior while updating changed native inputs. Ordinary Java sessions retain their original renderer; End light-image inputs and enhanced native lighting remain outside this port. -Defer Overworld air-fog blindness dimming until after night-vision RGB -normalization. The vanilla ordering cancels the fade on its first nonzero -frame and makes the sky jump to bright fog. Native 1.26.60.5 against -BDS 1.26.60.6 fades back during the final second in day and night controls. -Reuse the existing darkness factor and preserve alpha. Other fog media, -dimensions, and the Darkness effect retain their current paths. -Native fog colors and the precise native fade envelope still differ; -this ordering correction does not establish complete fog parity. +Attenuate both Overworld air-fog RGB and night-vision strength by the +squared tick-based blindness visibility. Cap reciprocal brightness at 255. +This preserves native gradual expiry instead of normalizing the first +nonzero dimmed color directly to full brightness. + +Synchronized 1.26.60.5 Fancy color and distance captures establish all 19 +expiry steps at noon and midnight, plus blindness without night vision. +The native distance ladder matches Java: 1.25..5 blocks while blinded, +then (5 + 187 * visibility) with start at one quarter of that distance for +192-block rendering. Expiry restores the pack's ordinary 64-block range. +Color updates are tick-based too. Earlier fractional color values were +the combined blend, not evidence for partial-tick interpolation. Remove +that approximation and the post-normalization dimming it accompanied. + +Native RGB equals dimmed * lerp(1, 1 / max(maxChannel, 1/255), +nightVisionStrength * visibilitySquared). Daytime low-light steps exercise +the gain cap; all native day/night vectors match this calculation within +0.000002. Preserve cancellation, infinite duration, void darkness, alpha, +and the existing other-dimension, medium, and Darkness paths. Native +sky sampling and broader effect combinations remain open. --- .../access/IBedrockLightmapState.java | 14 ++++ .../access/IBedrockLightmapWeather.java | 11 +++ @@ -43,7 +55,7 @@ this ordering correction does not establish complete fog parity. .../MixinBedrockRenderingResources.java | 24 ++++++ .../render/MixinBedrockRenderingSession.java | 22 ++++++ .../features/render/MixinBedrockVignette.java | 28 +++++++ - .../render/MixinBedrockVisionFog.java | 58 ++++++++++++++ + .../render/MixinBedrockVisionFog.java | 60 +++++++++++++++ .../light/BedrockLightmapExtractor.java | 66 ++++++++++++++++ .../render/light/BedrockLightmapMath.java | 74 ++++++++++++++++++ .../light/BedrockLightmapParameters.java | 60 +++++++++++++++ @@ -52,7 +64,7 @@ this ordering correction does not establish complete fog parity. .../viafabricplus-bedrock.mixins.json | 10 +++ .../render/light/BedrockLightmapMathTest.java | 77 +++++++++++++++++++ .../light/BedrockLightmapParametersTest.java | 38 +++++++++ - 20 files changed, 772 insertions(+) + 20 files changed, 774 insertions(+) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockLightmapState.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockLightmapWeather.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockLightmap.java @@ -426,10 +438,10 @@ index 0000000..75b1d93 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockVisionFog.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockVisionFog.java new file mode 100644 -index 0000000..86d0279 +index 0000000..3eff7e2 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockVisionFog.java -@@ -0,0 +1,58 @@ +@@ -0,0 +1,60 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -452,39 +464,41 @@ index 0000000..86d0279 +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.material.FogType; +import net.raphimc.viabedrock.protocol.data.enums.Dimension; -+import org.joml.Vector4f; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; -+import org.spongepowered.asm.mixin.injection.Inject; -+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FogRenderer.class) +public abstract class MixinBedrockVisionFog { + + @WrapOperation(method = "computeFogColor", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/fog/environment/FogEnvironment;getModifiedDarkness(Lnet/minecraft/world/entity/LivingEntity;FF)F")) -+ private float deferBlindnessDimming(final FogEnvironment environment, final LivingEntity entity, final float voidDarkness, -+ final float partialTick, final Operation original, -+ @Local(argsOnly = true) final Camera camera, -+ @Share("blindnessVisibility") final LocalRef visibility) { ++ private float captureBlindnessVisibility(final FogEnvironment environment, final LivingEntity entity, final float voidDarkness, ++ final float partialTick, final Operation original, ++ @Local(argsOnly = true) final Camera camera, ++ @Share("blindnessVisibility") final LocalRef visibility) { + final float darkness = original.call(environment, entity, voidDarkness, partialTick); + if (environment instanceof BlindnessFogEnvironment && entity.hasEffect(MobEffects.NIGHT_VISION) + && !entity.hasEffect(MobEffects.DARKNESS) && camera.getFluidInCamera() == FogType.NONE + && BedrockLightmapExtractor.dimension(Minecraft.getInstance()) == Dimension.OVERWORLD) { -+ // Normalizing already dimmed RGB cancels the fade as soon as a channel becomes nonzero. -+ visibility.set(1F - darkness); -+ return 0F; ++ // Native fades night-vision strength as well as the fog RGB, using the same tick-based visibility. ++ final float remaining = 1F - darkness; ++ visibility.set(remaining * remaining); + } + return darkness; + } + -+ @Inject(method = "computeFogColor", at = @At("TAIL")) -+ private void dimAfterNightVision(final CallbackInfo ci, @Local(argsOnly = true) final Vector4f color, -+ @Share("blindnessVisibility") final LocalRef visibility) { -+ final Float remaining = visibility.get(); -+ if (remaining != null) { -+ final float factor = remaining * remaining; -+ color.mul(factor, factor, factor, 1F); -+ } ++ @WrapOperation(method = "computeFogColor", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;nightVisionScale(Lnet/minecraft/world/entity/LivingEntity;F)F")) ++ private float fadeNightVision(final LivingEntity entity, final float partialTick, final Operation original, ++ @Share("blindnessVisibility") final LocalRef visibility) { ++ final float strength = original.call(entity, partialTick); ++ return visibility.get() != null ? strength * visibility.get() : strength; ++ } ++ ++ @WrapOperation(method = "computeFogColor", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 1)) ++ private float boundBlindnessNormalization(final float red, final float greenBlue, final Operation original, ++ @Share("blindnessVisibility") final LocalRef visibility) { ++ final float maximum = original.call(red, greenBlue); ++ // Current native controls cap reciprocal brightness at 255, including the first two daytime expiry steps. ++ return visibility.get() != null ? Math.max(maximum, 1F / 255F) : maximum; + } + +} @@ -823,10 +837,10 @@ index 0000000..2a2ae69 + fragColor = vec4(color, 1.0); +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 5efbac3..722a9a3 100644 +index c2f2431..a02cabd 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -109,6 +109,16 @@ +@@ -110,6 +110,16 @@ "features.render.MixinBedrockNameTags", "features.render.MixinBedrockNameTagMetadata", "features.render.MixinBedrockNameTagRenderer", diff --git a/patches/viafabricplus-bedrock/upstreamable/0014-reproduce-the-native-bedrock-classic-lightmap.pr.md b/patches/viafabricplus-bedrock/upstreamable/0014-reproduce-the-native-bedrock-classic-lightmap.pr.md index 5a334ce5..0c333daf 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0014-reproduce-the-native-bedrock-classic-lightmap.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0014-reproduce-the-native-bedrock-classic-lightmap.pr.md @@ -1,46 +1,31 @@ -Java 26.3 applies a different lightmap gamma transform from Bedrock 1.26.51.1. Its Nether brightness calculation also applies ambient light at a different stage. +# Native classic lighting and vision effects -This patch uses a dedicated shader for supported Bedrock Overworld and Nether sessions. It reproduces the classic native light-image calculation, including its float table, colored block light, sunrise tint, weather factor, quartic gamma, and byte truncation. Nether keeps the native white sky and full sky factor. +## Purpose -Direct Bedrock connections activate the pipeline through their protocol state. ViaProxy sessions activate it through versioned metadata in an accepted converted server pack. Local packs and unsupported metadata cannot activate it. Reload preparation retains the applied context until the replacement resources apply. Local disconnect clears the context after Minecraft removes server packs. A stale asynchronous reload completion cannot restore it. +Use native classic lighting for accepted Bedrock Overworld and Nether sessions on direct and ViaProxy routes. +Remove Java's extra low-light vignette and preserve gradual blindness expiry with night vision. -The patch also omits Java's low-light vignette on supported Bedrock sessions. Native Hive reference surfaces have uniform texture colors. A fit across 18,140 Java white-item pixels explains the extra vignette within 0.33 encoded RGB bytes. This overlay darkened the world and held items toward the screen edges. The fit identifies that overlay. It does not prove complete image parity. +## Design constraints -Immutable render snapshots refresh the lightmap when inputs change between ticks. This covers brightness changes, darkness animation, and transitions between supported and unsupported connections. The shader joins the normal resource reload pipeline. Its uniform buffer closes with its owning lightmap. +Only accepted server metadata or a direct Bedrock connection activates this pipeline. +Keep resource ownership through reload preparation, replacement, and disconnect. Reject stale completions. +Use immutable frame inputs and retire GPU resources with their owning lightmap. -Native evidence comes from the official Bedrock 1.26.51.1 executable, identified by SHA-256 `537c0aee2e79afbdc94b44b28e00f466ae62bc50e2733d953b430db9dbaa9ee7`. The port follows scalar builder `146619c50`, table initializer `142313be0`, Overworld producers `1423122e0`/`14230e620`/`146235f20`, and Nether producer `14692cd30`. Caller `1448018f0` fixes both bias stages on and the white boost off. The native contribution fixes the previous boss value to zero, so the port omits that inactive branch. - -Validation passed eight targeted tests for inputs, snapshot equality, and uniform packing. A private Mesa EGL check compiled the actual fragment shader and rendered all 256 light combinations. All 11,520 pixels across 45 scenarios matched an independent float32 reference byte for byte. Fractional cosine inputs also matched native reference vectors. - -The End retains the existing Java pipeline because its animated native sky producer is incomplete. Darkness uses Java's effect blend lifecycle, whose exact native fade timing remains unverified. Enhanced lighting and resource-pack lighting overrides remain outside this port. The GPU checks establish the calculation for supplied inputs, not complete visual parity with a running native client. - -A saved native Hive session exposed a separate clock error. It sends `SetTime` 12445, then `GameRulesChanged` with `dodaylightcycle=false`. Java previously stored the rule without changing its clock rate, so the scene drifted into night. Core clock patch `0014` now pauses and resumes legacy time at its current value. Explicit `SyncWorldClocks` rates remain independent. A measured night-floor value alone did not establish visual success for this fixed-time lobby. - -Official-client loopback replay now provides a controlled reference without another public join. Verification requires the complete scene payload SHA-256, completed pack reconstruction, local-player initialization, gameplay input, and a reference screenshot. Its `reference-captured` result remains distinct from add-on rendering verification. - -The ViaProxy route uses the accepted server-pack metadata for lighting, but it still lacks native actor and player geometry. Its replay therefore requires `--transport-only`. Actor material and controller lighting controls belong to the separate actor rendering patches. - -Targeted metadata regressions cover accepted server packs, rejected local packs, strict protocol numbers, reload replacement, disconnect, and stale completion. The dependency-ordered build passes. The complete add-on suite passes with all private fixtures enabled after integrating the latest main changes. The full CubeConverter suite passes eight tests, and the core suite passes its clock and renderer regressions with both Checkstyle tasks. Tooling checks and all 50 tooling tests pass. - -Complete loopback replays preserve the original Hive scene payload SHA-256. The direct add-on passes native actor and player rendering assertions. Actual ViaProxy frame observations hold time at 12445 and confirm the native lightmap stays active after the initial pack reload. Static scenery registration validates the color comparison within about one native pixel. Full difference maps still include changing actors, HUD, clouds, and resolution differences. - -The corrected terrain is much closer to the official reference. The sky still differs, and held-item sampling and directional shading remain incomplete. The final Java compass color is explained by its measured block light and quad normal. A brightness gain cannot resolve these separate inputs. Native assets, decompiler output, recordings, and screenshots remain private. - -The 26.60 combined-vision comparison exposes a separate fog-color ordering error. -Java normalizes RGB after blindness dimming, which cancels the fade when a channel first becomes nonzero. -Defer that dimming until after night-vision normalization for supported Overworld air fog. -Reuse the existing darkness factor and preserve alpha. +For Overworld air, retain tick-based blindness dimming and attenuate night-vision strength by the same squared visibility. +Cap reciprocal brightness at 255. Preserve alpha, infinite duration, cancellation, and void darkness. Other dimensions, fog media, and the Darkness effect retain their existing paths. -Five complete native 1.26.60.5 / BDS 1.26.60.6 controls cover natural blindness expiry -with and without night vision in daylight and darkness, plus forced cancellation. -Native restores the sky gradually during natural expiry and immediately after forced cancellation. -Fresh direct Java and ViaProxy controls confirm that the correction removes the bright-fog jump and preserves cancellation. -The original Java recording jumps from black to a blue value of 254 on its first visible fade frame. -Native and corrected Java recordings begin at values from 1 through 3 instead. -Native fog brightness and the precise fade curve still differ, especially at night. -These results establish the ordering correction, not complete native fog parity. +## Validation and remaining limits -The final ViaProxy comparison measures the same 12,039 pixels across three aligned terrain regions. Encoded RGB errors fall from 33.9 to 6.1 on the left masonry, 41.6 to 11.6 on the castle, and 36.1 to 10.0 on the pavement. Their weighted error drops 73.7%. This is a result for those regions, not an overall pixel parity score. Pavement edge alignment is validated locally but lies outside the building feature hull. Sky gradients and clouds remain different. The separate core fog patch corrects inherited air color without claiming a complete sky port. +The native 1.26.51.1 scalar builder supplies the light-image calculation and its input rules. +Private GPU fixtures cover supplied light combinations, gamma, weather, and native float vectors. +Synchronized native 1.26.60.5 Fancy color and distance controls establish 19 expiry steps at noon and midnight. +Native updates remain tick-based. The combined blend creates fractional color values; these do not establish interpolation between ticks. +The verified blend matches every native expiry step within 0.000002, including the gain cap at low brightness. +Blindness alone and the distance curve match the existing Java calculation. +The complete add-on build and its core dependencies pass. +Fresh direct and ViaProxy renderer samples match the native expiry curve within 0.00001 RGBA, including low-brightness gain limits. +The recordings verify cancellation after 20 server ticks and restoration of the pack's ordinary fog range. Both exit normally. -A reload keeps the accepted metadata active during preparation. This prevents a temporary return to Java rendering or physics in ViaProxy sessions. Removal becomes effective when the replacement resources apply. Central local disconnect also clears this metadata when Minecraft removes server packs. Reconfiguration that retains those packs keeps its context. Targeted tests cover retained state during preparation, removal at apply, and stale completion after cleanup. +End light-image inputs, enhanced lighting, resource-pack lighting overrides, other effect combinations, and full sky sampling remain open. +The [owning patch](0014-reproduce-the-native-bedrock-classic-lightmap.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index b77a6143..ee6a8f21 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 0d1e7a40e547044c3d6b6b2191bae16da88c83ee Mon Sep 17 00:00:00 2001 +From 65e075ccca9ef9a6584006ac6725c9e129493bbe 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 @@ -496,8 +496,7 @@ the original pack boundary to distinguish extended vanilla metadata from legacy server textures. Exercise provider, storage admission, conversion, and immutable library reuse together instead of testing only the file filter. Native 1.26.60.5 controls cover mixed-language and legacy packs. -All sixteen current scale-2 labels match after the atlas correction; -scale-1 rasterization remains open. +Matching native scale-1 and scale-2 controls establish atlas sampling. Retain native Unicode advances, bearings and atlas coordinates. Bedrock 1.26.60.5 uses fractional narrow advances, fixed wide ranges and @@ -511,8 +510,8 @@ lazily on the render thread and released when the provider closes. Retain colored pixels and source-cell bounds. Unique texture identities keep retired providers separate from replacement providers during reload. Scope this to converted viabedrock glyph pages; Java fonts and bitmap -layout conversion retain their own paths. Native scale-1 rasterization -remains a separate comparison limit rather than an inferred epsilon fix. +layout conversion retain their own paths. Native capture comparisons +establish rasterization separately from metrics. Use the measured ten-unit bitmap line advance. Label dimensions and line alignment retain rounded widths; only glyph advances remain fractional. @@ -556,6 +555,23 @@ world text and styled glyph metrics with their existing owners. The native captures reproduce 16 current and 16 legacy fixture masks. An offscreen framebuffer regression covers fractional origins at both Java GUI scales, since a default EGL surface hid the edge disagreement. + +Quantize nearest-point GUI bitmap addresses to the native n.8 texel grid, +then fetch the integer texel. Matching 1.26.60.5 native and Java GPU draws +isolate one missing accent pixel to float interpolation landing two steps +below a texel boundary. Integer fetches avoid another normalized-coordinate +rounding step on non-power-of-two atlases. Preserve Unicode's linear path. + +Native post-VS replays retain all 16 current scale-1, current scale-2, and +legacy scale-2 masks on Intel and software rendering. The corrected Java +draw also matches 16/16. A focused GPU regression uses the captured sample, +its exact boundary, and a distinct neighboring texel on 128- and 384-pixel +atlases. It fails before the correction. + +At 1280x720, direct and ViaProxy recordings match all 16 current labels at +native UI scales 1 and 2 and all 16 legacy labels at scale 2, before and +after Java GUI scale changes and resource reloads. Broader viewports and +TrueType comparisons remain separate limits. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../appearance/BedrockBoneAnimation.java | 3 + @@ -627,7 +643,7 @@ Java GUI scales, since a default EGL surface hid the edge disagreement. .../bedrock/settings/NativeUiScale.java | 27 + .../viafabricplus-bedrock/lang/en_us.json | 2 + .../shaders/core/bedrock_actor.fsh | 8 + - .../shaders/core/bedrock_bitmap_font.fsh | 22 + + .../shaders/core/bedrock_bitmap_font.fsh | 26 + .../shaders/core/bedrock_bitmap_font.vsh | 29 + .../viafabricplus-bedrock.accesswidener | 1 + .../viafabricplus-bedrock.mixins.json | 11 +- @@ -640,7 +656,7 @@ Java GUI scales, since a default EGL surface hid the edge disagreement. .../render/BedrockShaderPipelinesTest.java | 24 + .../render/font/BedrockBitmapFontTest.java | 239 +++++ .../render/font/BedrockBitmapGlyphTest.java | 71 ++ - .../font/BedrockBitmapShaderGpuTest.java | 220 ++++ + .../font/BedrockBitmapShaderGpuTest.java | 291 ++++++ .../bedrock/screen/ui/AnimationPrograms.java | 23 + .../screen/ui/NativeHudOverlayStateTest.java | 153 +++ .../screen/ui/NativeUiActorCameraTest.java | 102 ++ @@ -669,7 +685,7 @@ Java GUI scales, since a default EGL surface hid the edge disagreement. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ - 112 files changed, 12477 insertions(+), 36 deletions(-) + 112 files changed, 12552 insertions(+), 36 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 @@ -8339,10 +8355,10 @@ index 88f77b0..2628d69 100644 } diff --git a/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_bitmap_font.fsh b/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_bitmap_font.fsh new file mode 100644 -index 0000000..a83b21c +index 0000000..caf1dcd --- /dev/null +++ b/src/main/resources/assets/viafabricplus-bedrock/shaders/core/bedrock_bitmap_font.fsh -@@ -0,0 +1,22 @@ +@@ -0,0 +1,26 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : require +#include @@ -8358,10 +8374,14 @@ index 0000000..a83b21c + // Preserve the unshifted sample position while selecting native top-edge coverage. + vec2 sourceUv = glyphUv - dFdy(glyphUv) * sign(dFdy(glyphGuiY)) / 256.0; + vec2 uv = clamp(sourceUv, glyphBounds.xy, glyphBounds.zw); -+ vec4 color = texture(Sampler0, uv); +#ifdef BITMAP_SMOOTH ++ vec4 color = texture(Sampler0, uv); + color = clamp((color - 0.3) * 5.000000476837158, 0.0, 1.0); + color = color * color * (3.0 - 2.0 * color); ++#else ++ // Preserve native point addresses when float interpolation lands just below a texel boundary. ++ vec2 texels = roundEven(uv * vec2(textureSize(Sampler0, 0)) * 256.0) / 256.0; ++ vec4 color = texelFetch(Sampler0, ivec2(floor(texels)), 0); +#endif + fragColor = color * vertexColor * ColorModulator; +} @@ -9248,10 +9268,10 @@ index 0000000..f6651d3 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapShaderGpuTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapShaderGpuTest.java new file mode 100644 -index 0000000..17577c8 +index 0000000..0e8ffc2 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapShaderGpuTest.java -@@ -0,0 +1,220 @@ +@@ -0,0 +1,291 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -9262,9 +9282,11 @@ index 0000000..17577c8 +import com.viaversion.viafabricplus.bedrock.render.EglTestContext; +import java.io.IOException; +import java.nio.charset.StandardCharsets; ++import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.lwjgl.system.MemoryStack; ++import org.lwjgl.system.MemoryUtil; + +import static org.junit.jupiter.api.Assertions.*; +import static org.lwjgl.opengl.GL33C.*; @@ -9439,9 +9461,78 @@ index 0000000..17577c8 + }); + } + ++ @Test ++ void retainsTheNativeAccentPixelAtAPointSampleBoundary() { ++ EglTestContext.run(() -> { ++ final String vertex = """ ++ #version 330 ++ #extension GL_ARB_separate_shader_objects : require ++ uniform vec2 SampleUv; ++ layout(location = 0) out vec4 vertexColor; ++ layout(location = 1) out vec2 glyphUv; ++ layout(location = 2) flat out vec4 glyphBounds; ++ layout(location = 3) noperspective out float glyphGuiY; ++ void main() { ++ vec2 corner = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2); ++ gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0); ++ vertexColor = vec4(1.0); ++ glyphUv = SampleUv; ++ glyphBounds = vec4(0.0, 0.0, 1.0, 1.0); ++ glyphGuiY = 0.0; ++ } ++ """; ++ final int program = program(false, false, vertex), vao = glGenVertexArrays(), atlas = glGenTextures(); ++ final var texels = MemoryUtil.memCalloc(384 * 384 * 4); ++ try (var stack = MemoryStack.stackPush()) { ++ glBindTexture(GL_TEXTURE_2D, atlas); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); ++ glBindVertexArray(vao); ++ glUseProgram(program); ++ glUniform1i(glGetUniformLocation(program, "Sampler0"), 0); ++ glUniform4f(glGetUniformLocation(program, "ColorModulator"), 1, 1, 1, 1); ++ glViewport(0, 0, 1, 1); ++ glDisable(GL_BLEND); ++ glDisable(GL_DEPTH_TEST); ++ glDisable(GL_CULL_FACE); ++ record Sample(float v, int channel) { } ++ final var pixel = stack.malloc(4); ++ for (final int size : new int[] {128, 384}) { ++ texels.clear(); ++ texels.limit(size * size * 4); ++ texels.putInt((3 * size + 2) * 4, -1); ++ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels); ++ // Captured Java coordinate at the missing À pixel, its boundary, and a distinct texel. ++ final var samples = List.of(new Sample(0.0234374962747097F * 128F / size, 255), ++ new Sample(3F / size, 255), new Sample((3F - 1F / 256F) / size, 0)); ++ for (final Sample sample : samples) { ++ glUniform2f(glGetUniformLocation(program, "SampleUv"), 2F / size, sample.v()); ++ glDrawArrays(GL_TRIANGLES, 0, 3); ++ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); ++ for (int channel = 0; channel < 4; channel++) { ++ assertEquals(sample.channel(), Byte.toUnsignedInt(pixel.get(channel)), ++ "Atlas size " + size + ", texture V=" + sample.v()); ++ } ++ } ++ texels.putInt((3 * size + 2) * 4, 0); ++ } ++ } finally { ++ MemoryUtil.memFree(texels); ++ glUseProgram(0); ++ glDeleteTextures(atlas); ++ glDeleteVertexArrays(vao); ++ glDeleteProgram(program); ++ } ++ }); ++ } ++ + private static int program(final boolean smooth, final boolean feedback) { ++ return program(smooth, feedback, resource("vsh", smooth)); ++ } ++ ++ private static int program(final boolean smooth, final boolean feedback, final String vertexSource) { + final int program = glCreateProgram(); -+ final int vertex = shader(GL_VERTEX_SHADER, resource("vsh", smooth)); ++ final int vertex = shader(GL_VERTEX_SHADER, vertexSource); + final int fragment = shader(GL_FRAGMENT_SHADER, resource("fsh", smooth)); + try { + glAttachShader(program, vertex); diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index 8587ac6b..9fc53ecd 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -18,6 +18,8 @@ Prerequisites: Regression coverage includes `BedrockBitmapFontResourcesTest`, `BedrockGuiActorAppearanceTest`, `BedrockGuiActorContextTest`, `BedrockResourceLibraryTest`. -Current-font controls match 15/16 labels at native UI scale 1 and 16/16 at scale 2. Legacy scale-2 controls match 16/16 on both routes. One scale-1 pixel, broader viewports, and TrueType comparisons remain open. +At 1280 × 720, all 16 current-font labels match native 26.60 at UI scales 1 and 2. All 16 legacy labels match at UI scale 2. Direct and ViaProxy controls pass Java GUI scale changes and resource reloads. The point-sampling GPU regression fails before the correction and passes on Intel and software rendering. + +Broader viewports and TrueType comparisons remain open. The [owning patch](0029-share-native-ui-scene-rendering.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch b/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch index d6489803..0ac11c5e 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch @@ -1,11 +1,11 @@ -From d487f15066a5a0f2be59a545d4ae879c8502f2be Mon Sep 17 00:00:00 2001 +From 408ccb75f8215d4e355ad759d367be45117239f9 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:28:02 +0100 Subject: [PATCH] feat(fog): apply native biome and command settings Accept resolved fog snapshots through a connection-owned receiver. Select the camera's received biome key from the snapshot, below command settings -and above the existing core default policy. Apply fixed and render-relative +and above pack-defined data defaults. Apply fixed and render-relative air distances and colors through the atmospheric pipeline, preserving later camera, vision and void modifiers and existing fluid/weather paths. @@ -25,28 +25,36 @@ upload during client ticks before render passes. Dispatch disconnect GPU retirement to the client executor and check listener ownership inside the task so an old disconnect cannot clear a replacement session. +Modulate Overworld fog by the current world clock before later vision +modifiers. The raw pack RGB override previously skipped native night +dimming. Native 1.26.60.5 Fancy controls with #204060 establish midnight +RGB (0.0075294, 0.0150588, 0.0338824), then (2/9, 4/9, 1) with night vision. +The old direct renderer instead returned raw RGB, then (1/3, 2/3, 1). +Daylight and twilight vectors protect the clock phase and channel factors; +other dimensions keep their existing color path. + Focused regressions cover resource/session retirement, empty commands, medium boundaries and the observed dragon cap. Spatial biome blending, -legacy default bindings, engine-specific vanilla rules, exact transitions, +directional sunrise tint, engine-specific vanilla rules, exact transitions, End sky motion/sampling, fluids and weather still need native comparisons. BREAKING CHANGE: The fog receiver requires the matching core payload version 2. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + - .../appearance/BedrockEndSkyTexture.java | 96 +++++++++++++++ + .../appearance/BedrockEndSkyTexture.java | 96 ++++++++++++++ .../features/render/MixinBedrockEndSky.java | 32 +++++ .../render/MixinBedrockEndSkyPipeline.java | 25 ++++ - .../features/render/MixinBedrockFog.java | 43 +++++++ + .../features/render/MixinBedrockFog.java | 44 +++++++ .../render/fog/BedrockEndSkyPipeline.java | 38 ++++++ - .../bedrock/render/fog/BedrockFog.java | 111 ++++++++++++++++++ + .../bedrock/render/fog/BedrockFog.java | 124 ++++++++++++++++++ .../bedrock/render/fog/FogPayload.java | 24 ++++ - .../bedrock/render/fog/FogSession.java | 33 ++++++ + .../bedrock/render/fog/FogSession.java | 33 +++++ .../shaders/core/bedrock_end_sky.fsh | 12 ++ - .../shaders/core/bedrock_end_sky.vsh | 14 +++ + .../shaders/core/bedrock_end_sky.vsh | 14 ++ .../viafabricplus-bedrock.mixins.json | 3 + - .../bedrock/render/fog/BedrockFogTest.java | 58 +++++++++ - .../bedrock/render/fog/FogSessionTest.java | 66 +++++++++++ - 14 files changed, 557 insertions(+) + .../bedrock/render/fog/BedrockFogTest.java | 77 +++++++++++ + .../bedrock/render/fog/FogSessionTest.java | 66 ++++++++++ + 14 files changed, 590 insertions(+) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockEndSkyTexture.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockEndSky.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockEndSkyPipeline.java @@ -61,10 +69,10 @@ BREAKING CHANGE: The fog receiver requires the matching core payload version 2. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/fog/FogSessionTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 88cf6fe..775fe5f 100644 +index 90dadd2..bf38b8a 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -@@ -59,6 +59,7 @@ import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; +@@ -60,6 +60,7 @@ import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; import com.viaversion.viafabricplus.bedrock.input.InputPermissionsPayload; import com.viaversion.viafabricplus.bedrock.hud.HudVisibilityPayload; import com.viaversion.viafabricplus.bedrock.camera.CameraFadePayload; @@ -72,7 +80,7 @@ index 88cf6fe..775fe5f 100644 import com.viaversion.viafabricplus.bedrock.camera.BedrockCameraFade; import com.viaversion.viafabricplus.bedrock.camera.BedrockCameraFov; import com.viaversion.viafabricplus.bedrock.camera.CameraFovPayload; -@@ -221,6 +222,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { +@@ -223,6 +224,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { ClientPlayConnectionEvents.INIT.register((listener, client) -> ClientPlayNetworking.registerReceiver(LocatorBarPayload.TYPE, (payload, context) -> BedrockLocatorBar.accept(listener.getConnection(), payload.snapshot()))); @@ -253,10 +261,10 @@ index 0000000..4a06cb8 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockFog.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockFog.java new file mode 100644 -index 0000000..4b9c3cf +index 0000000..55e86fd --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockFog.java -@@ -0,0 +1,43 @@ +@@ -0,0 +1,44 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -267,14 +275,15 @@ index 0000000..4b9c3cf +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.viaversion.viafabricplus.bedrock.render.fog.BedrockFog; ++import com.viaversion.viafabricplus.bedrock.render.light.BedrockLightmapExtractor; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; ++import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.fog.FogData; +import net.minecraft.client.renderer.fog.FogRenderer; +import net.minecraft.client.renderer.fog.environment.AtmosphericFogEnvironment; +import net.minecraft.client.renderer.fog.environment.FogEnvironment; -+import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; @@ -288,7 +297,7 @@ index 0000000..4b9c3cf + final var air = BedrockFog.air(camera, level); + if (air == null) return color; + // Preserve the later void, blindness, darkness, night-vision and camera-effect modifiers. -+ return new Vector3f((air.color() >> 16 & 255) / 255F, (air.color() >> 8 & 255) / 255F, (air.color() & 255) / 255F); ++ return BedrockFog.color(air.color(), BedrockLightmapExtractor.dimension(Minecraft.getInstance()), level.getOverworldClockTime()); + } + + @WrapOperation(method = "setupFog", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/fog/environment/FogEnvironment;setupFog(Lnet/minecraft/client/renderer/fog/FogData;Lnet/minecraft/client/Camera;Lnet/minecraft/client/multiplayer/ClientLevel;FLnet/minecraft/client/DeltaTracker;)V")) @@ -346,10 +355,10 @@ index 0000000..09d62f3 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/fog/BedrockFog.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/fog/BedrockFog.java new file mode 100644 -index 0000000..43e1a96 +index 0000000..729644b --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/fog/BedrockFog.java -@@ -0,0 +1,111 @@ +@@ -0,0 +1,124 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -358,6 +367,7 @@ index 0000000..43e1a96 +package com.viaversion.viafabricplus.bedrock.render.fog; + +import com.viaversion.viafabricplus.bedrock.render.light.BedrockLightmapExtractor; ++import com.viaversion.viafabricplus.bedrock.render.light.BedrockLightmapMath; +import com.viaversion.viafabricplus.bedrock.appearance.BedrockEndSkyTexture; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; @@ -368,11 +378,13 @@ index 0000000..43e1a96 +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.renderer.fog.FogData; ++import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.level.material.FogType; +import net.raphimc.viabedrock.api.model.FogState; +import net.raphimc.viabedrock.protocol.data.enums.Dimension; +import org.jetbrains.annotations.Nullable; ++import org.joml.Vector3f; + +public final class BedrockFog { + private static final FogSession SESSION = new FogSession(); @@ -428,6 +440,16 @@ index 0000000..43e1a96 + return dimension != null && medium == FogType.NONE && rain <= 0 && thunder <= 0; + } + ++ public static Vector3f color(final int color, final Dimension dimension, final long time) { ++ final Vector3f rgb = new Vector3f((color >> 16 & 255) / 255F, (color >> 8 & 255) / 255F, (color & 255) / 255F); ++ if (dimension == Dimension.OVERWORLD) { ++ final float angle = BedrockLightmapMath.overworldAngle(time, 0F); ++ final float daylight = Mth.clamp((float) Math.cos(angle * ((float) Math.PI * 2F)) * 2F + 0.5F, 0F, 1F); ++ rgb.mul(daylight * 0.94F + 0.06F, daylight * 0.94F + 0.06F, daylight * 0.91F + 0.09F); ++ } ++ return rgb; ++ } ++ + public static void applyDistance(final FogData fog, final Camera camera, final ClientLevel level, final float renderDistance) { + final Dimension dimension = BedrockLightmapExtractor.dimension(Minecraft.getInstance()); + if (!active(dimension, camera, level)) return; @@ -569,10 +591,10 @@ index 0000000..afd7b44 + texCoord0 = UV0; +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 13433c2..7529378 100644 +index 4a42549..f6bc0fa 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -139,6 +139,9 @@ +@@ -140,6 +140,9 @@ "features.render.MixinBedrockLightmap", "features.render.MixinBedrockLightmapPipelines", "features.render.MixinBedrockVisionFog", @@ -584,10 +606,10 @@ index 13433c2..7529378 100644 "features.render.MixinEntityRenderDispatcher", diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/fog/BedrockFogTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/fog/BedrockFogTest.java new file mode 100644 -index 0000000..e147fca +index 0000000..cee16d4 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/fog/BedrockFogTest.java -@@ -0,0 +1,58 @@ +@@ -0,0 +1,77 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -605,6 +627,25 @@ index 0000000..e147fca + +class BedrockFogTest { + @Test ++ void overworldFogUsesNativeDayNightModulationBeforeVisionEffects() { ++ final var day = BedrockFog.color(0x204060, Dimension.OVERWORLD, 6000); ++ assertEquals(0.1254902035F, day.x(), 0.000001F); ++ assertEquals(0.2509804070F, day.y(), 0.000001F); ++ assertEquals(0.3764707744F, day.z(), 0.000001F); ++ final var night = BedrockFog.color(0x204060, Dimension.OVERWORLD, 18000); ++ assertEquals(0.0075294119F, night.x(), 0.000001F); ++ assertEquals(0.0150588239F, night.y(), 0.000001F); ++ assertEquals(0.0338823721F, night.z(), 0.000001F); ++ final var dusk = BedrockFog.color(0x204060, Dimension.OVERWORLD, 13000); ++ assertEquals(0.0522189364F, dusk.x(), 0.00001F); ++ assertEquals(0.1044378728F, dusk.y(), 0.00001F); ++ assertEquals(0.1636722237F, dusk.z(), 0.00001F); ++ for (final Dimension dimension : new Dimension[]{Dimension.NETHER, Dimension.END}) { ++ assertEquals(day, BedrockFog.color(0x204060, dimension, 18000)); ++ } ++ } ++ ++ @Test + void dryAirCommandsDoNotReplaceUnverifiedFluidOrWeatherDistanceRules() { + for (final Dimension dimension : Dimension.values()) { + assertTrue(BedrockFog.supported(dimension, FogType.NONE, 0, 0)); diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.pr.md b/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.pr.md index d4791ede..e47b74bc 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.pr.md @@ -1,47 +1,29 @@ # Native biome and command fog in dry air -Consume the core's world-resolved `viabedrock:fog` version-2 snapshots through a connection-owned Fabric receiver. -Use the received biome key at the camera to select settings below command layers and above the existing registry fallback. -Empty command updates reveal base resources. -Listener replacement and disconnect retire the full snapshot, and earlier generations cannot restore it. -Core, proxy, and client distributions require the matching payload contract. - -Apply resolved air colors before the existing vision, void, and camera modifiers. -Apply fixed or render-relative distances through the atmospheric fog buffer. -Fluid, weather, and status-effect distances retain their current paths until matching native controls establish their rules. -The current lookup selects the camera biome; native spatial blending and legacy data-default bindings remain open. - -A living admitted Ender Dragon uses start 0 and end `min(100, effective render distance)`, with or without commands. -Boss-bar visibility does not own that lifetime. -Matching native 1.26.60.5 `RenderChunk` instructions and live uniform reads establish 100 blocks at effective distance 192 and 80 at distance 80. -Native captures retain the override after an admitted actor moves far away; death restores ordinary fog. -A fresh distant connection without admission honors the ordinary distance. -These controls do not establish a universal admission radius or exact transition curve. - -The matching native `EndSky` fragment shader multiplies a grayscale texture by `SkyColor`. -Live white/red controls read approximately twice the fog RGB at that binding, with alpha 2. -Use the licensed world-selected texture below accepted server overrides for both command and base fog. -Decode off-thread and upload during client ticks before GPU render passes. -Retire GPU resources on the client executor and check listener ownership inside the scheduled task. -This fixes the Netty-thread deletion assertion observed on both routes during shutdown. - -Native 1.26.60.5 and BDS 1.26.60.6 use protocol 2225 for these controls. -A qualified `minecraft:the_end` definition binds a unique red fog identifier in the private fixture. -Without commands or living dragons, native uniforms measure red fog from 0 to 32 blocks. -The test pack matches every hashed file from that reference. -The old Java result was red registry metadata with 1,024-block distance and a purple sky. - -Fresh direct and ViaProxy recordings each verify seven corresponding phases with actual renderer buffers. -Base fog uses 32 blocks and red RGB; a water-only command preserves it. -A white command uses 16 blocks, and removal restores the red base. -An empty command stack retains that base. -A living dragon uses 100 blocks without changing red; death restores 32 blocks. -All fourteen captures retain the same camera position, rotation, and `minecraft:the_end` biome key. -The End sky is red without a fog command. -Both 180-second recordings finish normally, without logged errors or GPU cleanup failures. -Floor lighting still differs, and the captures do not establish native pixel parity. - -The full add-on build passes 1,300 cases, including 145 optional skips, with no failures or errors. -Focused tests cover generation rejection, resource replacement, command removal, fluid/weather boundaries, and the measured dragon policy. -Native biome-boundary blending, legacy default bindings, vanilla engine-specific fog rules, exact transitions, sky motion/sampling, and broader End lighting remain open. -This patch does not establish full fog parity. +## Purpose + +Apply world-resolved air colors and distances through the matching version-2 core fog payload. +Commands take priority over the camera biome and pack-defined data defaults. Empty commands retain base resources. + +## Design constraints + +Keep snapshots and End sky textures with their connection. Reject callbacks from retired generations. +Apply Overworld clock modulation before the existing vision, void, and camera modifiers. +Retain the current fluid, weather, and status-effect distance paths. + +An admitted living Ender Dragon caps distance at 100 blocks or the effective render distance. +Death restores ordinary settings. Boss bars do not own this lifetime. +Use the licensed world-selected End sky texture beneath server overrides. Retire GPU resources on the client executor. + +## Validation and remaining limits + +Native 1.26.60.5 / BDS 1.26.60.6 controls establish base fog, command priority, dragon lifetime, and sky color multiplication. +Prior direct and ViaProxy controls cover these paths and shutdown. +Native Fancy controls also expose missing night modulation for custom fog colors. +`BedrockFogTest` checks day, midnight, and twilight vectors, plus dimension boundaries and dragon policy. +Focused tests and the complete four-project build pass. +Fresh direct and ViaProxy renderer samples match native day, midnight, night-vision, and fixed twilight RGBA within 0.00001. +Both recordings exit normally. + +Directional sunrise tint, spatial biome blending, timed command transitions, other status effects, End sky sampling, fluids, and weather remain open. +The [owning patch](0039-apply-native-command-stacks-to-dry-air.patch) retains implementation rationale and native evidence.