From 52537c3681df68abee4943ddd1b2efbf36606ca7 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 21:46:20 +0000 Subject: [PATCH 01/12] Use slabs on gentle surface terraces --- CHANGELOG.md | 4 ++ README.md | 8 +-- gradle.properties | 2 +- .../probe/TransitionProbe.java | 50 ++++++++++++++++-- .../SkysTerrainSmoother.java | 2 +- .../internal/SmoothingEngine.java | 51 ++++++++++++++----- .../ProjectContractTest.java | 2 +- 7 files changed, 97 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e457ca8..9c39276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.1.1.121012 + +- Gentle surface terraces use full-width slabs at smoothing levels 2 and 3, giving each rise a wider tread. + ## 0.1.0.121012 - Updated for Minecraft 1.21.1 on NeoForge. diff --git a/README.md b/README.md index f590e22..48d3ff3 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,10 @@ Set `worldgen.smoothingLevel` in `config/skysterrainsmoother-common.toml`: 3. **Slabs, steps and corners** - also uses pieces covering a quarter of a slab's footprint. -Every smoothing piece is half a block tall. If a material doesn't have steps -or corners, it uses its slab at higher levels wherever a slab fits safely. +Every smoothing piece is half a block tall. Gentle surface terraces use a +full-width slab when there is room on both sides of a one-block rise. If a +material doesn't have steps or corners, it uses its slab at higher levels +wherever a slab fits safely. Narrow mountain ledges keep their downhill edge at its original height, rather than gaining a row of slabs that recreates a full-block step. Wider terraces @@ -51,7 +53,7 @@ For **Minecraft 1.21.1**, use **NeoForge 21.1.247** and **Java 21**, with: - Sky's Grass Slabs **1.2.0.121012**. - Sky's Building Pieces **0.3.9.121012**. -- Sky's Terrain Smoother **0.1.0.121012**. +- Sky's Terrain Smoother **0.1.1.121012**. Install all three NeoForge jars on both the client and server. If you use the BOP add-on, keep its matching dependencies installed too. diff --git a/gradle.properties b/gradle.properties index e5ccc0e..1e9809a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ release_enabled=true curseforge_project_id=1768953 mod_name=Sky's Terrain Smoother mod_license=LGPL-2.1-only -mod_version=0.1.0.121012 +mod_version=0.1.1.121012 mod_group=zone.moddev.mc.skysterrainsmoother mod_authors=SkyBlade1978 mod_description=Smooths new Overworld surfaces with existing slabs, steps and corners. Optional Mineralogy support. diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java index bae4a45..55622fc 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java @@ -38,6 +38,12 @@ static int run(ServerLevel world) { BlockPos upperCap = origin.relative(uphill).above(2); RuntimeProbe.require(MaterialCatalogue.instance().piece(world.getBlockState(lowCap)) == material, "the foot of a steep slope still receives its matching transition"); + BlockState step = material.step(direction(uphill)); + if (level >= 2 && step != null) { + RuntimeProbe.require(world.getBlockState(lowCap).equals(step), + "the foot of consecutive rises keeps its uphill step"); + checks++; + } requireOpenDownhillEdge(world, upperCap, uphill.getOpposite(), "consecutive mountain ledges, level " + level + " / " + uphill); RuntimeProbe.require(world.getChunkSource().getLoadedChunksCount() == loadedBefore, @@ -48,9 +54,11 @@ static int run(ServerLevel world) { prepare(world, origin, uphill, source, true, roofed); SkysTerrainSmoother.engine.smooth(world, owner, level); - RuntimeProbe.require(MaterialCatalogue.instance() - .piece(world.getBlockState(origin.relative(uphill).above())) == material, - "the bottom of a wider terrace is smoothed"); + BlockPos terraceCap = origin.relative(uphill).above(); + BlockState expected = roofed && level >= 2 && step != null ? step : material.slab; + RuntimeProbe.require(world.getBlockState(terraceCap).equals(expected), + "gentle surface terraces use a slab and cave terraces keep their existing shape: " + + source + " / " + uphill + " / level " + level + " / roofed " + roofed); RuntimeProbe.require(MaterialCatalogue.instance() .piece(world.getBlockState(origin.relative(uphill, 3).above(2))) == material, "a wider upper terrace retains room for its own transition"); @@ -75,7 +83,7 @@ static int run(ServerLevel world) { checks += 2; } - checks += borders(world) + lateLowerFloor(world, origin); + checks += borders(world) + gentleBorders(world) + lateLowerFloor(world, origin); zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_TRANSITIONS_PASS checks=" + checks); return checks; } @@ -117,6 +125,40 @@ private static int borders(ServerLevel world) { return checks; } + private static int gentleBorders(ServerLevel world) { + TerrainMaterial material = MaterialCatalogue.instance().source(Blocks.STONE.defaultBlockState()); + int checks = 0; + for (Direction uphill : new Direction[] { Direction.EAST, Direction.SOUTH }) { + BlockPos origin = uphill == Direction.EAST ? new BlockPos(3614, 210, 3656) + : new BlockPos(3656, 210, 3614); + BlockPos low = origin.relative(uphill); + LevelChunk lowerOwner = world.getChunkAt(low); + LevelChunk upperOwner = world.getChunkAt(low.relative(uphill)); + for (boolean reverse : new boolean[] { false, true }) { + prepare(world, origin, uphill, Blocks.STONE.defaultBlockState(), true, false); + int loadedBefore = world.getChunkSource().getLoadedChunksCount(); + SkysTerrainSmoother.engine.smooth(world, reverse ? upperOwner : lowerOwner, 3); + SkysTerrainSmoother.engine.smooth(world, reverse ? lowerOwner : upperOwner, 3); + RuntimeProbe.require(world.getBlockState(low.above()).equals(material.slab), + "the same gentle slab fits in both chunk orders: " + uphill); + RuntimeProbe.require(world.getChunkSource().getLoadedChunksCount() == loadedBefore, + "terrace lookahead never loads a chunk"); + checks += 2; + } + } + return checks; + } + + private static int direction(Direction uphill) { + return switch (uphill) { + case NORTH -> 0; + case EAST -> 1; + case SOUTH -> 2; + case WEST -> 3; + default -> throw new IllegalArgumentException("Expected horizontal uphill direction"); + }; + } + private static int lateLowerFloor(ServerLevel world, BlockPos origin) { LevelChunk owner = world.getChunkAt(origin); int checks = 0; diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/SkysTerrainSmoother.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/SkysTerrainSmoother.java index 90cf37f..0514563 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/SkysTerrainSmoother.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/SkysTerrainSmoother.java @@ -24,7 +24,7 @@ public final class SkysTerrainSmoother { public static final String ID = "skysterrainsmoother"; public static final String NAME = "Sky's Terrain Smoother"; - public static final String VERSION = "0.1.0.121012"; + public static final String VERSION = "0.1.1.121012"; public static final Logger logger = LogManager.getLogger(); public static final SmoothingEngine engine = new SmoothingEngine(MaterialCatalogue.instance()); public static boolean enabled = true; diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java index 59980eb..67cb7eb 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java @@ -250,8 +250,8 @@ private boolean validate(LevelAccessor world, int chunkX, int chunkZ, List= 2 && lowerEdges == 0 && result != material.slab && Integer.bitCount(cardinal) == 1) { + int uphill = Integer.numberOfTrailingZeros(cardinal); + if (safeMask == Footprint.EDGES[uphill]) { + // A full slab gives a broader tread where both sides of a one-block rise have room. + int dx = DX[uphill]; + int dz = DZ[uphill]; + if (hasTerrainFloor(world, owner, support.offset(-dx, 0, -dz)) + && hasTerrainFloor(world, owner, support.offset(2 * dx, 1, 2 * dz))) { + return material.slab; + } + } + } + + return result; } private static BlockState supportAfterPlacement(TerrainMaterial material, BlockState piece) { @@ -592,14 +615,19 @@ private boolean higher(LevelAccessor world, ChunkAccess owner, int localX, int l int worldX = (owner.getPos().x << 4) + localX; int worldZ = (owner.getPos().z << 4) + localZ; - ChunkAccess neighbour = loaded(world, worldX >> 4, worldZ >> 4); + return hasTerrainFloor(world, owner, new BlockPos(worldX, lowerY + 1, worldZ)); + } + private boolean hasTerrainFloor(LevelAccessor world, ChunkAccess owner, BlockPos floor) { + int chunkX = floor.getX() >> 4; + int chunkZ = floor.getZ() >> 4; + ChunkAccess neighbour = chunkX == owner.getPos().x && chunkZ == owner.getPos().z + ? owner : loaded(world, chunkX, chunkZ); if (neighbour == null) { return false; } - // Compare logical floors at this height, including existing caps over BOP's native soil. - BlockPos floor = new BlockPos(worldX, lowerY + 1, worldZ); + // Read the same logical height before and after a neighbouring column is capped. BlockState state = neighbour.getBlockState(floor); BlockState above = neighbour.getBlockState(floor.above()); @@ -607,8 +635,7 @@ private boolean higher(LevelAccessor world, ChunkAccess owner, int localX, int l TerrainMaterial cap = catalogue.piece(above); if (cap != null) { - return source != null || catalogue.source(state) == cap - || cap.supportAfterPlacement != null && state.equals(cap.supportAfterPlacement) + return source != null || cap.supportAfterPlacement != null && state.equals(cap.supportAfterPlacement) || oreSupport(state); } diff --git a/src/test/java/zone/moddev/mc/skysterrainsmoother/ProjectContractTest.java b/src/test/java/zone/moddev/mc/skysterrainsmoother/ProjectContractTest.java index 5479eb9..616850a 100644 --- a/src/test/java/zone/moddev/mc/skysterrainsmoother/ProjectContractTest.java +++ b/src/test/java/zone/moddev/mc/skysterrainsmoother/ProjectContractTest.java @@ -15,7 +15,7 @@ class ProjectContractTest { void targetMetadataAndToolchainMatchTheNativeRuntime() throws Exception { Properties metadata = new Properties(); metadata.load(new java.io.StringReader(text(Paths.get("gradle.properties")))); - assertEquals("0.1.0.121012", metadata.getProperty("mod_version")); + assertEquals("0.1.1.121012", metadata.getProperty("mod_version")); assertEquals("1.21.1", metadata.getProperty("minecraft_version")); assertEquals("21.1.247", metadata.getProperty("neo_version")); assertEquals("21.0.7+6", metadata.getProperty("java_toolchain_version")); From 4c3f9368f04f5d91aad78d5573ad4f3333d2540b Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 21:59:58 +0000 Subject: [PATCH 02/12] Match generated grass pieces to nearby snow --- CHANGELOG.md | 1 + README.md | 6 +- .../probe/FineGrassProbe.java | 62 +++++++++++++++++++ .../internal/SmoothingEngine.java | 53 +++++++++++++--- 4 files changed, 112 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c39276..9df4821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.1.1.121012 - Gentle surface terraces use full-width slabs at smoothing levels 2 and 3, giving each rise a wider tread. +- Grass slabs, steps and corners take on their snowy look as the surrounding terrain generates. ## 0.1.0.121012 diff --git a/README.md b/README.md index 48d3ff3..7a5210d 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,10 @@ than gaining a row of slabs that recreates a full-block step. Wider terraces still get transitions. At levels 2 and 3, the engine keeps a fitting uphill step or corner even when a whole slab would raise the downhill edge. -A single layer of snow doesn't block an otherwise valid transition. Deeper -snow, trees, crops and occupied blocks stay untouched. +A single layer of snow doesn't block an otherwise valid transition. Grass +slabs, steps and corners take on their snowy look alongside nearby snow as +the terrain generates. Deeper snow, trees, crops and occupied blocks stay +untouched. Only new, dry terrain is smoothed. Existing chunks, underwater floors and cave ceilings stay unchanged. Cave passages need three clear blocks above the floor, diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FineGrassProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FineGrassProbe.java index e2f41bf..df8b1d6 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FineGrassProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FineGrassProbe.java @@ -50,10 +50,72 @@ static int run(ServerLevel world) { checks += checkSupport(world, floor, corner, false, true); } checks += shrinkingSlab(world, material); + checks += snowyTransitions(world, material); zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_FINE_GRASS_SUPPORT_PASS checks=" + checks); return checks; } + private static int snowyTransitions(ServerLevel world, TerrainMaterial material) { + BlockPos floor = new BlockPos(4207, 210, 4200); + BlockPos target = floor.above(); + BlockPos lowerSnow = target.east(); + LevelChunk owner = world.getChunkAt(floor); + BlockState snow = Blocks.SNOW.defaultBlockState(); + int checks = 0; + + for (int level = 1; level <= 3; level++) { + BlockPos higher = level == 3 ? target.north().west() : target.north(); + BlockState dry = switch (level) { + case 1 -> material.slab; + case 2 -> material.step(0); + default -> material.corner(0); + }; + BlockState snowy = dry.setValue(GrassBlock.SNOWY, true); + + for (boolean lateSnow : new boolean[] { false, true }) { + prepare(world, floor); + world.setBlock(higher, Blocks.GRASS_BLOCK.defaultBlockState(), 2); + world.setBlock(higher.above(), snow, 2); + LevelChunk snowChunk = world.getChunkAt(lowerSnow); + if (!lateSnow) { + snowChunk.setBlockState(lowerSnow, snow, false); + } + + int loaded = world.getChunkSource().getLoadedChunksCount(); + SkysTerrainSmoother.engine.smooth(world, owner, level); + RuntimeProbe.require(world.getBlockState(target).equals(lateSnow ? dry : snowy), + "generated grass has the right snow appearance before ticking at level " + level + + " / snow placed later: " + lateSnow); + + if (lateSnow) { + // Match decoration writes that haven't notified the piece of its new neighbour. + snowChunk.setBlockState(lowerSnow, snow, false); + owner.setBlockState(floor, Blocks.DIAMOND_ORE.defaultBlockState(), false); + RuntimeProbe.require(world.getBlockState(target).equals(dry), + "late snow test starts with the generated piece still green"); + ProbeWorld.repair(world, floor); + RuntimeProbe.require(world.getBlockState(target).equals(snowy), + "late decoration gives the same piece its snow cap at level " + level); + RuntimeProbe.require(world.getBlockState(floor).is(Blocks.DIAMOND_ORE), + "refreshing the snow appearance preserves an ore placed under the piece"); + checks += 3; + } + + RuntimeProbe.require(world.getBlockState(lowerSnow).equals(snow) + && world.getBlockState(higher.above()).equals(snow), + "the grass cap uses the snow around it without removing either layer"); + RuntimeProbe.require(world.getChunkSource().getLoadedChunksCount() == loaded, + "snow appearance reads never load chunks"); + world.setBlock(lowerSnow, Blocks.AIR.defaultBlockState(), 3); + RuntimeProbe.require(world.getBlockState(target).equals(dry), + "removing the adjacent snow restores the green piece at level " + level); + checks += 4; + } + } + SkysTerrainSmoother.logger.info("TERRAIN_SNOWY_TRANSITIONS_PASS checks={}", checks); + return checks; + } + private static int checkSupport(ServerLevel world, BlockPos floor, BlockState piece, boolean fullCover, boolean snow) { RuntimeProbe.require(world.getBlockState(floor).getBlock() == (fullCover ? Blocks.DIRT : Blocks.GRASS_BLOCK), diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java index 67cb7eb..0369db0 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java @@ -296,14 +296,25 @@ private boolean validate(LevelAccessor world, int chunkX, int chunkZ, List placement.state.getBlock() == Blocks.AIR); // Biome decoration can introduce new grass oases as well as beaches. Re-snapshot registered dry floors only in // this tracked fresh owner; occupied targets and existing saved chunks remain untouched. int additional = smooth(world, owner, SkysTerrainSmoother.level, true); + + // Vanilla can add snow after our vegetation pass. Refresh the appearance once the terrain repairs are done. + for (FeatureDecorator placement : placements) { + if (placement.material.grass && placement.state.hasProperty(SnowyDirtBlock.SNOWY)) { + BlockState current = owner.getBlockState(placement.pos); + if (sameState(current, placement.state)) { + BlockState snowy = withSnowAppearance(world, owner, placement.pos, current); + changed |= setGenerationState(world, owner, placement.pos, snowy); + } + } + } + if (changed) { + owner.setUnsaved(true); + } + synchronized (this) { placed += additional; } @@ -407,6 +418,9 @@ private int smooth(LevelAccessor world, ChunkAccess owner, int level, boolean af cardinal, diagonal, level, cave); if (result != null) { + if (material.grass && result.hasProperty(SnowyDirtBlock.SNOWY)) { + result = withSnowAppearance(world, owner, target, result); + } buffer.add(localBlockX | localBlockZ << 4 | (floorY + 1) << 8, result, material, cave, above); } } @@ -471,6 +485,26 @@ private static boolean setGenerationState(LevelAccessor world, ChunkAccess owner return sameState(actual, state); } + private static BlockState withSnowAppearance(LevelAccessor world, ChunkAccess owner, BlockPos position, + BlockState state) { + // Use the same snow neighbours as the pieces' normal appearance updates. + if (isSnow(owner.getBlockState(position.above()))) { + return state.setValue(SnowyDirtBlock.SNOWY, true); + } + for (Direction side : Direction.Plane.HORIZONTAL) { + BlockPos neighbour = position.relative(side); + ChunkAccess chunk = ownerOrLoaded(world, owner, neighbour); + if (chunk != null && isSnow(chunk.getBlockState(neighbour))) { + return state.setValue(SnowyDirtBlock.SNOWY, true); + } + } + return state.setValue(SnowyDirtBlock.SNOWY, false); + } + + private static boolean isSnow(BlockState state) { + return state.is(Blocks.SNOW) || state.is(Blocks.SNOW_BLOCK); + } + private static boolean sameState(BlockState first, BlockState second) { // Nearby snow can update the visual property between decoration callbacks. // Geometry, material and water state must still match our original cap. @@ -619,10 +653,7 @@ private boolean higher(LevelAccessor world, ChunkAccess owner, int localX, int l } private boolean hasTerrainFloor(LevelAccessor world, ChunkAccess owner, BlockPos floor) { - int chunkX = floor.getX() >> 4; - int chunkZ = floor.getZ() >> 4; - ChunkAccess neighbour = chunkX == owner.getPos().x && chunkZ == owner.getPos().z - ? owner : loaded(world, chunkX, chunkZ); + ChunkAccess neighbour = ownerOrLoaded(world, owner, floor); if (neighbour == null) { return false; } @@ -649,6 +680,12 @@ private static boolean isOverworld(LevelAccessor world) { return world instanceof WorldGenLevel && ((WorldGenLevel) world).getLevel().dimension() == Level.OVERWORLD; } + private static ChunkAccess ownerOrLoaded(LevelAccessor world, ChunkAccess owner, BlockPos position) { + int chunkX = position.getX() >> 4; + int chunkZ = position.getZ() >> 4; + return chunkX == owner.getPos().x && chunkZ == owner.getPos().z ? owner : loaded(world, chunkX, chunkZ); + } + /** A missing neighbour is unavailable, never a reason to request or generate another chunk. */ private static ChunkAccess loaded(LevelAccessor world, int chunkX, int chunkZ) { if (world instanceof WorldGenRegion) { From 7d0e5c89fd1ed6fe26f179db54cc69eb8f74c3cc Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 10:35:02 +0000 Subject: [PATCH 03/12] Smooth snow terrain and match raised snow cover --- CHANGELOG.md | 2 + README.md | 6 +- gradle/content-dependencies.properties | 16 +- .../probe/FineGrassProbe.java | 14 +- .../probe/RuntimeProbe.java | 2 +- .../probe/SnowTerrainProbe.java | 81 ++++ .../probe/TransitionProbe.java | 3 +- .../resources/fixtures/reported_snow_gap.json | 422 ++++++++++++++++++ .../internal/BuiltInMaterials.java | 1 + .../internal/SmoothingEngine.java | 5 +- .../resources/META-INF/neoforge.mods.toml | 4 +- 11 files changed, 537 insertions(+), 19 deletions(-) create mode 100644 src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java create mode 100644 src/integrationTest/resources/fixtures/reported_snow_gap.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 9df4821..e202cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 0.1.1.121012 +- Smooth full snow-block terrain with matching snow slabs, steps and corners. +- Keep grass transitions snowy beside higher snow-covered ground. - Gentle surface terraces use full-width slabs at smoothing levels 2 and 3, giving each rise a wider tread. - Grass slabs, steps and corners take on their snowy look as the surrounding terrain generates. diff --git a/README.md b/README.md index 7a5210d..86e8036 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Sky's Terrain Smoother adds smaller transitions to new Overworld terrain, including cave floors. It uses slabs, steps and corners from Sky's Grass Slabs and Sky's Building Pieces, with matching materials for grass, dirt, stone, -sandstone, sand and hardened clay. +sandstone, sand, hardened clay and full snow blocks. ## Smoothing levels @@ -53,8 +53,8 @@ For BOP grass and soils, add the For **Minecraft 1.21.1**, use **NeoForge 21.1.247** and **Java 21**, with: -- Sky's Grass Slabs **1.2.0.121012**. -- Sky's Building Pieces **0.3.9.121012**. +- Sky's Grass Slabs **1.2.1.121012**. +- Sky's Building Pieces **0.3.10.121012**. - Sky's Terrain Smoother **0.1.1.121012**. Install all three NeoForge jars on both the client and server. If you use diff --git a/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index ffc56dd..07fe3e9 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -1,8 +1,8 @@ -grass.version=1.2.0.121012 -grass.commit=6061fa264f3d4e710fde7279ffe6d3958fc98929 -grass.sha256=F095DD84AF9BFB6F90A9C3A091B526508C9516D3B76E0162ED67B470004322F8 -grass.development.sha256=F095DD84AF9BFB6F90A9C3A091B526508C9516D3B76E0162ED67B470004322F8 -pieces.version=0.3.9.121012 -pieces.commit=98a8c72378829b7815527015c8aa124718beea04 -pieces.sha256=6E992F1A5CC79235C4FBD3E8EFCC2A865E147B793D2C7D20D6EF1815630F3105 -pieces.development.sha256=6E992F1A5CC79235C4FBD3E8EFCC2A865E147B793D2C7D20D6EF1815630F3105 +grass.version=1.2.1.121012 +grass.commit=6e56a72be3621a1d6b8c3f5c806abd008224104a +grass.sha256=70267E6B4BDB378E798C146E6C2E094800D0FDE764F2DF073B071B91DBC8117D +grass.development.sha256=70267E6B4BDB378E798C146E6C2E094800D0FDE764F2DF073B071B91DBC8117D +pieces.version=0.3.10.121012 +pieces.commit=4557f56dafcefec378ba0b79a3e4ca2e1b97c2d6 +pieces.sha256=7948D86CA1567DD50D69F05C279363EC6DF812BBDFA233F22DE923FC1637677D +pieces.development.sha256=7948D86CA1567DD50D69F05C279363EC6DF812BBDFA233F22DE923FC1637677D diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FineGrassProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FineGrassProbe.java index df8b1d6..29a20d8 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FineGrassProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FineGrassProbe.java @@ -75,9 +75,9 @@ private static int snowyTransitions(ServerLevel world, TerrainMaterial material) for (boolean lateSnow : new boolean[] { false, true }) { prepare(world, floor); world.setBlock(higher, Blocks.GRASS_BLOCK.defaultBlockState(), 2); - world.setBlock(higher.above(), snow, 2); LevelChunk snowChunk = world.getChunkAt(lowerSnow); if (!lateSnow) { + world.setBlock(higher.above(), snow, 2); snowChunk.setBlockState(lowerSnow, snow, false); } @@ -90,6 +90,9 @@ private static int snowyTransitions(ServerLevel world, TerrainMaterial material) if (lateSnow) { // Match decoration writes that haven't notified the piece of its new neighbour. snowChunk.setBlockState(lowerSnow, snow, false); + world.getChunkAt(higher).setBlockState(higher.above(), snow, false); + world.getChunkAt(higher).setBlockState(higher, + Blocks.GRASS_BLOCK.defaultBlockState().setValue(GrassBlock.SNOWY, true), false); owner.setBlockState(floor, Blocks.DIAMOND_ORE.defaultBlockState(), false); RuntimeProbe.require(world.getBlockState(target).equals(dry), "late snow test starts with the generated piece still green"); @@ -107,8 +110,15 @@ private static int snowyTransitions(ServerLevel world, TerrainMaterial material) RuntimeProbe.require(world.getChunkSource().getLoadedChunksCount() == loaded, "snow appearance reads never load chunks"); world.setBlock(lowerSnow, Blocks.AIR.defaultBlockState(), 3); + if (level != 3) { + RuntimeProbe.require(world.getBlockState(target).getValue(GrassBlock.SNOWY), + "the higher terrace still supplies snow after the lower snow is removed"); + checks++; + } + world.setBlock(higher.above(), Blocks.AIR.defaultBlockState(), 3); + world.getBlockState(target).randomTick(world, target, RandomSource.create(1)); RuntimeProbe.require(world.getBlockState(target).equals(dry), - "removing the adjacent snow restores the green piece at level " + level); + "removing the surrounding snow restores the green piece at level " + level); checks += 4; } } diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java index b245537..fa60672 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java @@ -198,7 +198,7 @@ public void tick(ServerTickEvent.Post event) { require(SkysTerrainSmoother.level == Integer.getInteger("skysterrainsmoother.expectedLevel", 3), "The native configuration selected the requested generation level"); } else { - gameplay = MineralogyProbe.run() + FineGrassProbe.run(world) + OpportunityProbe.run(world) + gameplay = SnowTerrainProbe.run(world) + MineralogyProbe.run() + FineGrassProbe.run(world) + OpportunityProbe.run(world) + TransitionProbe.run(world) + FoliageProbe.run(world) + persistence(world) + LateTerrainProbe.run(world) + CaveProbe.run(world) + decorationChanges(world) + geometry(world) + grass(world) + sand(world) + generationOrder(world) diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java new file mode 100644 index 0000000..e4a54e5 --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java @@ -0,0 +1,81 @@ +package zone.moddev.mc.skysterrainsmoother.probe; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SnowyDirtBlock; +import net.minecraft.world.level.block.state.BlockState; +import zone.moddev.mc.skysbuildingpieces.api.BuildingPiecesApi; +import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; +import zone.moddev.mc.skysterrainsmoother.internal.MaterialCatalogue; + +/** Covers the raised snow neighbours from the reported world and their normal block updates. */ +final class SnowTerrainProbe { + static int run(ServerLevel world) throws Exception { + BlockPos target = new BlockPos(4680, 210, 4680); + try (var input = SnowTerrainProbe.class.getResourceAsStream("/fixtures/reported_snow_gap.json")) { + if (input == null) { + throw new IllegalStateException("The reported snow fixture is missing"); + } + JsonObject fixture = JsonParser.parseReader(new InputStreamReader(input, StandardCharsets.UTF_8)) + .getAsJsonObject(); + for (JsonElement entry : fixture.getAsJsonArray("blocks")) { + JsonObject block = entry.getAsJsonObject(); + var offset = block.getAsJsonArray("offset"); + BlockPos position = target.offset(offset.get(0).getAsInt(), offset.get(1).getAsInt(), + offset.get(2).getAsInt()); + JsonObject state = block.getAsJsonObject("state"); + CompoundTag tag = new CompoundTag(); + tag.putString("Name", state.get("Name").getAsString()); + if (state.has("Properties")) { + CompoundTag properties = new CompoundTag(); + state.getAsJsonObject("Properties").entrySet().forEach(property -> + properties.putString(property.getKey(), property.getValue().getAsString())); + tag.put("Properties", properties); + } + world.setBlock(position, NbtUtils.readBlockState(world.holderLookup(Registries.BLOCK), tag), 2); + } + } + ProbeWorld.light(world, LightLayer.SKY, target, 15); + BlockState slab = world.getBlockState(target); + int loaded = world.getChunkSource().getLoadedChunksCount(); + int checks = 0; + for (BlockState dry : new BlockState[] { slab.setValue(SnowyDirtBlock.SNOWY, false), + BuildingPiecesApi.bottomPiece("minecraft:grass", BuildingPiecesApi.TerrainShape.STEP, Direction.NORTH), + BuildingPiecesApi.bottomPiece("minecraft:grass", BuildingPiecesApi.TerrainShape.CORNER, Direction.NORTH) }) { + world.setBlock(target.below(), Blocks.GRASS_BLOCK.defaultBlockState(), 2); + world.setBlock(target, dry, 2); + BlockState snowy = dry.updateShape(Direction.SOUTH, world.getBlockState(target.south()), world, + target, target.south()); + RuntimeProbe.require(snowy.getValue(SnowyDirtBlock.SNOWY), + "a raised snow-covered neighbour gives the reported piece its snow cap: " + dry); + world.getChunkAt(target).setBlockState(target, dry, false); + dry.randomTick(world, target, RandomSource.create(1)); + RuntimeProbe.require(world.getBlockState(target).getValue(SnowyDirtBlock.SNOWY), + "normal random ticks keep the snow cap beside the raised snow: " + dry); + checks += 2; + } + RuntimeProbe.require(world.getChunkSource().getLoadedChunksCount() == loaded, + "snow appearance does not load chunks"); + RuntimeProbe.require(MaterialCatalogue.instance().source(Blocks.SNOW_BLOCK.defaultBlockState()) != null, + "full snow blocks have a registered smoothing material"); + RuntimeProbe.require(MaterialCatalogue.instance().source(Blocks.POWDER_SNOW.defaultBlockState()) == null, + "powder snow keeps its native behaviour"); + SkysTerrainSmoother.logger.info("TERRAIN_SNOW_TERRAIN_PASS checks={}", checks + 3); + return checks + 3; + } + + private SnowTerrainProbe() { + } +} diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java index 55622fc..f20d472 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java @@ -20,7 +20,8 @@ static int run(ServerLevel world) { int checks = 0; for (BlockState source : new BlockState[] { Blocks.STONE.defaultBlockState(), - Blocks.SANDSTONE.defaultBlockState(), Blocks.SAND.defaultBlockState(), Blocks.GRASS_BLOCK.defaultBlockState() }) { + Blocks.SANDSTONE.defaultBlockState(), Blocks.SAND.defaultBlockState(), Blocks.GRASS_BLOCK.defaultBlockState(), + Blocks.SNOW_BLOCK.defaultBlockState() }) { if (source.getBlock() == Blocks.GRASS_BLOCK && !GrassSlabsApi.grassGenerationAllowed()) { continue; } diff --git a/src/integrationTest/resources/fixtures/reported_snow_gap.json b/src/integrationTest/resources/fixtures/reported_snow_gap.json new file mode 100644 index 0000000..b86581e --- /dev/null +++ b/src/integrationTest/resources/fixtures/reported_snow_gap.json @@ -0,0 +1,422 @@ +{ + "origin": [ + 7876, + 63, + -3745 + ], + "blocks": [ + { + "offset": [ + -1, + -1, + -1 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + -1, + 0, + -1 + ], + "state": { + "Name": "skysgrassslabs:grass_slab", + "Properties": { + "waterlogged": "false", + "type": "bottom", + "snowy": "false" + } + } + }, + { + "offset": [ + -1, + 1, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + -1, + 2, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + -1, + -1, + 0 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + -1, + 0, + 0 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "true" + } + } + }, + { + "offset": [ + -1, + 1, + 0 + ], + "state": { + "Name": "minecraft:snow", + "Properties": { + "layers": "1" + } + } + }, + { + "offset": [ + -1, + 2, + 0 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + -1, + -1, + 1 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + -1, + 0, + 1 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "true" + } + } + }, + { + "offset": [ + -1, + 1, + 1 + ], + "state": { + "Name": "minecraft:snow", + "Properties": { + "layers": "1" + } + } + }, + { + "offset": [ + -1, + 2, + 1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 0, + -1, + -1 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "false" + } + } + }, + { + "offset": [ + 0, + 0, + -1 + ], + "state": { + "Name": "skysbuildingpieces:grass_corner", + "Properties": { + "orientation": "bottom_south_west", + "waterlogged": "false", + "snowy": "true" + } + } + }, + { + "offset": [ + 0, + 1, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 0, + 2, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 0, + -1, + 0 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + 0, + 0, + 0 + ], + "state": { + "Name": "skysgrassslabs:grass_slab", + "Properties": { + "waterlogged": "false", + "type": "bottom", + "snowy": "false" + } + } + }, + { + "offset": [ + 0, + 1, + 0 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 0, + 2, + 0 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 0, + -1, + 1 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + 0, + 0, + 1 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "true" + } + } + }, + { + "offset": [ + 0, + 1, + 1 + ], + "state": { + "Name": "minecraft:snow", + "Properties": { + "layers": "1" + } + } + }, + { + "offset": [ + 0, + 2, + 1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 1, + -1, + -1 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "true" + } + } + }, + { + "offset": [ + 1, + 0, + -1 + ], + "state": { + "Name": "minecraft:snow", + "Properties": { + "layers": "1" + } + } + }, + { + "offset": [ + 1, + 1, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 1, + 2, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 1, + -1, + 0 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + 1, + 0, + 0 + ], + "state": { + "Name": "skysgrassslabs:grass_slab", + "Properties": { + "waterlogged": "false", + "type": "bottom", + "snowy": "true" + } + } + }, + { + "offset": [ + 1, + 1, + 0 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 1, + 2, + 0 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 1, + -1, + 1 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + 1, + 0, + 1 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "true" + } + } + }, + { + "offset": [ + 1, + 1, + 1 + ], + "state": { + "Name": "minecraft:snow", + "Properties": { + "layers": "1" + } + } + }, + { + "offset": [ + 1, + 2, + 1 + ], + "state": { + "Name": "minecraft:air" + } + } + ] +} diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/BuiltInMaterials.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/BuiltInMaterials.java index 166ac9d..d4eacdb 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/BuiltInMaterials.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/BuiltInMaterials.java @@ -52,6 +52,7 @@ public static synchronized void ensureRegistered() { vanilla("sandstone", Blocks.SANDSTONE.defaultBlockState(), false, null); vanilla("red_sandstone", Blocks.RED_SANDSTONE.defaultBlockState(), false, null); vanilla("hardened_clay", Blocks.TERRACOTTA.defaultBlockState(), false, null); + vanilla("snow", Blocks.SNOW_BLOCK.defaultBlockState(), false, null); for (int variant = 0; variant < 2; variant++) { Block sand = variant == 0 ? Blocks.SAND : Blocks.RED_SAND; MaterialCatalogue.instance().register(new TerrainMaterial( diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java index 0369db0..cd6b910 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java @@ -494,7 +494,8 @@ private static BlockState withSnowAppearance(LevelAccessor world, ChunkAccess ow for (Direction side : Direction.Plane.HORIZONTAL) { BlockPos neighbour = position.relative(side); ChunkAccess chunk = ownerOrLoaded(world, owner, neighbour); - if (chunk != null && isSnow(chunk.getBlockState(neighbour))) { + if (chunk != null && (isSnow(chunk.getBlockState(neighbour)) + || fullCollision(chunk.getBlockState(neighbour)) && isSnow(chunk.getBlockState(neighbour.above())))) { return state.setValue(SnowyDirtBlock.SNOWY, true); } } @@ -502,7 +503,7 @@ private static BlockState withSnowAppearance(LevelAccessor world, ChunkAccess ow } private static boolean isSnow(BlockState state) { - return state.is(Blocks.SNOW) || state.is(Blocks.SNOW_BLOCK); + return state.is(BlockTags.SNOW); } private static boolean sameState(BlockState first, BlockState second) { diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index 8fd265a..e971bce 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -25,12 +25,12 @@ side="BOTH" [[dependencies.${mod_id}]] modId="skysgrassslabs" type="required" -versionRange="[1.2.0.121012,1.3)" +versionRange="[1.2.1.121012,1.3)" ordering="AFTER" side="BOTH" [[dependencies.${mod_id}]] modId="skysbuildingpieces" type="required" -versionRange="[0.3.8.121012,0.4)" +versionRange="[0.3.10.121012,0.4)" ordering="AFTER" side="BOTH" From 7aa1b930421012810a1e20b59ee6c3527e6e104e Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 10:56:58 +0000 Subject: [PATCH 04/12] Snow exposed cold transitions without a full supporting face --- README.md | 6 +- gradle/content-dependencies.properties | 12 +-- .../probe/LateTerrainProbe.java | 1 + .../skysterrainsmoother/probe/ProbeWorld.java | 13 ++++ .../probe/SnowTerrainProbe.java | 73 +++++++++++++++++++ .../internal/SmoothingEngine.java | 7 +- 6 files changed, 102 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 86e8036..f219da4 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ still get transitions. At levels 2 and 3, the engine keeps a fitting uphill step or corner even when a whole slab would raise the downhill edge. A single layer of snow doesn't block an otherwise valid transition. Grass -slabs, steps and corners take on their snowy look alongside nearby snow as -the terrain generates. Deeper snow, trees, crops and occupied blocks stay -untouched. +slabs, steps and corners take on their snowy look alongside nearby snow and +on exposed cold ground as the terrain generates. Deeper snow, trees, crops +and occupied blocks stay untouched. Only new, dry terrain is smoothed. Existing chunks, underwater floors and cave ceilings stay unchanged. Cave passages need three clear blocks above the floor, diff --git a/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index 07fe3e9..ed9dc66 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -1,8 +1,8 @@ grass.version=1.2.1.121012 -grass.commit=6e56a72be3621a1d6b8c3f5c806abd008224104a -grass.sha256=70267E6B4BDB378E798C146E6C2E094800D0FDE764F2DF073B071B91DBC8117D -grass.development.sha256=70267E6B4BDB378E798C146E6C2E094800D0FDE764F2DF073B071B91DBC8117D +grass.commit=58d98da6136069d1097b40103b3656412b75fa73 +grass.sha256=844B51DFB6380EF6E70F773FD3704A954E5682117341C408414FE6F95EDB4915 +grass.development.sha256=844B51DFB6380EF6E70F773FD3704A954E5682117341C408414FE6F95EDB4915 pieces.version=0.3.10.121012 -pieces.commit=4557f56dafcefec378ba0b79a3e4ca2e1b97c2d6 -pieces.sha256=7948D86CA1567DD50D69F05C279363EC6DF812BBDFA233F22DE923FC1637677D -pieces.development.sha256=7948D86CA1567DD50D69F05C279363EC6DF812BBDFA233F22DE923FC1637677D +pieces.commit=3cd96588bd0183753b9e704a69ecfbec1aea0c32 +pieces.sha256=86F186AB2CBDDD75864B4DD4D3EE7BACD9DCD2C0A131A56A94843B50F4857242 +pieces.development.sha256=86F186AB2CBDDD75864B4DD4D3EE7BACD9DCD2C0A131A56A94843B50F4857242 diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/LateTerrainProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/LateTerrainProbe.java index f44a92d..98f1072 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/LateTerrainProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/LateTerrainProbe.java @@ -155,6 +155,7 @@ static int generationOrder(ServerLevel world) throws Exception { for (int order = 0; order < 2; order++) { int origin = order == 0 ? 128 : 256; + ProbeWorld.coldBiome(world, origin, 128, 9); List chunks = new ArrayList<>(); for (int blockZ = 0; blockZ < 9; blockZ++) { diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ProbeWorld.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ProbeWorld.java index ca268c6..e633f8c 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ProbeWorld.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ProbeWorld.java @@ -55,6 +55,19 @@ static void rebuildHeights(net.minecraft.world.level.chunk.ChunkAccess chunk) { java.util.EnumSet.allOf(net.minecraft.world.level.levelgen.Heightmap.Types.class)); } + static void coldBiome(Level world, int firstChunkX, int firstChunkZ, int width) { + var biome = world.registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.BIOME) + .getHolderOrThrow(net.minecraft.world.level.biome.Biomes.SNOWY_PLAINS); + var serverWorld = (net.minecraft.server.level.ServerLevel) world; + var sampler = serverWorld.getChunkSource().randomState().sampler(); + // Both traversal fixtures need the same climate, including the biome lookup's boundary halo. + for (int chunkZ = firstChunkZ - 1; chunkZ <= firstChunkZ + width; chunkZ++) { + for (int chunkX = firstChunkX - 1; chunkX <= firstChunkX + width; chunkX++) { + world.getChunk(chunkX, chunkZ).fillBiomesFromNoise((quartX, quartY, quartZ, climate) -> biome, sampler); + } + } + } + static boolean oreSupport(BlockState state) { if (state.hasBlockEntity() || state.liquid()) { return false; diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java index e4a54e5..a21b55e 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java @@ -16,6 +16,7 @@ import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.SnowyDirtBlock; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.biome.Biomes; import zone.moddev.mc.skysbuildingpieces.api.BuildingPiecesApi; import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; import zone.moddev.mc.skysterrainsmoother.internal.MaterialCatalogue; @@ -72,10 +73,82 @@ static int run(ServerLevel world) throws Exception { "full snow blocks have a registered smoothing material"); RuntimeProbe.require(MaterialCatalogue.instance().source(Blocks.POWDER_SNOW.defaultBlockState()) == null, "powder snow keeps its native behaviour"); + checks += coldSurfaces(world); SkysTerrainSmoother.logger.info("TERRAIN_SNOW_TERRAIN_PASS checks={}", checks + 3); return checks + 3; } + private static int coldSurfaces(ServerLevel world) { + BlockPos floor = new BlockPos(4744, 210, 4744); + BlockPos target = floor.above(); + var owner = world.getChunkAt(floor); + var originalBiomes = new java.util.HashMap>(); + int firstQuartX = owner.getPos().x * 4; + int firstQuartZ = owner.getPos().z * 4; + for (int quartY = world.getMinBuildHeight() >> 2; quartY < world.getMaxBuildHeight() >> 2; quartY++) { + for (int quartZ = firstQuartZ; quartZ < firstQuartZ + 4; quartZ++) { + for (int quartX = firstQuartX; quartX < firstQuartX + 4; quartX++) { + originalBiomes.put(new BlockPos(quartX, quartY, quartZ), owner.getNoiseBiome(quartX, quartY, quartZ)); + } + } + } + var sampler = world.getChunkSource().randomState().sampler(); + var registry = world.registryAccess().registryOrThrow(Registries.BIOME); + var cold = registry.getHolderOrThrow(Biomes.SNOWY_PLAINS); + var warm = registry.getHolderOrThrow(Biomes.DESERT); + int checks = 0; + try { + owner.fillBiomesFromNoise((quartX, quartY, quartZ, climate) -> cold, sampler); + for (int level = 1; level <= 3; level++) { + for (int offsetZ = -3; offsetZ <= 3; offsetZ++) { + for (int offsetX = -3; offsetX <= 3; offsetX++) { + BlockPos column = floor.offset(offsetX, 0, offsetZ); + for (int height = 1; column.getY() + height < world.getMaxBuildHeight(); height++) { + ProbeWorld.clear(world, column.above(height)); + } + world.setBlock(column, Blocks.GRASS_BLOCK.defaultBlockState(), 2); + world.setBlock(column.below(), Blocks.STONE.defaultBlockState(), 2); + } + } + world.setBlock(level == 3 ? target.north().west() : target.north(), + Blocks.GRASS_BLOCK.defaultBlockState(), 2); + SkysTerrainSmoother.engine.smooth(world, owner, level); + BlockState piece = world.getBlockState(target); + RuntimeProbe.require(piece.hasProperty(SnowyDirtBlock.SNOWY) + && piece.getValue(SnowyDirtBlock.SNOWY), + "exposed cold transitions receive snow without a supported snow layer at level " + level); + piece.randomTick(world, target, RandomSource.create(1)); + RuntimeProbe.require(world.getBlockState(target).getValue(SnowyDirtBlock.SNOWY), + "normal random ticks retain the exposed cold snow cap"); + + world.setBlock(target.above(3), Blocks.STONE.defaultBlockState(), 2); + piece.randomTick(world, target, RandomSource.create(1)); + RuntimeProbe.require(!world.getBlockState(target).getValue(SnowyDirtBlock.SNOWY), + "sheltered pieces keep their green appearance"); + ProbeWorld.clear(world, target.above(3)); + owner.fillBiomesFromNoise((quartX, quartY, quartZ, climate) -> warm, sampler); + world.getBlockState(target).randomTick(world, target, RandomSource.create(1)); + RuntimeProbe.require(!world.getBlockState(target).getValue(SnowyDirtBlock.SNOWY), + "exposed warm surfaces keep their green appearance"); + owner.fillBiomesFromNoise((quartX, quartY, quartZ, climate) -> cold, sampler); + world.setBlock(target.east(), Blocks.GLOWSTONE.defaultBlockState(), 2); + ProbeWorld.light(world, LightLayer.BLOCK, target, 15); + RuntimeProbe.require(world.getBrightness(LightLayer.BLOCK, target.above()) >= 10, + "the lighting fixture is bright enough to prevent snowfall"); + world.getBlockState(target).randomTick(world, target, RandomSource.create(1)); + RuntimeProbe.require(!world.getBlockState(target).getValue(SnowyDirtBlock.SNOWY), + "bright block light keeps the cold surface green"); + ProbeWorld.clear(world, target.east()); + ProbeWorld.light(world, LightLayer.BLOCK, target, 0); + checks += 6; + } + } finally { + owner.fillBiomesFromNoise((quartX, quartY, quartZ, climate) -> + originalBiomes.get(new BlockPos(quartX, quartY, quartZ)), sampler); + } + return checks; + } + private SnowTerrainProbe() { } } diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java index cd6b910..d719a5e 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java @@ -16,6 +16,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.LightLayer; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.server.level.ServerChunkCache; @@ -499,7 +500,11 @@ private static BlockState withSnowAppearance(LevelAccessor world, ChunkAccess ow return state.setValue(SnowyDirtBlock.SNOWY, true); } } - return state.setValue(SnowyDirtBlock.SNOWY, false); + boolean exposedColdSurface = world.getBiome(position).value().coldEnoughToSnow(position) + && position.getY() >= SmoothingFoliage.surfaceHeight(owner, position.getX() & 15, + position.getZ() & 15, owner.getPos().x << 4, owner.getPos().z << 4) + && world.getBrightness(LightLayer.BLOCK, position.above()) < 10; + return state.setValue(SnowyDirtBlock.SNOWY, exposedColdSurface); } private static boolean isSnow(BlockState state) { From 927a58ff49e327b7a1cd08f3d890e821038607d3 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 12:01:04 +0000 Subject: [PATCH 05/12] Use the audited grass and building pieces fixes --- gradle/content-dependencies.properties | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index ed9dc66..5ed3a35 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -1,8 +1,8 @@ grass.version=1.2.1.121012 -grass.commit=58d98da6136069d1097b40103b3656412b75fa73 -grass.sha256=844B51DFB6380EF6E70F773FD3704A954E5682117341C408414FE6F95EDB4915 -grass.development.sha256=844B51DFB6380EF6E70F773FD3704A954E5682117341C408414FE6F95EDB4915 +grass.commit=34dc9266c885985419361d86f91f588fbef3d8a9 +grass.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B +grass.development.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B pieces.version=0.3.10.121012 -pieces.commit=3cd96588bd0183753b9e704a69ecfbec1aea0c32 -pieces.sha256=86F186AB2CBDDD75864B4DD4D3EE7BACD9DCD2C0A131A56A94843B50F4857242 -pieces.development.sha256=86F186AB2CBDDD75864B4DD4D3EE7BACD9DCD2C0A131A56A94843B50F4857242 +pieces.commit=a3b2ee023eabc3a02cc854e0ed398839e6f9c14b +pieces.sha256=1837B314F8E419794C5E9F236109ED6E0F595EF65B9CA055F6D9DB9AF265F3FE +pieces.development.sha256=1837B314F8E419794C5E9F236109ED6E0F595EF65B9CA055F6D9DB9AF265F3FE From 9b10316bb53ad5ff796f27a02c64bec26849e36a Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 12:05:28 +0000 Subject: [PATCH 06/12] Pin the shared native survival checks --- gradle/content-dependencies.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index 5ed3a35..0a10b02 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -3,6 +3,6 @@ grass.commit=34dc9266c885985419361d86f91f588fbef3d8a9 grass.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B grass.development.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B pieces.version=0.3.10.121012 -pieces.commit=a3b2ee023eabc3a02cc854e0ed398839e6f9c14b +pieces.commit=9fcf412daaad7e1f41d97e863d4860db3df07901 pieces.sha256=1837B314F8E419794C5E9F236109ED6E0F595EF65B9CA055F6D9DB9AF265F3FE pieces.development.sha256=1837B314F8E419794C5E9F236109ED6E0F595EF65B9CA055F6D9DB9AF265F3FE From 6b1b2c13adfbbb194de0274b10674f25eb2bd0e1 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 12:55:01 +0000 Subject: [PATCH 07/12] Pin the corrected piece recipes --- gradle/content-dependencies.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index 0a10b02..ca6fe96 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -3,6 +3,6 @@ grass.commit=34dc9266c885985419361d86f91f588fbef3d8a9 grass.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B grass.development.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B pieces.version=0.3.10.121012 -pieces.commit=9fcf412daaad7e1f41d97e863d4860db3df07901 -pieces.sha256=1837B314F8E419794C5E9F236109ED6E0F595EF65B9CA055F6D9DB9AF265F3FE -pieces.development.sha256=1837B314F8E419794C5E9F236109ED6E0F595EF65B9CA055F6D9DB9AF265F3FE +pieces.commit=6583abd559095d780dc4f5fe81cb082b4c4284cd +pieces.sha256=A8245453273898B8A991BB205656A1F437BB4614C1C1F42ED18D5FBF436B6477 +pieces.development.sha256=A8245453273898B8A991BB205656A1F437BB4614C1C1F42ED18D5FBF436B6477 From eda027fdc587db66fba81e64cfabae907b2f3cef Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 13:01:23 +0000 Subject: [PATCH 08/12] Use the shared recipe unlock fix --- gradle/content-dependencies.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index ca6fe96..d2c6aa7 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -3,6 +3,6 @@ grass.commit=34dc9266c885985419361d86f91f588fbef3d8a9 grass.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B grass.development.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B pieces.version=0.3.10.121012 -pieces.commit=6583abd559095d780dc4f5fe81cb082b4c4284cd -pieces.sha256=A8245453273898B8A991BB205656A1F437BB4614C1C1F42ED18D5FBF436B6477 -pieces.development.sha256=A8245453273898B8A991BB205656A1F437BB4614C1C1F42ED18D5FBF436B6477 +pieces.commit=591a8defcc60c2334a4235332c23df42fd07f2c2 +pieces.sha256=D9FF6B4B428DD2FF431E30DD079D24F83DE44A365BF38EE915B6F7C50C849197 +pieces.development.sha256=D9FF6B4B428DD2FF431E30DD079D24F83DE44A365BF38EE915B6F7C50C849197 From 3c91ee8b348b5a4ff7ecbee63a2cad9f443a9099 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 14:14:52 +0000 Subject: [PATCH 09/12] Pin the snow-covered support rendering --- gradle/content-dependencies.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index d2c6aa7..cbf420c 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -3,6 +3,6 @@ grass.commit=34dc9266c885985419361d86f91f588fbef3d8a9 grass.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B grass.development.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B pieces.version=0.3.10.121012 -pieces.commit=591a8defcc60c2334a4235332c23df42fd07f2c2 -pieces.sha256=D9FF6B4B428DD2FF431E30DD079D24F83DE44A365BF38EE915B6F7C50C849197 -pieces.development.sha256=D9FF6B4B428DD2FF431E30DD079D24F83DE44A365BF38EE915B6F7C50C849197 +pieces.commit=157f3bd7db4e9afecc252998f1d07be23715d7bd +pieces.sha256=C9FB776B051A6A06170163F5665A35DDC8B00192FE12A6DC92C159172796F10F +pieces.development.sha256=C9FB776B051A6A06170163F5665A35DDC8B00192FE12A6DC92C159172796F10F From 4a8cfa35c56d97dc884e0db14c9edcbf8389dcd9 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 15:41:11 +0000 Subject: [PATCH 10/12] Smooth packed ice and add native powder-snow pieces --- CHANGELOG.md | 4 + README.md | 19 +- docs/API.md | 5 + gradle/content-dependencies.properties | 12 +- .../probe/ClientProbe.java | 1 + .../probe/FrozenTerrainProbe.java | 65 +++++ .../probe/PowderSnowClientProbe.java | 86 +++++++ .../probe/PowderSnowProbe.java | 175 +++++++++++++ .../probe/RuntimeProbe.java | 2 +- .../probe/SnowTerrainProbe.java | 4 +- .../client/PowderSnowFog.java | 52 ++++ .../content/PowderSnowContent.java | 66 +++++ .../content/PowderSnowCuttingRecipe.java | 83 ++++++ .../content/PowderSnowPieceBlock.java | 241 ++++++++++++++++++ .../internal/BuiltInMaterials.java | 16 ++ .../internal/MaterialCatalogue.java | 5 +- .../internal/SmoothingEngine.java | 8 +- .../internal/TerrainGeometry.java | 30 +++ src/main/recipe-book/recipes.json | 82 +++++- .../blockstates/powder_snow_corner.json | 28 ++ .../blockstates/powder_snow_slab.json | 28 ++ .../blockstates/powder_snow_step.json | 28 ++ .../skysterrainsmoother/lang/de_at.json | 5 +- .../skysterrainsmoother/lang/de_au.json | 5 +- .../skysterrainsmoother/lang/de_de.json | 5 +- .../skysterrainsmoother/lang/en_ca.json | 5 +- .../skysterrainsmoother/lang/en_en.json | 5 +- .../skysterrainsmoother/lang/en_gb.json | 5 +- .../skysterrainsmoother/lang/en_pt.json | 5 +- .../skysterrainsmoother/lang/en_us.json | 5 +- .../skysterrainsmoother/lang/es_es.json | 5 +- .../skysterrainsmoother/lang/es_mx.json | 5 +- .../skysterrainsmoother/lang/fr_ca.json | 5 +- .../skysterrainsmoother/lang/fr_fr.json | 5 +- .../skysterrainsmoother/lang/ja_jp.json | 5 +- .../skysterrainsmoother/lang/ko_kr.json | 5 +- .../skysterrainsmoother/lang/pt_br.json | 5 +- .../skysterrainsmoother/lang/pt_pt.json | 5 +- .../skysterrainsmoother/lang/ru_ru.json | 5 +- .../skysterrainsmoother/lang/zh_cn.json | 5 +- .../block/powder_snow_corner_bottom_east.json | 41 +++ .../powder_snow_corner_bottom_north.json | 41 +++ .../powder_snow_corner_bottom_south.json | 41 +++ .../block/powder_snow_corner_bottom_west.json | 41 +++ .../block/powder_snow_corner_top_east.json | 41 +++ .../block/powder_snow_corner_top_north.json | 41 +++ .../block/powder_snow_corner_top_south.json | 41 +++ .../block/powder_snow_corner_top_west.json | 41 +++ .../block/powder_snow_slab_bottom_east.json | 41 +++ .../block/powder_snow_slab_bottom_north.json | 41 +++ .../block/powder_snow_slab_bottom_south.json | 41 +++ .../block/powder_snow_slab_bottom_west.json | 41 +++ .../block/powder_snow_slab_top_east.json | 41 +++ .../block/powder_snow_slab_top_north.json | 41 +++ .../block/powder_snow_slab_top_south.json | 41 +++ .../block/powder_snow_slab_top_west.json | 41 +++ .../block/powder_snow_step_bottom_east.json | 41 +++ .../block/powder_snow_step_bottom_north.json | 41 +++ .../block/powder_snow_step_bottom_south.json | 41 +++ .../block/powder_snow_step_bottom_west.json | 41 +++ .../block/powder_snow_step_top_east.json | 41 +++ .../block/powder_snow_step_top_north.json | 41 +++ .../block/powder_snow_step_top_south.json | 41 +++ .../block/powder_snow_step_top_west.json | 41 +++ .../models/item/powder_snow_corner.json | 3 + .../models/item/powder_snow_slab.json | 3 + .../models/item/powder_snow_step.json | 3 + .../loot_table/blocks/powder_snow_corner.json | 19 ++ .../loot_table/blocks/powder_snow_slab.json | 19 ++ .../loot_table/blocks/powder_snow_step.json | 19 ++ .../recipe/powder_snow_corner.json | 17 ++ .../recipe/powder_snow_from_slabs.json | 20 ++ .../recipe/powder_snow_slab.json | 4 + .../recipe/powder_snow_slab_from_steps.json | 18 ++ .../recipe/powder_snow_step.json | 18 ++ .../recipe/powder_snow_step_from_corners.json | 18 ++ .../recipe/red_sand_from_slabs.json | 2 +- .../recipe/red_sand_slab.json | 2 +- .../recipe/sand_from_slabs.json | 2 +- .../skysterrainsmoother/recipe/sand_slab.json | 2 +- .../ProjectContractTest.java | 9 +- tools/generate_powder_snow.py | 95 +++++++ tools/test_powder_snow_resources.py | 35 +++ 83 files changed, 2377 insertions(+), 45 deletions(-) create mode 100644 src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FrozenTerrainProbe.java create mode 100644 src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/PowderSnowClientProbe.java create mode 100644 src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/PowderSnowProbe.java create mode 100644 src/main/java/zone/moddev/mc/skysterrainsmoother/client/PowderSnowFog.java create mode 100644 src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowContent.java create mode 100644 src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowCuttingRecipe.java create mode 100644 src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowPieceBlock.java create mode 100644 src/main/java/zone/moddev/mc/skysterrainsmoother/internal/TerrainGeometry.java create mode 100644 src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_corner.json create mode 100644 src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_slab.json create mode 100644 src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_step.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_east.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_north.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_south.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_west.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_east.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_north.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_south.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_west.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_east.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_north.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_south.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_west.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_east.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_north.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_south.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_west.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_east.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_north.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_south.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_west.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_east.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_north.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_south.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_west.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_corner.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_slab.json create mode 100644 src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_step.json create mode 100644 src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_corner.json create mode 100644 src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_slab.json create mode 100644 src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_step.json create mode 100644 src/main/resources/data/skysterrainsmoother/recipe/powder_snow_corner.json create mode 100644 src/main/resources/data/skysterrainsmoother/recipe/powder_snow_from_slabs.json create mode 100644 src/main/resources/data/skysterrainsmoother/recipe/powder_snow_slab.json create mode 100644 src/main/resources/data/skysterrainsmoother/recipe/powder_snow_slab_from_steps.json create mode 100644 src/main/resources/data/skysterrainsmoother/recipe/powder_snow_step.json create mode 100644 src/main/resources/data/skysterrainsmoother/recipe/powder_snow_step_from_corners.json create mode 100644 tools/generate_powder_snow.py create mode 100644 tools/test_powder_snow_resources.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e202cf9..06ffb54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.1.1.121012 +- Smooth packed ice and powder snow with matching pieces. +- Keep powder snow's sinking, freezing, leather-boot support and fog within each piece's footprint. +- Add powder-snow slab, step and corner recipes with bucket returns and volume-preserving rebuilding. +- Keep different crafting results on separate recipe-book buttons. - Smooth full snow-block terrain with matching snow slabs, steps and corners. - Keep grass transitions snowy beside higher snow-covered ground. - Gentle surface terraces use full-width slabs at smoothing levels 2 and 3, giving each rise a wider tread. diff --git a/README.md b/README.md index f219da4..9a554b1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Sky's Terrain Smoother adds smaller transitions to new Overworld terrain, including cave floors. It uses slabs, steps and corners from Sky's Grass Slabs and Sky's Building Pieces, with matching materials for grass, dirt, stone, -sandstone, sand, hardened clay and full snow blocks. +sandstone, sand, hardened clay, packed ice, full snow blocks and powder snow. ## Smoothing levels @@ -46,6 +46,23 @@ its slabs are enabled, Building Pieces provides the matching steps and corners. Ores, gravel, wood and decorative stone aren't smoothing materials. +## Snow and powder snow + +Packed ice and snow blocks use matching Building Pieces slabs, steps and corners. +Powder snow has its own horizontal pieces: they retain sinking, freezing and +slow movement, while leather boots let you stand on them. The empty part of a +step or corner stays empty, including its fog and freezing effects. + +One powder-snow bucket makes two slabs and returns an empty bucket. To fill it +again, craft two slabs with an empty bucket. Slabs cut into steps, steps cut +into corners, and matching vertical pairs rebuild the larger pieces. Breaking +a partial piece returns that piece; it cannot fill a whole bucket on its own. +Matching halves placed together become an ordinary full powder-snow block. + +For ordinary snow slabs, craft two snow blocks diagonally to make four slabs. +Vanilla's three-block row still makes snow layers. Recipe-book buttons show one +result each; right-click for alternative ways to make the same item. + For BOP grass and soils, add the [Terrain Smoother Biomes O' Plenty add-on](https://github.com/MinecraftModDevelopmentMods/SkysTerrainSmoother-BiomesOPlenty-Addon). diff --git a/docs/API.md b/docs/API.md index 8d86c3c..50cc246 100644 --- a/docs/API.md +++ b/docs/API.md @@ -15,6 +15,11 @@ southwest. Steps occupy one quarter of a block's volume and corners one eighth. Every output must be half a block tall, dry, non-tile, and fill that footprint. Sources and converted supports must be dry full blocks without tile entities. +Powder snow is the one non-solid exception. Planning validates its full outline +and the partial outlines of the built-in powder pieces, while their actual +entity collision remains native powder snow. Other non-solid sources and +empty-collision outputs are still rejected. + ```java TerrainSmoothingApi.registerMaterial(new TerrainMaterial( ResourceLocation.fromNamespaceAndPath("example", "natural_rock"), diff --git a/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index cbf420c..3922178 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -1,8 +1,8 @@ grass.version=1.2.1.121012 -grass.commit=34dc9266c885985419361d86f91f588fbef3d8a9 -grass.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B -grass.development.sha256=1656E47CCE9963B4B29F61B5CBD5BD0AD42BB285C4C7C6FC31B891356C2D130B +grass.commit=d022eba609e6cf0381183232d35768b6190f2d53 +grass.sha256=9A122E63CCAC190246ECA4DD6D5A8D84CA0150A10AF7768142F97EF5DEB8A3C8 +grass.development.sha256=9A122E63CCAC190246ECA4DD6D5A8D84CA0150A10AF7768142F97EF5DEB8A3C8 pieces.version=0.3.10.121012 -pieces.commit=157f3bd7db4e9afecc252998f1d07be23715d7bd -pieces.sha256=C9FB776B051A6A06170163F5665A35DDC8B00192FE12A6DC92C159172796F10F -pieces.development.sha256=C9FB776B051A6A06170163F5665A35DDC8B00192FE12A6DC92C159172796F10F +pieces.commit=708347be4f0ad385d81ca070477628b092fe6925 +pieces.sha256=322C3D68E40630C6C4672CD74FB438FA08E15FB30FE524B421145A80F7E48573 +pieces.development.sha256=322C3D68E40630C6C4672CD74FB438FA08E15FB30FE524B421145A80F7E48573 diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java index e8a57a6..34566b5 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java @@ -47,6 +47,7 @@ public void tick(ClientTickEvent.Post event) { && !minecraft.getConnection().getRecipeManager().getRecipes().isEmpty()) { if (++settledWorldTicks < 20) return; RecipePreviewModels.check(minecraft); + PowderSnowClientProbe.run(minecraft); checkBiomeTints(minecraft); zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_CLIENT_WORLD_PASS"); integrated = false; diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FrozenTerrainProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FrozenTerrainProbe.java new file mode 100644 index 0000000..4a93eb7 --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FrozenTerrainProbe.java @@ -0,0 +1,65 @@ +package zone.moddev.mc.skysterrainsmoother.probe; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Block; +import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; +import zone.moddev.mc.skysterrainsmoother.internal.MaterialCatalogue; +import zone.moddev.mc.skysterrainsmoother.internal.TerrainGeometry; + +/** Frozen surfaces need explicit mappings; their similar appearance is not a substitute. */ +final class FrozenTerrainProbe { + static int run(ServerLevel world) { + RuntimeProbe.require(MaterialCatalogue.instance().source(Blocks.PACKED_ICE.defaultBlockState()) != null, + "Packed ice has its own smoothing material"); + int checks = 1 + PowderSnowProbe.run(world); + BlockPos floor = new BlockPos(5076, world.getMaxBuildHeight() - 10, 5076); + var owner = world.getChunkAt(floor); + int loaded = world.getChunkSource().getLoadedChunksCount(); + for (Block source : new Block[] { Blocks.PACKED_ICE, Blocks.POWDER_SNOW }) { + var material = MaterialCatalogue.instance().source(source.defaultBlockState()); + RuntimeProbe.require(material != null, "Every frozen surface has a distinct material"); + for (int level = 1; level <= 3; level++) { + for (Direction direction : Direction.Plane.HORIZONTAL) { + for (int offsetZ = -3; offsetZ <= 3; offsetZ++) { + for (int offsetX = -3; offsetX <= 3; offsetX++) { + BlockPos column = floor.offset(offsetX, 0, offsetZ); + world.setBlock(column, source.defaultBlockState(), 2); + world.setBlock(column.below(), source.defaultBlockState(), 2); + for (int height = 1; height <= 8; height++) { + ProbeWorld.clear(world, column.above(height)); + } + } + } + BlockPos higher = level == 3 ? floor.relative(direction).relative(direction.getClockWise()).above() + : floor.relative(direction).above(); + world.setBlock(higher, source.defaultBlockState(), 2); + ProbeWorld.rebuildHeights(owner); + var support = world.getBlockState(floor); + SkysTerrainSmoother.engine.smooth(world, owner, level); + var piece = world.getBlockState(floor.above()); + RuntimeProbe.require(MaterialCatalogue.instance().piece(piece) == material, + "Frozen terrain receives its own pieces at every level and rotation: " + source + " / " + level); + RuntimeProbe.require(world.getBlockState(floor).equals(support), + "Frozen terrain smoothing preserves the original full support"); + RuntimeProbe.require(TerrainGeometry.shape(piece).bounds().maxY == .5, + "Every frozen transition stays half a block tall"); + if (source == Blocks.POWDER_SNOW) { + RuntimeProbe.require(piece.getCollisionShape(world, floor.above()).isEmpty(), + "Generated powder pieces stay non-solid"); + } + SkysTerrainSmoother.engine.smooth(world, owner, level); + RuntimeProbe.require(world.getBlockState(floor.above()).equals(piece), + "Repeating a frozen generation pass is idempotent"); + checks += 5; + } + } + } + RuntimeProbe.require(world.getChunkSource().getLoadedChunksCount() == loaded, + "Frozen terrain reads do not force neighbouring chunks"); + SkysTerrainSmoother.logger.info("TERRAIN_FROZEN_SURFACES_PASS checks={} materials=2 levels=3 rotations=4", checks); + return checks; + } +} diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/PowderSnowClientProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/PowderSnowClientProbe.java new file mode 100644 index 0000000..7e2ca54 --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/PowderSnowClientProbe.java @@ -0,0 +1,86 @@ +package zone.moddev.mc.skysterrainsmoother.probe; + +import com.mojang.blaze3d.shaders.FogShape; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.FogRenderer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.properties.Half; +import net.minecraft.world.level.material.FogType; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.client.event.ViewportEvent; +import net.neoforged.neoforge.common.NeoForge; +import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; +import zone.moddev.mc.skysterrainsmoother.content.PowderSnowContent; +import zone.moddev.mc.skysterrainsmoother.content.PowderSnowPieceBlock; + +/** Exercise the actual baked models and viewport events inside and outside each footprint. */ +public final class PowderSnowClientProbe { + public static void run(Minecraft client) { + BlockPos position = client.player.blockPosition().above(10); + var original = client.level.getBlockState(position); + ProbeCamera camera = new ProbeCamera(); + camera.setup(client.level, client.player, false, false, 0); + int checks = 0; + try { + for (var kind : PowderSnowPieceBlock.Kind.values()) { + for (Half half : Half.values()) { + for (Direction facing : Direction.Plane.HORIZONTAL) { + var state = PowderSnowContent.state(kind, half, facing); + client.level.setBlock(position, state, 2); + var model = client.getBlockRenderer().getBlockModel(state); + RuntimeProbe.require(model != client.getModelManager().getMissingModel(), + "Every powder-snow orientation has a baked model"); + for (Direction face : Direction.values()) { + var quads = model.getQuads(state, face, RandomSource.create(0)); + var unculled = model.getQuads(state, null, RandomSource.create(0)); + RuntimeProbe.require(java.util.stream.Stream.concat(quads.stream(), unculled.stream()) + .allMatch(quad -> !quad.isTinted() && quad.getSprite().contents().name() + .toString().equals("minecraft:block/powder_snow")), + "Powder-snow models use native untinted powder snow"); + } + var bounds = state.getShape(client.level, position).bounds(); + Vec3 inside = new Vec3((bounds.minX + bounds.maxX) / 2, + (bounds.minY + bounds.maxY) / 2, (bounds.minZ + bounds.maxZ) / 2); + camera.point(Vec3.atLowerCornerOf(position).add(inside)); + checkFog(camera, true); + Vec3 outside = new Vec3(inside.x, half == Half.BOTTOM ? .75 : .25, inside.z); + camera.point(Vec3.atLowerCornerOf(position).add(outside)); + checkFog(camera, false); + checks += 3; + } + } + } + client.level.setBlock(position, Blocks.SNOW_BLOCK.defaultBlockState(), 2); + camera.point(Vec3.atCenterOf(position)); + checkFog(camera, false); + } finally { + client.level.setBlock(position, original, 2); + } + SkysTerrainSmoother.logger.info("TERRAIN_POWDER_SNOW_CLIENT_PASS checks={} models=true fog=true empty_space=true", checks + 1); + } + + private static void checkFog(Camera camera, boolean inside) { + var color = new ViewportEvent.ComputeFogColor(camera, 0, 1, 1, 1); + NeoForge.EVENT_BUS.post(color); + var fog = new ViewportEvent.RenderFog(FogRenderer.FogMode.FOG_TERRAIN, FogType.NONE, + camera, 0, 0, 128, FogShape.SPHERE); + NeoForge.EVENT_BUS.post(fog); + RuntimeProbe.require(inside ? color.getRed() == .623F && color.getGreen() == .734F + && color.getBlue() == .785F && fog.isCanceled() && fog.getFarPlaneDistance() == 2 + : color.getRed() == 1 && color.getGreen() == 1 && color.getBlue() == 1 && !fog.isCanceled(), + "Native powder fog applies only inside the occupied piece volume"); + } + + private static final class ProbeCamera extends Camera { + void point(Vec3 position) { + setPosition(position); + } + } + + private PowderSnowClientProbe() { + } +} diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/PowderSnowProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/PowderSnowProbe.java new file mode 100644 index 0000000..419cc55 --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/PowderSnowProbe.java @@ -0,0 +1,175 @@ +package zone.moddev.mc.skysterrainsmoother.probe; + +import com.mojang.authlib.GameProfile; +import java.util.List; +import java.util.UUID; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.animal.Pig; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.properties.Half; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; +import zone.moddev.mc.skysterrainsmoother.content.PowderSnowContent; +import zone.moddev.mc.skysterrainsmoother.content.PowderSnowPieceBlock; + +/** Real entity, item-use and recipe-manager checks for non-solid snow pieces. */ +final class PowderSnowProbe { + static int run(ServerLevel world) { + BlockPos position = new BlockPos(4964, 240, 4964); + world.getChunkAt(position); + int checks = 0; + for (var kind : PowderSnowPieceBlock.Kind.values()) { + for (Direction facing : Direction.Plane.HORIZONTAL) { + for (Half half : Half.values()) { + Pig pig = new Pig(EntityType.PIG, world); + var state = PowderSnowContent.state(kind, half, facing); + var block = (PowderSnowPieceBlock) state.getBlock(); + world.setBlock(position, state, 2); + var outline = state.getShape(world, position); + var bounds = outline.bounds(); + double x = position.getX() + (bounds.minX + bounds.maxX) / 2; + double z = position.getZ() + (bounds.minZ + bounds.maxZ) / 2; + pig.setItemSlot(EquipmentSlot.FEET, ItemStack.EMPTY); + pig.fallDistance = 0; + pig.setPos(x, position.getY() + bounds.maxY + .01, z); + require(state.getCollisionShape(world, position, CollisionContext.of(pig)).isEmpty(), + "Bare feet sink through every powder-snow orientation"); + pig.setItemSlot(EquipmentSlot.FEET, new ItemStack(Items.LEATHER_BOOTS)); + require(!state.getCollisionShape(world, position, CollisionContext.of(pig)).isEmpty(), + "Leather boots stand on the actual partial surface"); + pig.setShiftKeyDown(true); + require(state.getCollisionShape(world, position, CollisionContext.of(pig)).isEmpty(), + "Descending in leather boots allows sinking"); + pig.setShiftKeyDown(false); + pig.setItemSlot(EquipmentSlot.FEET, ItemStack.EMPTY); + pig.fallDistance = 4; + require(state.getCollisionShape(world, position, CollisionContext.of(pig)).bounds().maxY + < bounds.maxY, + "Long falls receive native-style soft landing collision"); + pig.fallDistance = 0; + pig.setPos(x, position.getY() + bounds.minY + .05, z); + pig.setIsInPowderSnow(false); + pig.setTicksFrozen(0); + state.entityInside(world, position, pig); + require(pig.isInPowderSnow, "The occupied volume applies native powder-snow freezing"); + double before = pig.getX(); + pig.move(MoverType.SELF, new Vec3(.1, 0, 0)); + require(Math.abs(pig.getX() - before - .09) < .00001, + "Powder snow keeps its native movement slowdown"); + pig.aiStep(); + require(pig.getTicksFrozen() > 0, "A real living-entity tick accumulates freezing"); + if (kind != PowderSnowPieceBlock.Kind.SLAB) { + double emptyX = bounds.minX == 0 && bounds.maxX == .5 ? .75 : .25; + double emptyZ = bounds.minZ == 0 && bounds.maxZ == .5 ? .75 : .25; + if (kind == PowderSnowPieceBlock.Kind.STEP && bounds.maxX - bounds.minX == 1) { + emptyX = .5; + } + pig.setPos(position.getX() + emptyX, position.getY() + bounds.minY + .05, + position.getZ() + emptyZ); + pig.setBoundingBox(new AABB(pig.getX() - .025, pig.getY(), pig.getZ() - .025, + pig.getX() + .025, pig.getY() + .05, pig.getZ() + .025)); + pig.setIsInPowderSnow(false); + state.entityInside(world, position, pig); + require(!pig.isInPowderSnow, "Empty parts of a step or corner do not freeze entities"); + } + require(block.pickupBlock(null, world, position, state).isEmpty() + && world.getBlockState(position).equals(state), + "A partial block cannot be scooped into a full powder bucket"); + var drops = Block.getDrops(state, world, position, null); + require(drops.size() == 1 && drops.getFirst().is(block.asItem()) && drops.getFirst().getCount() == 1, + "Every orientation drops its normal piece when broken by hand"); + checks += 9; + } + } + } + checks += recipes(world); + var player = ProbeItems.player(world, new GameProfile( + UUID.fromString("90def6bd-e26a-4091-8fc9-36a6eed97eb2"), "PowderSnowProbe")); + player.getAbilities().instabuild = false; + player.getAbilities().mayBuild = true; + player.setPos(position.getX() + 3, position.getY(), position.getZ() + 3); + var slab = PowderSnowContent.state(PowderSnowPieceBlock.Kind.SLAB, Half.BOTTOM, Direction.NORTH); + world.setBlock(position, slab, 3); + player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(slab.getBlock(), 2)); + var context = new UseOnContext(player, InteractionHand.MAIN_HAND, new BlockHitResult( + Vec3.atLowerCornerOf(position).add(.5, .5, .5), Direction.UP, position, false)); + require(player.getMainHandItem().useOn(context).consumesAction() + && world.getBlockState(position).is(Blocks.POWDER_SNOW) + && player.getMainHandItem().getCount() == 1, + "Two placed halves join into vanilla powder snow without creating solid snow"); + checks++; + var corner = PowderSnowContent.state(PowderSnowPieceBlock.Kind.CORNER, Half.BOTTOM, Direction.NORTH); + world.setBlock(position, corner, 3); + player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(corner.getBlock(), 2)); + context = new UseOnContext(player, InteractionHand.MAIN_HAND, new BlockHitResult( + Vec3.atLowerCornerOf(position).add(.5, .25, .25), Direction.EAST, position, false)); + require(player.getMainHandItem().useOn(context).consumesAction() + && world.getBlockState(position).getBlock() instanceof PowderSnowPieceBlock joined + && joined.kind() == PowderSnowPieceBlock.Kind.STEP, + "Adjacent powder corners join into a matching step"); + var step = PowderSnowContent.state(PowderSnowPieceBlock.Kind.STEP, Half.BOTTOM, Direction.NORTH); + world.setBlock(position, step, 3); + player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(step.getBlock(), 2)); + context = new UseOnContext(player, InteractionHand.MAIN_HAND, new BlockHitResult( + Vec3.atLowerCornerOf(position).add(.5, .25, .5), Direction.SOUTH, position, false)); + require(player.getMainHandItem().useOn(context).consumesAction() + && world.getBlockState(position).getBlock() instanceof PowderSnowPieceBlock joined + && joined.kind() == PowderSnowPieceBlock.Kind.SLAB, + "Matching powder steps join into a slab"); + checks += 2; + SkysTerrainSmoother.logger.info("TERRAIN_POWDER_SNOW_PASS checks={} sinking=true freezing=true boots=true" + + " partial_pickup_blocked=true drops=true volume=true", checks); + return checks; + } + + private static int recipes(ServerLevel world) { + var slab = PowderSnowContent.state(PowderSnowPieceBlock.Kind.SLAB, Half.BOTTOM, Direction.NORTH).getBlock().asItem(); + var step = PowderSnowContent.state(PowderSnowPieceBlock.Kind.STEP, Half.BOTTOM, Direction.NORTH).getBlock().asItem(); + var corner = PowderSnowContent.state(PowderSnowPieceBlock.Kind.CORNER, Half.BOTTOM, Direction.NORTH).getBlock().asItem(); + CraftingInput bucket = CraftingInput.of(1, 1, List.of(new ItemStack(Items.POWDER_SNOW_BUCKET))); + var cut = world.getRecipeManager().getRecipesFor(RecipeType.CRAFTING, bucket, world); + require(cut.size() == 1, "One powder bucket has one cutting recipe"); + require(ItemStack.matches(new ItemStack(slab, 2), cut.getFirst().value().assemble(bucket, world.registryAccess())) + && cut.getFirst().value().getRemainingItems(bucket).getFirst().is(Items.BUCKET), + "One full powder bucket makes two slabs and returns one empty bucket"); + craft(world, 2, 2, List.of(new ItemStack(slab), ItemStack.EMPTY, ItemStack.EMPTY, new ItemStack(slab)), step, 4); + craft(world, 2, 1, List.of(new ItemStack(step), new ItemStack(step)), corner, 4); + craft(world, 1, 2, List.of(new ItemStack(corner), new ItemStack(corner)), step, 1); + craft(world, 1, 2, List.of(new ItemStack(step), new ItemStack(step)), slab, 1); + craft(world, 2, 2, List.of(new ItemStack(slab), new ItemStack(slab), new ItemStack(Items.BUCKET), ItemStack.EMPTY), + Items.POWDER_SNOW_BUCKET, 1); + var noBucket = CraftingInput.of(2, 1, List.of(new ItemStack(slab), new ItemStack(slab))); + require(world.getRecipeManager().getRecipesFor(RecipeType.CRAFTING, noBucket, world).isEmpty(), + "Rebuilding powder snow cannot manufacture an empty bucket"); + return 8; + } + + private static void craft(ServerLevel world, int width, int height, List inputs, + Item output, int count) { + var input = CraftingInput.of(width, height, inputs); + var matches = world.getRecipeManager().getRecipesFor(RecipeType.CRAFTING, input, world); + require(matches.size() == 1 && ItemStack.matches(new ItemStack(output, count), + matches.getFirst().value().assemble(input, world.registryAccess())), + "Powder snow has one volume-conserving crafting result: " + inputs); + } + + private static void require(boolean condition, String message) { + RuntimeProbe.require(condition, message); + } +} diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java index fa60672..70de31b 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java @@ -198,7 +198,7 @@ public void tick(ServerTickEvent.Post event) { require(SkysTerrainSmoother.level == Integer.getInteger("skysterrainsmoother.expectedLevel", 3), "The native configuration selected the requested generation level"); } else { - gameplay = SnowTerrainProbe.run(world) + MineralogyProbe.run() + FineGrassProbe.run(world) + OpportunityProbe.run(world) + gameplay = FrozenTerrainProbe.run(world) + SnowTerrainProbe.run(world) + MineralogyProbe.run() + FineGrassProbe.run(world) + OpportunityProbe.run(world) + TransitionProbe.run(world) + FoliageProbe.run(world) + persistence(world) + LateTerrainProbe.run(world) + CaveProbe.run(world) + decorationChanges(world) + geometry(world) + grass(world) + sand(world) + generationOrder(world) diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java index a21b55e..a28aa59 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java @@ -71,8 +71,8 @@ static int run(ServerLevel world) throws Exception { "snow appearance does not load chunks"); RuntimeProbe.require(MaterialCatalogue.instance().source(Blocks.SNOW_BLOCK.defaultBlockState()) != null, "full snow blocks have a registered smoothing material"); - RuntimeProbe.require(MaterialCatalogue.instance().source(Blocks.POWDER_SNOW.defaultBlockState()) == null, - "powder snow keeps its native behaviour"); + RuntimeProbe.require(MaterialCatalogue.instance().source(Blocks.POWDER_SNOW.defaultBlockState()) != null, + "powder snow has its own non-solid smoothing pieces"); checks += coldSurfaces(world); SkysTerrainSmoother.logger.info("TERRAIN_SNOW_TERRAIN_PASS checks={}", checks + 3); return checks + 3; diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/client/PowderSnowFog.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/client/PowderSnowFog.java new file mode 100644 index 0000000..f664bd1 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/client/PowderSnowFog.java @@ -0,0 +1,52 @@ +package zone.moddev.mc.skysterrainsmoother.client; + +import com.mojang.blaze3d.shaders.FogShape; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ViewportEvent; +import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; +import zone.moddev.mc.skysterrainsmoother.content.PowderSnowPieceBlock; + +/** Vanilla recognises only its full powder-snow block when choosing camera fog. */ +@EventBusSubscriber(modid = SkysTerrainSmoother.ID, value = Dist.CLIENT) +public final class PowderSnowFog { + private static boolean inside(Camera camera) { + var world = Minecraft.getInstance().level; + if (world == null) { + return false; + } + var position = camera.getBlockPosition(); + var state = world.getBlockState(position); + return state.getBlock() instanceof PowderSnowPieceBlock + && state.getShape(world, position).bounds().contains( + camera.getPosition().subtract(Vec3.atLowerCornerOf(position))); + } + + @SubscribeEvent + public static void color(ViewportEvent.ComputeFogColor event) { + if (inside(event.getCamera())) { + event.setRed(.623F); + event.setGreen(.734F); + event.setBlue(.785F); + } + } + + @SubscribeEvent + public static void distance(ViewportEvent.RenderFog event) { + if (inside(event.getCamera())) { + var entity = event.getCamera().getEntity(); + boolean spectator = entity != null && entity.isSpectator(); + event.setNearPlaneDistance(spectator ? -8 : 0); + event.setFarPlaneDistance(spectator ? event.getFarPlaneDistance() * .5F : 2); + event.setFogShape(FogShape.SPHERE); + event.setCanceled(true); + } + } + + private PowderSnowFog() { + } +} diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowContent.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowContent.java new file mode 100644 index 0000000..6f166a5 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowContent.java @@ -0,0 +1,66 @@ +package zone.moddev.mc.skysterrainsmoother.content; + +import java.util.EnumMap; +import java.util.Map; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Half; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; +import net.neoforged.neoforge.registries.RegisterEvent; +import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; + +/** Natural powder snow needs its own pieces; solid snow would remove the hazard. */ +@EventBusSubscriber(modid = SkysTerrainSmoother.ID, bus = EventBusSubscriber.Bus.MOD) +public final class PowderSnowContent { + public static final RecipeSerializer CUTTING = + new SimpleCraftingRecipeSerializer<>(PowderSnowCuttingRecipe::new); + private static final Map BLOCKS = + new EnumMap<>(PowderSnowPieceBlock.Kind.class); + + @SubscribeEvent + public static void register(RegisterEvent event) { + event.register(Registries.RECIPE_SERIALIZER, registry -> registry.register( + ResourceLocation.fromNamespaceAndPath(SkysTerrainSmoother.ID, "powder_snow_cutting"), CUTTING)); + event.register(Registries.BLOCK, registry -> { + for (var kind : PowderSnowPieceBlock.Kind.values()) { + var block = new PowderSnowPieceBlock(kind); + BLOCKS.put(kind, block); + registry.register(id(kind), block); + } + }); + event.register(Registries.ITEM, registry -> { + for (var entry : BLOCKS.entrySet()) { + registry.register(id(entry.getKey()), new BlockItem(entry.getValue(), new Item.Properties())); + } + }); + } + + @SubscribeEvent + public static void creative(BuildCreativeModeTabContentsEvent event) { + if (event.getTabKey().equals(CreativeModeTabs.BUILDING_BLOCKS)) { + BLOCKS.values().forEach(event::accept); + } + } + + private static ResourceLocation id(PowderSnowPieceBlock.Kind kind) { + return ResourceLocation.fromNamespaceAndPath(SkysTerrainSmoother.ID, + "powder_snow_" + kind.getSerializedName()); + } + + public static BlockState state(PowderSnowPieceBlock.Kind kind, Half half, Direction facing) { + return BLOCKS.get(kind).defaultBlockState().setValue(PowderSnowPieceBlock.HALF, half) + .setValue(PowderSnowPieceBlock.FACING, facing); + } + + private PowderSnowContent() { + } +} diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowCuttingRecipe.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowCuttingRecipe.java new file mode 100644 index 0000000..0bb635c --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowCuttingRecipe.java @@ -0,0 +1,83 @@ +package zone.moddev.mc.skysterrainsmoother.content; + +import net.minecraft.core.HolderLookup; +import net.minecraft.core.NonNullList; +import net.minecraft.core.Direction; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.CraftingBookCategory; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.CustomRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.properties.Half; + +/** Cut one bucket of powder snow without consuming the bucket itself. */ +public final class PowderSnowCuttingRecipe extends CustomRecipe { + public PowderSnowCuttingRecipe(CraftingBookCategory category) { + super(category); + } + + @Override + public boolean matches(CraftingInput input, Level world) { + int buckets = 0; + for (int slot = 0; slot < input.size(); slot++) { + ItemStack stack = input.getItem(slot); + if (!stack.isEmpty()) { + if (!stack.is(Items.POWDER_SNOW_BUCKET)) { + return false; + } + buckets++; + } + } + return buckets == 1; + } + + @Override + public ItemStack assemble(CraftingInput input, HolderLookup.Provider registries) { + return getResultItem(registries); + } + + @Override + public ItemStack getResultItem(HolderLookup.Provider registries) { + return new ItemStack(PowderSnowContent.state(PowderSnowPieceBlock.Kind.SLAB, + Half.BOTTOM, Direction.NORTH).getBlock(), 2); + } + + @Override + public NonNullList getRemainingItems(CraftingInput input) { + NonNullList remainders = NonNullList.withSize(input.size(), ItemStack.EMPTY); + for (int slot = 0; slot < input.size(); slot++) { + if (input.getItem(slot).is(Items.POWDER_SNOW_BUCKET)) { + remainders.set(slot, new ItemStack(Items.BUCKET)); + } + } + return remainders; + } + + @Override + public NonNullList getIngredients() { + return NonNullList.of(Ingredient.EMPTY, Ingredient.of(Items.POWDER_SNOW_BUCKET)); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return width * height >= 1; + } + + @Override + public boolean isSpecial() { + return false; + } + + @Override + public String getGroup() { + return "skys/result/skysterrainsmoother/powder_snow_slab"; + } + + @Override + public RecipeSerializer getSerializer() { + return PowderSnowContent.CUTTING; + } +} diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowPieceBlock.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowPieceBlock.java new file mode 100644 index 0000000..4ebfd6c --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowPieceBlock.java @@ -0,0 +1,241 @@ +package zone.moddev.mc.skysterrainsmoother.content; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.Locale; +import java.util.Optional; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.StringRepresentable; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.PowderSnowBlock; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.Half; +import net.minecraft.world.level.material.MapColor; +import net.minecraft.world.level.pathfinder.PathType; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.EntityCollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +/** Native powder-snow behaviour confined to a slab, step or corner's actual volume. */ +public final class PowderSnowPieceBlock extends PowderSnowBlock { + public enum Kind implements StringRepresentable { + SLAB, STEP, CORNER; + + @Override + public String getSerializedName() { + return name().toLowerCase(Locale.ROOT); + } + } + + public static final EnumProperty HALF = BlockStateProperties.HALF; + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + StringRepresentable.fromEnum(Kind::values).fieldOf("shape") + .forGetter(block -> ((PowderSnowPieceBlock) block).kind), + propertiesCodec()).apply(instance, PowderSnowPieceBlock::new)); + private static final int[] EDGES = { 3, 6, 12, 9 }; + private final Kind kind; + private final VoxelShape[] outlines = new VoxelShape[8]; + private final VoxelShape[] landings = new VoxelShape[8]; + + public PowderSnowPieceBlock(Kind kind) { + this(kind, Properties.of().mapColor(MapColor.SNOW).strength(.25F).sound(SoundType.POWDER_SNOW) + .noOcclusion().dynamicShape().forceSolidOff()); + } + + private PowderSnowPieceBlock(Kind kind, Properties properties) { + super(properties); + this.kind = kind; + registerDefaultState(stateDefinition.any().setValue(HALF, Half.BOTTOM).setValue(FACING, Direction.NORTH)); + for (BlockState state : stateDefinition.getPossibleStates()) { + int index = index(state); + outlines[index] = shape(state, false); + landings[index] = shape(state, true); + } + } + + @Override + public MapCodec codec() { + return CODEC; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(HALF, FACING); + } + + public Kind kind() { + return kind; + } + + public int footprint(BlockState state) { + int facing = switch (state.getValue(FACING)) { + case NORTH -> 0; + case EAST -> 1; + case SOUTH -> 2; + default -> 3; + }; + return switch (kind) { + case SLAB -> 15; + case STEP -> EDGES[facing]; + case CORNER -> 1 << facing; + }; + } + + public int mask(BlockState state) { + return footprint(state) << (state.getValue(HALF) == Half.TOP ? 4 : 0); + } + + private static int index(BlockState state) { + return state.getValue(FACING).get2DDataValue() + (state.getValue(HALF) == Half.TOP ? 4 : 0); + } + + private VoxelShape shape(BlockState state, boolean landing) { + double bottom = state.getValue(HALF) == Half.TOP ? .5 : 0; + double top = bottom + (landing ? .45 : .5); + VoxelShape result = Shapes.empty(); + for (int quadrant = 0; quadrant < 4; quadrant++) { + if ((footprint(state) & 1 << quadrant) != 0) { + double left = quadrant == 0 || quadrant == 3 ? 0 : .5; + double front = quadrant < 2 ? 0 : .5; + result = Shapes.or(result, Shapes.box(left, bottom, front, left + .5, top, front + .5)); + } + } + return result.optimize(); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos position, CollisionContext context) { + return outlines[index(state)]; + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter world, BlockPos position, CollisionContext context) { + if (context instanceof EntityCollisionContext entityContext) { + Entity entity = entityContext.getEntity(); + if (entity != null) { + if (entity.fallDistance > 2.5F) { + return landings[index(state)]; + } + if (entity instanceof FallingBlockEntity || canEntityWalkOnPowderSnow(entity) + && context.isAbove(outlines[index(state)], position, false) && !context.isDescending()) { + return outlines[index(state)]; + } + } + } + return Shapes.empty(); + } + + @Override + protected void entityInside(BlockState state, Level world, BlockPos position, Entity entity) { + boolean inside = outlines[index(state)].bounds().move(position).intersects(entity.getBoundingBox()); + if (inside) { + // The superclass sets slowdown, freezing, particles and fire behaviour. + super.entityInside(state, world, position, entity); + } + } + + @Override + public ItemStack pickupBlock(Player player, LevelAccessor world, BlockPos position, BlockState state) { + // A fraction of a block cannot fill a whole bucket. Rebuild the pieces first. + return ItemStack.EMPTY; + } + + @Override + public Optional getPickupSound() { + return Optional.empty(); + } + + @Override + public PathType getBlockPathType(BlockState state, BlockGetter world, BlockPos position, Mob mob) { + return PathType.POWDER_SNOW; + } + + @Override + protected boolean skipRendering(BlockState state, BlockState neighbour, Direction face) { + return false; + } + + @Override + protected boolean canBeReplaced(BlockState state, BlockPlaceContext context) { + return Block.byItem(context.getItemInHand().getItem()) instanceof PowderSnowPieceBlock incoming + && joined(state, incoming, context) != null; + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + BlockState existing = context.getLevel().getBlockState(context.getClickedPos()); + if (existing.getBlock() instanceof PowderSnowPieceBlock) { + return joined(existing, this, context); + } + if (!context.getLevel().getFluidState(context.getClickedPos()).isEmpty()) { + return null; + } + Vec3 hit = context.getClickLocation().subtract(Vec3.atLowerCornerOf(context.getClickedPos())); + Half half = context.getClickedFace() == Direction.DOWN + || context.getClickedFace().getAxis() != Direction.Axis.Y && hit.y > .5 ? Half.TOP : Half.BOTTOM; + Direction facing = kind == Kind.CORNER + ? (hit.z < .5 ? (hit.x < .5 ? Direction.NORTH : Direction.EAST) + : (hit.x < .5 ? Direction.WEST : Direction.SOUTH)) + : (Math.abs(hit.x - .5) > Math.abs(hit.z - .5) + ? (hit.x < .5 ? Direction.WEST : Direction.EAST) + : (hit.z < .5 ? Direction.NORTH : Direction.SOUTH)); + return defaultBlockState().setValue(HALF, half).setValue(FACING, kind == Kind.SLAB ? Direction.NORTH : facing); + } + + private static BlockState joined(BlockState existing, PowderSnowPieceBlock incoming, BlockPlaceContext context) { + if (!(existing.getBlock() instanceof PowderSnowPieceBlock present)) { + return null; + } + Vec3 normal = Vec3.atLowerCornerOf(context.getClickedFace().getNormal()); + Vec3 inside = context.getClickLocation().subtract(normal.scale(.0001)); + BlockPos cell = BlockPos.containing(inside); + Vec3 next = context.getClickLocation().add(normal.scale(.0001)).subtract(Vec3.atLowerCornerOf(cell)); + if (next.x < 0 || next.x > 1 || next.y < 0 || next.y > 1 || next.z < 0 || next.z > 1) { + return null; + } + int quadrant = next.z < .5 ? (next.x < .5 ? 0 : 1) : (next.x < .5 ? 3 : 2); + int selected = 1 << (quadrant + (next.y >= .5 ? 4 : 0)); + int occupied = present.mask(existing); + for (BlockState candidate : incoming.stateDefinition.getPossibleStates()) { + int added = incoming.mask(candidate); + if ((added & occupied) != 0 || (added & selected) == 0) { + continue; + } + int union = added | occupied; + if (union == 255) { + return Blocks.POWDER_SNOW.defaultBlockState(); + } + for (Kind kind : Kind.values()) { + for (Direction facing : Direction.Plane.HORIZONTAL) { + for (Half half : Half.values()) { + BlockState result = PowderSnowContent.state(kind, half, facing); + if (((PowderSnowPieceBlock) result.getBlock()).mask(result) == union) { + return result; + } + } + } + } + } + return null; + } +} diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/BuiltInMaterials.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/BuiltInMaterials.java index d4eacdb..1f0aee3 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/BuiltInMaterials.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/BuiltInMaterials.java @@ -17,6 +17,9 @@ import zone.moddev.mc.skysterrainsmoother.api.TerrainMaterial; import zone.moddev.mc.skysterrainsmoother.config.TerrainSmootherConfig; import zone.moddev.mc.skysterrainsmoother.content.SandContent; +import zone.moddev.mc.skysterrainsmoother.content.PowderSnowContent; +import zone.moddev.mc.skysterrainsmoother.content.PowderSnowPieceBlock; +import net.minecraft.world.level.block.state.properties.Half; /** Exact natural surfaces; ores and decorative variants are never inferred from tags. */ public final class BuiltInMaterials { @@ -53,6 +56,19 @@ public static synchronized void ensureRegistered() { vanilla("red_sandstone", Blocks.RED_SANDSTONE.defaultBlockState(), false, null); vanilla("hardened_clay", Blocks.TERRACOTTA.defaultBlockState(), false, null); vanilla("snow", Blocks.SNOW_BLOCK.defaultBlockState(), false, null); + vanilla("packed_ice", Blocks.PACKED_ICE.defaultBlockState(), false, null); + BlockState[] powderSteps = new BlockState[4]; + BlockState[] powderCorners = new BlockState[4]; + for (int direction = 0; direction < DIRECTIONS.length; direction++) { + powderSteps[direction] = PowderSnowContent.state(PowderSnowPieceBlock.Kind.STEP, + Half.BOTTOM, DIRECTIONS[direction]); + powderCorners[direction] = PowderSnowContent.state(PowderSnowPieceBlock.Kind.CORNER, + Half.BOTTOM, DIRECTIONS[direction]); + } + MaterialCatalogue.instance().register(new TerrainMaterial(ResourceLocation.withDefaultNamespace("powder_snow"), + Collections.singleton(Blocks.POWDER_SNOW.defaultBlockState()), + PowderSnowContent.state(PowderSnowPieceBlock.Kind.SLAB, Half.BOTTOM, Direction.NORTH), + powderSteps, powderCorners, false, null)); for (int variant = 0; variant < 2; variant++) { Block sand = variant == 0 ? Blocks.SAND : Blocks.RED_SAND; MaterialCatalogue.instance().register(new TerrainMaterial( diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/MaterialCatalogue.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/MaterialCatalogue.java index 1336139..c1fc612 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/MaterialCatalogue.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/MaterialCatalogue.java @@ -224,7 +224,7 @@ private static void validatePiece(BlockState state, int footprint) { throw new IllegalArgumentException("A terrain piece cannot be full or tile-backed"); } - VoxelShape shape = state.getCollisionShape(EmptyAccess.INSTANCE, BlockPos.ZERO); + VoxelShape shape = TerrainGeometry.shape(state); if (shape.isEmpty()) { throw new IllegalArgumentException("A terrain piece needs solid collision geometry"); } @@ -257,8 +257,7 @@ private static void validatePiece(BlockState state, int footprint) { } private static boolean fullCollision(BlockState state) { - return !Shapes.joinIsNotEmpty(state.getCollisionShape(EmptyAccess.INSTANCE, BlockPos.ZERO), - Shapes.block(), BooleanOp.NOT_SAME); + return TerrainGeometry.full(state); } private static BlockState canonical(BlockState state) { diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java index d719a5e..13183ab 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/SmoothingEngine.java @@ -224,7 +224,7 @@ private boolean validate(LevelAccessor world, int chunkX, int chunkZ, List files = Files.list(recipes)) { - assertEquals(4, files.count()); + assertEquals(4, files.filter(path -> path.getFileName().toString().startsWith("sand_") + || path.getFileName().toString().startsWith("red_sand_")).count()); } for (String sand : new String[] { "sand", "red_sand" }) { JsonObject cut = json(recipes.resolve(sand + "_slab.json")); @@ -87,7 +88,11 @@ void eighteenUtf8LanguagesRetainNamesAndConfigHelp() throws Exception { assertFalse(source.contains("\r")); assertTrue(source.endsWith("\n")); JsonObject values = json(path); - assertEquals(8, values.size()); + assertEquals(11, values.size()); + for (String shape : new String[] { "slab", "step", "corner" }) { + assertFalse(values.get("block.skysterrainsmoother.powder_snow_" + shape) + .getAsString().trim().isEmpty()); + } for (String sand : new String[] { "sand", "red_sand" }) { assertFalse(values.get("block.skysterrainsmoother." + sand + "_slab").getAsString().trim().isEmpty()); } diff --git a/tools/generate_powder_snow.py b/tools/generate_powder_snow.py new file mode 100644 index 0000000..95c8e07 --- /dev/null +++ b/tools/generate_powder_snow.py @@ -0,0 +1,95 @@ +"""Build the small powder-snow resource set from its horizontal footprints.""" +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MOD = 'skysterrainsmoother' +DIRECTIONS = ('north', 'east', 'south', 'west') + + +def resources(): + files = {} + for kind in ('slab', 'step', 'corner'): + name = 'powder_snow_' + kind + variants = {} + for half in ('bottom', 'top'): + for direction, facing in enumerate(DIRECTIONS): + mask = 15 if kind == 'slab' else (3, 6, 12, 9)[direction] if kind == 'step' else 1 << direction + corners = [(0 if cell in (0, 3) else 8, 0 if cell < 2 else 8) + for cell in range(4) if mask & 1 << cell] + bottom = 0 if half == 'bottom' else 8 + model = name + '_' + half + '_' + facing + files['assets/' + MOD + '/models/block/' + model + '.json'] = { + 'parent': 'minecraft:block/block', + 'textures': {'particle': 'minecraft:block/powder_snow', 'snow': 'minecraft:block/powder_snow'}, + 'elements': [{'from': [min(x for x, z in corners), bottom, min(z for x, z in corners)], + 'to': [max(x for x, z in corners) + 8, bottom + 8, max(z for x, z in corners) + 8], + 'faces': {side: {'texture': '#snow'} for side in + ('down', 'up', 'north', 'south', 'west', 'east')}}]} + variants['facing=' + facing + ',half=' + half] = {'model': MOD + ':block/' + model} + files['assets/' + MOD + '/blockstates/' + name + '.json'] = {'variants': variants} + files['assets/' + MOD + '/models/item/' + name + '.json'] = { + 'parent': MOD + ':block/' + name + '_bottom_north'} + files['data/' + MOD + '/loot_table/blocks/' + name + '.json'] = { + 'type': 'minecraft:block', 'pools': [{'rolls': 1, 'entries': [{'type': 'minecraft:item', + 'name': MOD + ':' + name}], 'conditions': [{'condition': 'minecraft:survives_explosion'}]}]} + + slab, step, corner = [MOD + ':powder_snow_' + kind for kind in ('slab', 'step', 'corner')] + manifest = [] + + def recipe(name, body, output, count, inputs): + body.update({'category': 'misc' if output.endswith('_bucket') else 'building', + 'group': 'skys/result/' + output.replace(':', '/'), + 'result': {'id': output, 'count': count}}) + files['data/' + MOD + '/recipe/' + name + '.json'] = body + manifest.append({'recipe': MOD + ':' + name, 'owner': MOD, + 'ingredients': [{'items': [item]} for item in inputs]}) + + recipe('powder_snow_slab', {'type': 'minecraft:crafting_shapeless', + 'ingredients': [{'item': 'minecraft:powder_snow_bucket'}]}, slab, 2, + ['minecraft:powder_snow_bucket']) + files['data/' + MOD + '/recipe/powder_snow_slab.json'] = { + 'type': MOD + ':powder_snow_cutting', 'category': 'building'} + for name, pattern, ingredient, output, count in ( + ('powder_snow_step', ['X ', ' X'], slab, step, 4), + ('powder_snow_corner', ['XX'], step, corner, 4), + ('powder_snow_step_from_corners', ['X', 'X'], corner, step, 1), + ('powder_snow_slab_from_steps', ['X', 'X'], step, slab, 1)): + recipe(name, {'type': 'minecraft:crafting_shaped', 'pattern': pattern, + 'key': {'X': {'item': ingredient}}}, output, count, [ingredient]) + recipe('powder_snow_from_slabs', {'type': 'minecraft:crafting_shapeless', + 'ingredients': [{'item': slab}, {'item': slab}, {'item': 'minecraft:bucket'}]}, + 'minecraft:powder_snow_bucket', 1, [slab]) + return files, manifest + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--write', action='store_true') + options = parser.parse_args() + files, manifest = resources() + target = ROOT / 'src/main/resources' + changed = [] + for relative, body in files.items(): + path = target / relative + expected = json.dumps(body, indent=2) + '\n' + if not path.is_file() or path.read_text(encoding='utf-8') != expected: + changed.append(relative) + if options.write: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(expected, encoding='utf-8', newline='\n') + path = ROOT / 'src/main/recipe-book/recipes.json' + current = json.loads(path.read_text(encoding='utf-8')) + merged = [row for row in current if not row['recipe'].startswith(MOD + ':powder_snow_')] + manifest + if current != merged: + changed.append('recipe-book manifest') + if options.write: + path.write_text(json.dumps(merged, indent=2) + '\n', encoding='utf-8', newline='\n') + print('Powder snow resources:', len(files), 'files,', len(changed), 'changes') + if changed and not options.write: + raise SystemExit(1) + + +if __name__ == '__main__': + main() diff --git a/tools/test_powder_snow_resources.py b/tools/test_powder_snow_resources.py new file mode 100644 index 0000000..369f96b --- /dev/null +++ b/tools/test_powder_snow_resources.py @@ -0,0 +1,35 @@ +"""Keep powder-snow models, recipe identities and bucket accounting explicit.""" +import json +from pathlib import Path +import unittest +from generate_powder_snow import ROOT, resources + + +class PowderSnowResources(unittest.TestCase): + def test_all_generated_resources_match_their_source(self): + expected, _ = resources() + self.assertEqual(39, len(expected)) + for relative, data in expected.items(): + path = ROOT / 'src/main/resources' / relative + self.assertEqual(data, json.loads(path.read_text(encoding='utf-8')), relative) + + def test_only_the_three_flat_piece_ids_are_generated(self): + expected, _ = resources() + blockstates = {Path(path).stem for path in expected if '/blockstates/' in path} + self.assertEqual({'powder_snow_slab', 'powder_snow_step', 'powder_snow_corner'}, blockstates) + for path, model in expected.items(): + if '/models/block/' in path: + for element in model['elements']: + self.assertEqual(8, element['to'][1] - element['from'][1]) + self.assertTrue(all('cullface' not in face for face in element['faces'].values())) + + def test_all_six_recipe_unlocks_remain_in_the_manifest(self): + _, expected = resources() + actual = json.loads((ROOT / 'src/main/recipe-book/recipes.json').read_text(encoding='utf-8')) + actual = [row for row in actual if row['recipe'].startswith('skysterrainsmoother:powder_snow_')] + self.assertEqual(6, len(actual)) + self.assertEqual(expected, actual) + + +if __name__ == '__main__': + unittest.main() From 55c39ebe7bf8ffaf14a1ba3e782772d11c98ecbb Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 15:44:06 +0000 Subject: [PATCH 11/12] Give shovels their normal advantage on powder-snow pieces --- .../data/minecraft/tags/block/mineable/shovel.json | 5 ++++- tools/test_powder_snow_resources.py | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/resources/data/minecraft/tags/block/mineable/shovel.json b/src/main/resources/data/minecraft/tags/block/mineable/shovel.json index c5e145c..10a6a59 100644 --- a/src/main/resources/data/minecraft/tags/block/mineable/shovel.json +++ b/src/main/resources/data/minecraft/tags/block/mineable/shovel.json @@ -2,6 +2,9 @@ "replace": false, "values": [ "skysterrainsmoother:sand_slab", - "skysterrainsmoother:red_sand_slab" + "skysterrainsmoother:red_sand_slab", + "skysterrainsmoother:powder_snow_slab", + "skysterrainsmoother:powder_snow_step", + "skysterrainsmoother:powder_snow_corner" ] } diff --git a/tools/test_powder_snow_resources.py b/tools/test_powder_snow_resources.py index 369f96b..d02b384 100644 --- a/tools/test_powder_snow_resources.py +++ b/tools/test_powder_snow_resources.py @@ -30,6 +30,13 @@ def test_all_six_recipe_unlocks_remain_in_the_manifest(self): self.assertEqual(6, len(actual)) self.assertEqual(expected, actual) + def test_shovels_keep_their_native_advantage_on_all_powder_pieces(self): + tag = json.loads((ROOT / 'src/main/resources/data/minecraft/tags/block/mineable/shovel.json') + .read_text(encoding='utf-8')) + self.assertFalse(tag['replace']) + for shape in ('slab', 'step', 'corner'): + self.assertIn('skysterrainsmoother:powder_snow_' + shape, tag['values']) + if __name__ == '__main__': unittest.main() From 6fb1a1cb1629fbe8dc8f495025eeefcd8585d520 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 17:25:57 +0000 Subject: [PATCH 12/12] Pin the approved content builds for NeoForge 1.21.1 --- gradle/content-dependencies.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index 3922178..c2ebe63 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -1,8 +1,8 @@ grass.version=1.2.1.121012 -grass.commit=d022eba609e6cf0381183232d35768b6190f2d53 +grass.commit=dfc69bddd7f9aa9a8ddde74673d3e5b19286b763 grass.sha256=9A122E63CCAC190246ECA4DD6D5A8D84CA0150A10AF7768142F97EF5DEB8A3C8 grass.development.sha256=9A122E63CCAC190246ECA4DD6D5A8D84CA0150A10AF7768142F97EF5DEB8A3C8 pieces.version=0.3.10.121012 -pieces.commit=708347be4f0ad385d81ca070477628b092fe6925 +pieces.commit=e251b23632f49eb54573bd6ff7373f4d00eb9560 pieces.sha256=322C3D68E40630C6C4672CD74FB438FA08E15FB30FE524B421145A80F7E48573 pieces.development.sha256=322C3D68E40630C6C4672CD74FB438FA08E15FB30FE524B421145A80F7E48573