diff --git a/CHANGELOG.md b/CHANGELOG.md index e457ca8..06ffb54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 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. +- Grass slabs, steps and corners take on their snowy look as the surrounding terrain generates. + ## 0.1.0.121012 - Updated for Minecraft 1.21.1 on NeoForge. diff --git a/README.md b/README.md index f590e22..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 and hardened clay. +sandstone, sand, hardened clay, packed ice, full snow blocks and powder snow. ## Smoothing levels @@ -14,16 +14,20 @@ 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 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 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, @@ -42,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). @@ -49,9 +70,9 @@ 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 Terrain Smoother **0.1.0.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 the BOP add-on, keep its matching dependencies installed too. 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.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/gradle/content-dependencies.properties b/gradle/content-dependencies.properties index ffc56dd..c2ebe63 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=dfc69bddd7f9aa9a8ddde74673d3e5b19286b763 +grass.sha256=9A122E63CCAC190246ECA4DD6D5A8D84CA0150A10AF7768142F97EF5DEB8A3C8 +grass.development.sha256=9A122E63CCAC190246ECA4DD6D5A8D84CA0150A10AF7768142F97EF5DEB8A3C8 +pieces.version=0.3.10.121012 +pieces.commit=e251b23632f49eb54573bd6ff7373f4d00eb9560 +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/FineGrassProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/FineGrassProbe.java index e2f41bf..29a20d8 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,82 @@ 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); + LevelChunk snowChunk = world.getChunkAt(lowerSnow); + if (!lateSnow) { + world.setBlock(higher.above(), snow, 2); + 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); + 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"); + 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); + 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 surrounding 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/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/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/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/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/RuntimeProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java index b245537..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 = 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 new file mode 100644 index 0000000..a28aa59 --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java @@ -0,0 +1,154 @@ +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 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; + +/** 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 has its own non-solid smoothing pieces"); + 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/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java index bae4a45..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; } @@ -38,6 +39,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 +55,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 +84,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 +126,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/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/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/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 166ac9d..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 { @@ -52,6 +55,20 @@ 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); + 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 59980eb..13183ab 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; @@ -223,7 +224,7 @@ 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; } @@ -376,7 +388,7 @@ private int smooth(LevelAccessor world, ChunkAccess owner, int level, boolean af boolean cave = floorY < surfaceY; if (cave && !caveHeadroom(world, owner, support) - || !lower.isCollisionShapeFullBlock(world, support) + || !TerrainGeometry.full(lower, world, support) || !SmoothingFoliage.canReplace(world, owner, target) || lower.hasBlockEntity() || owner.getBlockEntity(target) != null @@ -403,10 +415,13 @@ private int smooth(LevelAccessor world, ChunkAccess owner, int level, boolean af } } - BlockState result = transitionResult(world, support, material, - Footprint.mask(cardinal, diagonal, level), level); + BlockState result = transitionResult(world, owner, support, material, + 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 +486,31 @@ 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)) + || fullCollision(chunk.getBlockState(neighbour)) && isSnow(chunk.getBlockState(neighbour.above())))) { + return state.setValue(SnowyDirtBlock.SNOWY, true); + } + } + 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) { + return state.is(BlockTags.SNOW); + } + 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. @@ -483,13 +523,36 @@ private static boolean sameState(BlockState first, BlockState second) { return first.equals(second); } - private BlockState transitionResult(LevelAccessor world, BlockPos support, TerrainMaterial material, int mask, int level) { + private BlockState transitionResult(LevelAccessor world, ChunkAccess owner, BlockPos support, + TerrainMaterial material, int cardinal, int diagonal, int level, boolean cave) { + int mask = Footprint.mask(cardinal, diagonal, level); + if (mask == 0) { + return null; + } + int lowerEdges = lowerEdges(world, support); int safeMask = Footprint.trimDownhillEdges(mask, lowerEdges); BlockState result = catalogue.result(material, safeMask, level); - // Missing finer shapes fall back to slabs. Check the real result, not just the requested footprint. - return result != null && (touchingEdges(result) & lowerEdges) == 0 ? result : null; + // Missing finer shapes fall back to slabs. Check the actual piece against the downhill edges. + if (result == null || (touchingEdges(result) & lowerEdges) != 0) { + return null; + } + + if (!cave && level >= 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) { @@ -506,7 +569,7 @@ private int touchingEdges(BlockState result) { Integer edges = pieceEdges.get(result); if (edges == null) { - AABB bounds = result.getCollisionShape(EmptyAccess.INSTANCE, BlockPos.ZERO).bounds(); + AABB bounds = TerrainGeometry.shape(result).bounds(); edges = (bounds.minZ == 0 ? 1 : 0) | (bounds.maxX == 1 ? 2 : 0) | (bounds.maxZ == 1 ? 4 : 0) | (bounds.minX == 0 ? 8 : 0); pieceEdges.put(result, edges); @@ -541,7 +604,7 @@ private int lowerEdges(LevelAccessor world, BlockPos support) { BlockPos lowerFloor = beside.below(); BlockState below = neighbour.getBlockState(lowerFloor); - if (fullCollision(below) && below.isCollisionShapeFullBlock(world, lowerFloor) + if (TerrainGeometry.full(below) && TerrainGeometry.full(below, world, lowerFloor) && !below.hasBlockEntity()) { edges |= 1 << direction; } @@ -592,14 +655,16 @@ 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) { + ChunkAccess neighbour = ownerOrLoaded(world, owner, floor); 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 +672,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); } @@ -622,6 +686,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) { diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/TerrainGeometry.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/TerrainGeometry.java new file mode 100644 index 0000000..697f1e6 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/TerrainGeometry.java @@ -0,0 +1,30 @@ +package zone.moddev.mc.skysterrainsmoother.internal; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.PowderSnowBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +/** Powder snow has a real terrain outline even though most entities sink through it. */ +public final class TerrainGeometry { + public static VoxelShape shape(BlockState state) { + return state.getBlock() instanceof PowderSnowBlock + ? state.getShape(EmptyAccess.INSTANCE, BlockPos.ZERO) + : state.getCollisionShape(EmptyAccess.INSTANCE, BlockPos.ZERO); + } + + public static boolean full(BlockState state) { + return !Shapes.joinIsNotEmpty(shape(state), Shapes.block(), BooleanOp.NOT_SAME); + } + + public static boolean full(BlockState state, BlockGetter world, BlockPos position) { + return state.getBlock() instanceof PowderSnowBlock + ? full(state) : state.isCollisionShapeFullBlock(world, position); + } + + private TerrainGeometry() { + } +} diff --git a/src/main/recipe-book/recipes.json b/src/main/recipe-book/recipes.json index 14beff0..247d417 100644 --- a/src/main/recipe-book/recipes.json +++ b/src/main/recipe-book/recipes.json @@ -4,7 +4,9 @@ "owner": "skysterrainsmoother", "ingredients": [ { - "items": ["minecraft:sand"] + "items": [ + "minecraft:sand" + ] } ] }, @@ -13,7 +15,9 @@ "owner": "skysterrainsmoother", "ingredients": [ { - "items": ["skysterrainsmoother:sand_slab"] + "items": [ + "skysterrainsmoother:sand_slab" + ] } ] }, @@ -22,7 +26,9 @@ "owner": "skysterrainsmoother", "ingredients": [ { - "items": ["minecraft:red_sand"] + "items": [ + "minecraft:red_sand" + ] } ] }, @@ -31,7 +37,75 @@ "owner": "skysterrainsmoother", "ingredients": [ { - "items": ["skysterrainsmoother:red_sand_slab"] + "items": [ + "skysterrainsmoother:red_sand_slab" + ] + } + ] + }, + { + "recipe": "skysterrainsmoother:powder_snow_slab", + "owner": "skysterrainsmoother", + "ingredients": [ + { + "items": [ + "minecraft:powder_snow_bucket" + ] + } + ] + }, + { + "recipe": "skysterrainsmoother:powder_snow_step", + "owner": "skysterrainsmoother", + "ingredients": [ + { + "items": [ + "skysterrainsmoother:powder_snow_slab" + ] + } + ] + }, + { + "recipe": "skysterrainsmoother:powder_snow_corner", + "owner": "skysterrainsmoother", + "ingredients": [ + { + "items": [ + "skysterrainsmoother:powder_snow_step" + ] + } + ] + }, + { + "recipe": "skysterrainsmoother:powder_snow_step_from_corners", + "owner": "skysterrainsmoother", + "ingredients": [ + { + "items": [ + "skysterrainsmoother:powder_snow_corner" + ] + } + ] + }, + { + "recipe": "skysterrainsmoother:powder_snow_slab_from_steps", + "owner": "skysterrainsmoother", + "ingredients": [ + { + "items": [ + "skysterrainsmoother:powder_snow_step" + ] + } + ] + }, + { + "recipe": "skysterrainsmoother:powder_snow_from_slabs", + "owner": "skysterrainsmoother", + "ingredients": [ + { + "items": [ + "skysterrainsmoother:powder_snow_slab" + ] } ] } 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" diff --git a/src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_corner.json b/src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_corner.json new file mode 100644 index 0000000..0ab4f07 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_corner.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=north,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_corner_bottom_north" + }, + "facing=east,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_corner_bottom_east" + }, + "facing=south,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_corner_bottom_south" + }, + "facing=west,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_corner_bottom_west" + }, + "facing=north,half=top": { + "model": "skysterrainsmoother:block/powder_snow_corner_top_north" + }, + "facing=east,half=top": { + "model": "skysterrainsmoother:block/powder_snow_corner_top_east" + }, + "facing=south,half=top": { + "model": "skysterrainsmoother:block/powder_snow_corner_top_south" + }, + "facing=west,half=top": { + "model": "skysterrainsmoother:block/powder_snow_corner_top_west" + } + } +} diff --git a/src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_slab.json b/src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_slab.json new file mode 100644 index 0000000..571c43c --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_slab.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=north,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_slab_bottom_north" + }, + "facing=east,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_slab_bottom_east" + }, + "facing=south,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_slab_bottom_south" + }, + "facing=west,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_slab_bottom_west" + }, + "facing=north,half=top": { + "model": "skysterrainsmoother:block/powder_snow_slab_top_north" + }, + "facing=east,half=top": { + "model": "skysterrainsmoother:block/powder_snow_slab_top_east" + }, + "facing=south,half=top": { + "model": "skysterrainsmoother:block/powder_snow_slab_top_south" + }, + "facing=west,half=top": { + "model": "skysterrainsmoother:block/powder_snow_slab_top_west" + } + } +} diff --git a/src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_step.json b/src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_step.json new file mode 100644 index 0000000..3dbc487 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/blockstates/powder_snow_step.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=north,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_step_bottom_north" + }, + "facing=east,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_step_bottom_east" + }, + "facing=south,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_step_bottom_south" + }, + "facing=west,half=bottom": { + "model": "skysterrainsmoother:block/powder_snow_step_bottom_west" + }, + "facing=north,half=top": { + "model": "skysterrainsmoother:block/powder_snow_step_top_north" + }, + "facing=east,half=top": { + "model": "skysterrainsmoother:block/powder_snow_step_top_east" + }, + "facing=south,half=top": { + "model": "skysterrainsmoother:block/powder_snow_step_top_south" + }, + "facing=west,half=top": { + "model": "skysterrainsmoother:block/powder_snow_step_top_west" + } + } +} diff --git a/src/main/resources/assets/skysterrainsmoother/lang/de_at.json b/src/main/resources/assets/skysterrainsmoother/lang/de_at.json index 096ec20..f87b92d 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/de_at.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/de_at.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Mineralogy-Stufen verwenden", "config.skysterrainsmoother.enabled.tooltip": "Glättet neue trockene Hänge und Höhlenböden der Oberwelt. Ausschalten stoppt auch den separaten Grass-Slabs-Durchlauf. Bestehende Chunks bleiben unverändert. Minecraft neu starten.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: Platten. 2: Platten und Viertelblöcke. 3: zusätzlich Achtelblöcke. Fehlende Formen werden nur durch sicher passende Platten ersetzt. Minecraft neu starten.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Nutzt verfügbare Platten natürlicher Mineralogy-Gesteine, wenn die Mod installiert ist. Deaktivierte Plattentypen werden übersprungen. Minecraft neu starten." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Nutzt verfügbare Platten natürlicher Mineralogy-Gesteine, wenn die Mod installiert ist. Deaktivierte Plattentypen werden übersprungen. Minecraft neu starten.", + "block.skysterrainsmoother.powder_snow_slab": "Pulverschneestufe", + "block.skysterrainsmoother.powder_snow_step": "Pulverschnee-Viertelstufe", + "block.skysterrainsmoother.powder_snow_corner": "Pulverschnee-Eckstück" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/de_au.json b/src/main/resources/assets/skysterrainsmoother/lang/de_au.json index 4f63544..1949ce9 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/de_au.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/de_au.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Mineralogy-Stufen verwenden", "config.skysterrainsmoother.enabled.tooltip": "Glättet neue trockene Hänge und Höhlenböden der Oberwelt. Ausschalten stoppt auch den separaten Grass-Slabs-Durchlauf. Bestehende Chunks bleiben unverändert. Minecraft neu starten.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: Platten. 2: Platten und Viertelblöcke. 3: zusätzlich Achtelblöcke. Fehlende Formen werden nur durch sicher passende Platten ersetzt. Minecraft neu starten.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Nutzt verfügbare Platten natürlicher Mineralogy-Gesteine, wenn die Mod installiert ist. Deaktivierte Plattentypen werden übersprungen. Minecraft neu starten." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Nutzt verfügbare Platten natürlicher Mineralogy-Gesteine, wenn die Mod installiert ist. Deaktivierte Plattentypen werden übersprungen. Minecraft neu starten.", + "block.skysterrainsmoother.powder_snow_slab": "Pulverschneestufe", + "block.skysterrainsmoother.powder_snow_step": "Pulverschnee-Viertelstufe", + "block.skysterrainsmoother.powder_snow_corner": "Pulverschnee-Eckstück" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/de_de.json b/src/main/resources/assets/skysterrainsmoother/lang/de_de.json index 4f63544..1949ce9 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/de_de.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/de_de.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Mineralogy-Stufen verwenden", "config.skysterrainsmoother.enabled.tooltip": "Glättet neue trockene Hänge und Höhlenböden der Oberwelt. Ausschalten stoppt auch den separaten Grass-Slabs-Durchlauf. Bestehende Chunks bleiben unverändert. Minecraft neu starten.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: Platten. 2: Platten und Viertelblöcke. 3: zusätzlich Achtelblöcke. Fehlende Formen werden nur durch sicher passende Platten ersetzt. Minecraft neu starten.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Nutzt verfügbare Platten natürlicher Mineralogy-Gesteine, wenn die Mod installiert ist. Deaktivierte Plattentypen werden übersprungen. Minecraft neu starten." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Nutzt verfügbare Platten natürlicher Mineralogy-Gesteine, wenn die Mod installiert ist. Deaktivierte Plattentypen werden übersprungen. Minecraft neu starten.", + "block.skysterrainsmoother.powder_snow_slab": "Pulverschneestufe", + "block.skysterrainsmoother.powder_snow_step": "Pulverschnee-Viertelstufe", + "block.skysterrainsmoother.powder_snow_corner": "Pulverschnee-Eckstück" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/en_ca.json b/src/main/resources/assets/skysterrainsmoother/lang/en_ca.json index 4fd9d74..0a880b6 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/en_ca.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/en_ca.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Use Mineralogy slabs", "config.skysterrainsmoother.enabled.tooltip": "Smooth new dry Overworld slopes and cave floors. Turning this off also stops the separate Grass Slabs pass. Existing chunks stay unchanged. Restart Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: slabs. 2: slabs and steps. 3: slabs, steps and corners. Missing shapes use a slab only where it fits safely. Restart Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Use available natural Mineralogy rock slabs when the mod is installed. Disabled slab types are skipped. Restart Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Use available natural Mineralogy rock slabs when the mod is installed. Disabled slab types are skipped. Restart Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Powder Snow Slab", + "block.skysterrainsmoother.powder_snow_step": "Powder Snow Step", + "block.skysterrainsmoother.powder_snow_corner": "Powder Snow Corner" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/en_en.json b/src/main/resources/assets/skysterrainsmoother/lang/en_en.json index 4fd9d74..0a880b6 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/en_en.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/en_en.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Use Mineralogy slabs", "config.skysterrainsmoother.enabled.tooltip": "Smooth new dry Overworld slopes and cave floors. Turning this off also stops the separate Grass Slabs pass. Existing chunks stay unchanged. Restart Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: slabs. 2: slabs and steps. 3: slabs, steps and corners. Missing shapes use a slab only where it fits safely. Restart Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Use available natural Mineralogy rock slabs when the mod is installed. Disabled slab types are skipped. Restart Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Use available natural Mineralogy rock slabs when the mod is installed. Disabled slab types are skipped. Restart Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Powder Snow Slab", + "block.skysterrainsmoother.powder_snow_step": "Powder Snow Step", + "block.skysterrainsmoother.powder_snow_corner": "Powder Snow Corner" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/en_gb.json b/src/main/resources/assets/skysterrainsmoother/lang/en_gb.json index 4fd9d74..0a880b6 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/en_gb.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/en_gb.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Use Mineralogy slabs", "config.skysterrainsmoother.enabled.tooltip": "Smooth new dry Overworld slopes and cave floors. Turning this off also stops the separate Grass Slabs pass. Existing chunks stay unchanged. Restart Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: slabs. 2: slabs and steps. 3: slabs, steps and corners. Missing shapes use a slab only where it fits safely. Restart Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Use available natural Mineralogy rock slabs when the mod is installed. Disabled slab types are skipped. Restart Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Use available natural Mineralogy rock slabs when the mod is installed. Disabled slab types are skipped. Restart Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Powder Snow Slab", + "block.skysterrainsmoother.powder_snow_step": "Powder Snow Step", + "block.skysterrainsmoother.powder_snow_corner": "Powder Snow Corner" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/en_pt.json b/src/main/resources/assets/skysterrainsmoother/lang/en_pt.json index 4fd9d74..0a880b6 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/en_pt.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/en_pt.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Use Mineralogy slabs", "config.skysterrainsmoother.enabled.tooltip": "Smooth new dry Overworld slopes and cave floors. Turning this off also stops the separate Grass Slabs pass. Existing chunks stay unchanged. Restart Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: slabs. 2: slabs and steps. 3: slabs, steps and corners. Missing shapes use a slab only where it fits safely. Restart Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Use available natural Mineralogy rock slabs when the mod is installed. Disabled slab types are skipped. Restart Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Use available natural Mineralogy rock slabs when the mod is installed. Disabled slab types are skipped. Restart Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Powder Snow Slab", + "block.skysterrainsmoother.powder_snow_step": "Powder Snow Step", + "block.skysterrainsmoother.powder_snow_corner": "Powder Snow Corner" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/en_us.json b/src/main/resources/assets/skysterrainsmoother/lang/en_us.json index 4fd9d74..0a880b6 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/en_us.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/en_us.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Use Mineralogy slabs", "config.skysterrainsmoother.enabled.tooltip": "Smooth new dry Overworld slopes and cave floors. Turning this off also stops the separate Grass Slabs pass. Existing chunks stay unchanged. Restart Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: slabs. 2: slabs and steps. 3: slabs, steps and corners. Missing shapes use a slab only where it fits safely. Restart Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Use available natural Mineralogy rock slabs when the mod is installed. Disabled slab types are skipped. Restart Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Use available natural Mineralogy rock slabs when the mod is installed. Disabled slab types are skipped. Restart Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Powder Snow Slab", + "block.skysterrainsmoother.powder_snow_step": "Powder Snow Step", + "block.skysterrainsmoother.powder_snow_corner": "Powder Snow Corner" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/es_es.json b/src/main/resources/assets/skysterrainsmoother/lang/es_es.json index 0674145..d259818 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/es_es.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/es_es.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Usar losas de Mineralogy", "config.skysterrainsmoother.enabled.tooltip": "Suaviza pendientes secas y suelos de cuevas nuevos del mundo normal. Al desactivarlo también se detiene la generación separada de Grass Slabs. Los chunks existentes no cambian. Reinicia Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: losas. 2: losas y peldaños. 3: losas, peldaños y esquinas. Las formas que faltan solo se sustituyen por losas donde encajan sin empeorar el desnivel. Reinicia Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Usa las losas disponibles de rocas naturales de Mineralogy si está instalado. Omite los tipos de losa desactivados. Reinicia Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Usa las losas disponibles de rocas naturales de Mineralogy si está instalado. Omite los tipos de losa desactivados. Reinicia Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Losa de nieve en polvo", + "block.skysterrainsmoother.powder_snow_step": "Escalón de nieve en polvo", + "block.skysterrainsmoother.powder_snow_corner": "Esquina de nieve en polvo" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/es_mx.json b/src/main/resources/assets/skysterrainsmoother/lang/es_mx.json index 0674145..d259818 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/es_mx.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/es_mx.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Usar losas de Mineralogy", "config.skysterrainsmoother.enabled.tooltip": "Suaviza pendientes secas y suelos de cuevas nuevos del mundo normal. Al desactivarlo también se detiene la generación separada de Grass Slabs. Los chunks existentes no cambian. Reinicia Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: losas. 2: losas y peldaños. 3: losas, peldaños y esquinas. Las formas que faltan solo se sustituyen por losas donde encajan sin empeorar el desnivel. Reinicia Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Usa las losas disponibles de rocas naturales de Mineralogy si está instalado. Omite los tipos de losa desactivados. Reinicia Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Usa las losas disponibles de rocas naturales de Mineralogy si está instalado. Omite los tipos de losa desactivados. Reinicia Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Losa de nieve en polvo", + "block.skysterrainsmoother.powder_snow_step": "Escalón de nieve en polvo", + "block.skysterrainsmoother.powder_snow_corner": "Esquina de nieve en polvo" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/fr_ca.json b/src/main/resources/assets/skysterrainsmoother/lang/fr_ca.json index 3eda14b..62c473d 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/fr_ca.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/fr_ca.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Utiliser les dalles de Mineralogy", "config.skysterrainsmoother.enabled.tooltip": "Adoucit les nouvelles pentes sèches et les sols de grottes du monde normal. Désactiver arrête aussi la génération séparée de Grass Slabs. Les chunks existants ne changent pas. Redémarrez Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1 : dalles. 2 : dalles et quarts de bloc. 3 : dalles, quarts et huitièmes de bloc. Une forme manquante n'est remplacée par une dalle que si elle convient. Redémarrez Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Utilise les dalles disponibles des roches naturelles de Mineralogy si le mod est installé. Les types désactivés sont ignorés. Redémarrez Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Utilise les dalles disponibles des roches naturelles de Mineralogy si le mod est installé. Les types désactivés sont ignorés. Redémarrez Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Dalle de neige poudreuse", + "block.skysterrainsmoother.powder_snow_step": "Quart de neige poudreuse", + "block.skysterrainsmoother.powder_snow_corner": "Coin de neige poudreuse" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/fr_fr.json b/src/main/resources/assets/skysterrainsmoother/lang/fr_fr.json index 3eda14b..62c473d 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/fr_fr.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/fr_fr.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Utiliser les dalles de Mineralogy", "config.skysterrainsmoother.enabled.tooltip": "Adoucit les nouvelles pentes sèches et les sols de grottes du monde normal. Désactiver arrête aussi la génération séparée de Grass Slabs. Les chunks existants ne changent pas. Redémarrez Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1 : dalles. 2 : dalles et quarts de bloc. 3 : dalles, quarts et huitièmes de bloc. Une forme manquante n'est remplacée par une dalle que si elle convient. Redémarrez Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Utilise les dalles disponibles des roches naturelles de Mineralogy si le mod est installé. Les types désactivés sont ignorés. Redémarrez Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Utilise les dalles disponibles des roches naturelles de Mineralogy si le mod est installé. Les types désactivés sont ignorés. Redémarrez Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Dalle de neige poudreuse", + "block.skysterrainsmoother.powder_snow_step": "Quart de neige poudreuse", + "block.skysterrainsmoother.powder_snow_corner": "Coin de neige poudreuse" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/ja_jp.json b/src/main/resources/assets/skysterrainsmoother/lang/ja_jp.json index bff3a64..1d483b8 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/ja_jp.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/ja_jp.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Mineralogyのハーフブロックを使用", "config.skysterrainsmoother.enabled.tooltip": "新しく生成されるオーバーワールドの乾いた斜面と洞窟の床をなだらかにします。無効にするとGrass Slabs独自の生成も停止します。既存のチャンクは変わりません。再起動が必要です。", "config.skysterrainsmoother.smoothingLevel.tooltip": "1:ハーフブロック。2:ハーフブロックと段差用の小ブロック。3:さらに角用の小ブロック。形がない場合は安全に収まる場所だけハーフブロックを使います。再起動が必要です。", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Mineralogy導入時に、利用可能な自然岩のハーフブロックを使います。無効にされた種類は使いません。Minecraftの再起動が必要です。" + "config.skysterrainsmoother.enableMineralogy.tooltip": "Mineralogy導入時に、利用可能な自然岩のハーフブロックを使います。無効にされた種類は使いません。Minecraftの再起動が必要です。", + "block.skysterrainsmoother.powder_snow_slab": "粉雪のハーフブロック", + "block.skysterrainsmoother.powder_snow_step": "粉雪の段", + "block.skysterrainsmoother.powder_snow_corner": "粉雪の角" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/ko_kr.json b/src/main/resources/assets/skysterrainsmoother/lang/ko_kr.json index e0574fe..7490462 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/ko_kr.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/ko_kr.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Mineralogy 반 블록 사용", "config.skysterrainsmoother.enabled.tooltip": "새로 생성되는 오버월드의 마른 경사면과 동굴 바닥을 완만하게 만듭니다. 끄면 Grass Slabs의 별도 생성도 중지됩니다. 기존 청크는 바뀌지 않습니다. 다시 시작해야 합니다.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: 반 블록. 2: 반 블록과 계단 조각. 3: 반 블록, 계단 조각과 모서리 조각. 없는 모양은 안전하게 맞는 곳에서만 반 블록으로 대신합니다. 다시 시작해야 합니다.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Mineralogy가 설치되어 있으면 사용 가능한 자연 암석 반 블록을 사용합니다. 비활성화된 종류는 건너뜁니다. Minecraft를 다시 시작해야 합니다." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Mineralogy가 설치되어 있으면 사용 가능한 자연 암석 반 블록을 사용합니다. 비활성화된 종류는 건너뜁니다. Minecraft를 다시 시작해야 합니다.", + "block.skysterrainsmoother.powder_snow_slab": "가루눈 반 블록", + "block.skysterrainsmoother.powder_snow_step": "가루눈 단", + "block.skysterrainsmoother.powder_snow_corner": "가루눈 모서리" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/pt_br.json b/src/main/resources/assets/skysterrainsmoother/lang/pt_br.json index b9dc4ed..98b5ee1 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/pt_br.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/pt_br.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Usar lajes do Mineralogy", "config.skysterrainsmoother.enabled.tooltip": "Suaviza novas encostas secas e pisos de cavernas do mundo normal. Desativar também interrompe a geração separada do Grass Slabs. Os chunks existentes não mudam. Reinicie o Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: lajes. 2: lajes e peças de um quarto de bloco. 3: também peças de um oitavo de bloco. Formas ausentes só usam uma laje onde ela cabe com segurança. Reinicie o Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Usa as lajes disponíveis de rochas naturais do Mineralogy quando ele está instalado. Tipos de laje desativados são ignorados. Reinicie o Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Usa as lajes disponíveis de rochas naturais do Mineralogy quando ele está instalado. Tipos de laje desativados são ignorados. Reinicie o Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Laje de neve fofa", + "block.skysterrainsmoother.powder_snow_step": "Degrau de neve fofa", + "block.skysterrainsmoother.powder_snow_corner": "Canto de neve fofa" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/pt_pt.json b/src/main/resources/assets/skysterrainsmoother/lang/pt_pt.json index 022ca1d..6b6e646 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/pt_pt.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/pt_pt.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Usar lajes do Mineralogy", "config.skysterrainsmoother.enabled.tooltip": "Suaviza novas encostas secas e pisos de grutas do mundo normal. Desativar também interrompe a geração separada do Grass Slabs. Os chunks existentes não mudam. Reinicie o Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: meios blocos. 2: meios blocos e quartos de bloco. 3: também oitavos de bloco. Formas em falta só usam um meio bloco onde caiba em segurança. Reinicie o Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Usa os degraus disponíveis de rochas naturais do Mineralogy quando está instalado. Os tipos desativados são ignorados. Reinicie o Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Usa os degraus disponíveis de rochas naturais do Mineralogy quando está instalado. Os tipos desativados são ignorados. Reinicie o Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Laje de neve fofa", + "block.skysterrainsmoother.powder_snow_step": "Degrau de neve fofa", + "block.skysterrainsmoother.powder_snow_corner": "Canto de neve fofa" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/ru_ru.json b/src/main/resources/assets/skysterrainsmoother/lang/ru_ru.json index 04b2eab..d15c3dd 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/ru_ru.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/ru_ru.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "Использовать плиты Mineralogy", "config.skysterrainsmoother.enabled.tooltip": "Сглаживает новые сухие склоны и полы пещер обычного мира. Выключение также останавливает отдельную генерацию Grass Slabs. Существующие чанки не меняются. Перезапустите Minecraft.", "config.skysterrainsmoother.smoothingLevel.tooltip": "1: плиты. 2: плиты и четверти блока. 3: также восьмые доли блока. Недостающую форму заменяет плита только там, где она безопасно помещается. Перезапустите Minecraft.", - "config.skysterrainsmoother.enableMineralogy.tooltip": "Использует доступные плиты природных пород Mineralogy, если мод установлен. Отключённые виды плит пропускаются. Перезапустите Minecraft." + "config.skysterrainsmoother.enableMineralogy.tooltip": "Использует доступные плиты природных пород Mineralogy, если мод установлен. Отключённые виды плит пропускаются. Перезапустите Minecraft.", + "block.skysterrainsmoother.powder_snow_slab": "Плита из рыхлого снега", + "block.skysterrainsmoother.powder_snow_step": "Ступень из рыхлого снега", + "block.skysterrainsmoother.powder_snow_corner": "Уголок из рыхлого снега" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/zh_cn.json b/src/main/resources/assets/skysterrainsmoother/lang/zh_cn.json index f913162..bb994e1 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/zh_cn.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/zh_cn.json @@ -6,5 +6,8 @@ "config.skysterrainsmoother.enableMineralogy": "使用 Mineralogy 台阶", "config.skysterrainsmoother.enabled.tooltip": "平滑新生成的主世界干燥斜坡和洞穴地面。关闭后也会停止Grass Slabs的独立生成。已有区块不会改变。需要重启Minecraft。", "config.skysterrainsmoother.smoothingLevel.tooltip": "1:台阶。2:台阶和四分之一方块。3:再加入八分之一方块。缺少的形状仅在安全合适的位置用台阶代替。需要重启Minecraft。", - "config.skysterrainsmoother.enableMineralogy.tooltip": "安装Mineralogy后使用可用的天然岩石台阶。已禁用的台阶类型会跳过。需要重启Minecraft。" + "config.skysterrainsmoother.enableMineralogy.tooltip": "安装Mineralogy后使用可用的天然岩石台阶。已禁用的台阶类型会跳过。需要重启Minecraft。", + "block.skysterrainsmoother.powder_snow_slab": "细雪台阶", + "block.skysterrainsmoother.powder_snow_step": "细雪小台阶", + "block.skysterrainsmoother.powder_snow_corner": "细雪角块" } diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_east.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_east.json new file mode 100644 index 0000000..6335347 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_east.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 8, + 0, + 0 + ], + "to": [ + 16, + 8, + 8 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_north.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_north.json new file mode 100644 index 0000000..00a9ed2 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_north.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 8, + 8, + 8 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_south.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_south.json new file mode 100644 index 0000000..a1ea9c9 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_south.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 8, + 0, + 8 + ], + "to": [ + 16, + 8, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_west.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_west.json new file mode 100644 index 0000000..7bd8339 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_bottom_west.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 0, + 8 + ], + "to": [ + 8, + 8, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_east.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_east.json new file mode 100644 index 0000000..c80629e --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_east.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 8, + 8, + 0 + ], + "to": [ + 16, + 16, + 8 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_north.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_north.json new file mode 100644 index 0000000..5242d92 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_north.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 8, + 0 + ], + "to": [ + 8, + 16, + 8 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_south.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_south.json new file mode 100644 index 0000000..fd43f9d --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_south.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 8, + 8, + 8 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_west.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_west.json new file mode 100644 index 0000000..294fe08 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_corner_top_west.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 8, + 8 + ], + "to": [ + 8, + 16, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_east.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_east.json new file mode 100644 index 0000000..740a967 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_east.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 8, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_north.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_north.json new file mode 100644 index 0000000..740a967 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_north.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 8, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_south.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_south.json new file mode 100644 index 0000000..740a967 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_south.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 8, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_west.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_west.json new file mode 100644 index 0000000..740a967 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_bottom_west.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 8, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_east.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_east.json new file mode 100644 index 0000000..44e4a16 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_east.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 8, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_north.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_north.json new file mode 100644 index 0000000..44e4a16 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_north.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 8, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_south.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_south.json new file mode 100644 index 0000000..44e4a16 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_south.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 8, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_west.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_west.json new file mode 100644 index 0000000..44e4a16 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_slab_top_west.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 8, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_east.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_east.json new file mode 100644 index 0000000..e14c1e6 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_east.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 8, + 0, + 0 + ], + "to": [ + 16, + 8, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_north.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_north.json new file mode 100644 index 0000000..dd34739 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_north.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 8, + 8 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_south.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_south.json new file mode 100644 index 0000000..0176a24 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_south.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 0, + 8 + ], + "to": [ + 16, + 8, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_west.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_west.json new file mode 100644 index 0000000..e4f4c81 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_bottom_west.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 8, + 8, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_east.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_east.json new file mode 100644 index 0000000..6a729a7 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_east.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 8, + 8, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_north.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_north.json new file mode 100644 index 0000000..53d97bd --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_north.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 8, + 0 + ], + "to": [ + 16, + 16, + 8 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_south.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_south.json new file mode 100644 index 0000000..28698e1 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_south.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 8, + 8 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_west.json b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_west.json new file mode 100644 index 0000000..0e05be8 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/powder_snow_step_top_west.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/powder_snow", + "snow": "minecraft:block/powder_snow" + }, + "elements": [ + { + "from": [ + 0, + 8, + 0 + ], + "to": [ + 8, + 16, + 16 + ], + "faces": { + "down": { + "texture": "#snow" + }, + "up": { + "texture": "#snow" + }, + "north": { + "texture": "#snow" + }, + "south": { + "texture": "#snow" + }, + "west": { + "texture": "#snow" + }, + "east": { + "texture": "#snow" + } + } + } + ] +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_corner.json b/src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_corner.json new file mode 100644 index 0000000..8fc3975 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_corner.json @@ -0,0 +1,3 @@ +{ + "parent": "skysterrainsmoother:block/powder_snow_corner_bottom_north" +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_slab.json b/src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_slab.json new file mode 100644 index 0000000..fb89bfe --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_slab.json @@ -0,0 +1,3 @@ +{ + "parent": "skysterrainsmoother:block/powder_snow_slab_bottom_north" +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_step.json b/src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_step.json new file mode 100644 index 0000000..f4fde94 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/item/powder_snow_step.json @@ -0,0 +1,3 @@ +{ + "parent": "skysterrainsmoother:block/powder_snow_step_bottom_north" +} 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/src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_corner.json b/src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_corner.json new file mode 100644 index 0000000..487ee59 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_corner.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "skysterrainsmoother:powder_snow_corner" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_slab.json b/src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_slab.json new file mode 100644 index 0000000..136aeba --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_slab.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "skysterrainsmoother:powder_snow_slab" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_step.json b/src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_step.json new file mode 100644 index 0000000..b835db8 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/loot_table/blocks/powder_snow_step.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "skysterrainsmoother:powder_snow_step" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_corner.json b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_corner.json new file mode 100644 index 0000000..0a098c8 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_corner.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "XX" + ], + "key": { + "X": { + "item": "skysterrainsmoother:powder_snow_step" + } + }, + "category": "building", + "group": "skys/result/skysterrainsmoother/powder_snow_corner", + "result": { + "id": "skysterrainsmoother:powder_snow_corner", + "count": 4 + } +} diff --git a/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_from_slabs.json b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_from_slabs.json new file mode 100644 index 0000000..98357f8 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_from_slabs.json @@ -0,0 +1,20 @@ +{ + "type": "minecraft:crafting_shapeless", + "ingredients": [ + { + "item": "skysterrainsmoother:powder_snow_slab" + }, + { + "item": "skysterrainsmoother:powder_snow_slab" + }, + { + "item": "minecraft:bucket" + } + ], + "category": "misc", + "group": "skys/result/minecraft/powder_snow_bucket", + "result": { + "id": "minecraft:powder_snow_bucket", + "count": 1 + } +} diff --git a/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_slab.json b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_slab.json new file mode 100644 index 0000000..6ddb4db --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_slab.json @@ -0,0 +1,4 @@ +{ + "type": "skysterrainsmoother:powder_snow_cutting", + "category": "building" +} diff --git a/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_slab_from_steps.json b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_slab_from_steps.json new file mode 100644 index 0000000..9129dd5 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_slab_from_steps.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "X", + "X" + ], + "key": { + "X": { + "item": "skysterrainsmoother:powder_snow_step" + } + }, + "category": "building", + "group": "skys/result/skysterrainsmoother/powder_snow_slab", + "result": { + "id": "skysterrainsmoother:powder_snow_slab", + "count": 1 + } +} diff --git a/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_step.json b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_step.json new file mode 100644 index 0000000..5c3bc81 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_step.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "X ", + " X" + ], + "key": { + "X": { + "item": "skysterrainsmoother:powder_snow_slab" + } + }, + "category": "building", + "group": "skys/result/skysterrainsmoother/powder_snow_step", + "result": { + "id": "skysterrainsmoother:powder_snow_step", + "count": 4 + } +} diff --git a/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_step_from_corners.json b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_step_from_corners.json new file mode 100644 index 0000000..b5c4261 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_step_from_corners.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "X", + "X" + ], + "key": { + "X": { + "item": "skysterrainsmoother:powder_snow_corner" + } + }, + "category": "building", + "group": "skys/result/skysterrainsmoother/powder_snow_step", + "result": { + "id": "skysterrainsmoother:powder_snow_step", + "count": 1 + } +} diff --git a/src/main/resources/data/skysterrainsmoother/recipe/red_sand_from_slabs.json b/src/main/resources/data/skysterrainsmoother/recipe/red_sand_from_slabs.json index 8dd9e62..25e3383 100644 --- a/src/main/resources/data/skysterrainsmoother/recipe/red_sand_from_slabs.json +++ b/src/main/resources/data/skysterrainsmoother/recipe/red_sand_from_slabs.json @@ -1,7 +1,7 @@ { "type": "minecraft:crafting_shaped", "category": "building", - "group": "skysterrainsmoother/full_blocks", + "group": "skys/result/minecraft/red_sand", "pattern": [ "SS" ], diff --git a/src/main/resources/data/skysterrainsmoother/recipe/red_sand_slab.json b/src/main/resources/data/skysterrainsmoother/recipe/red_sand_slab.json index d01c68a..a89c4e6 100644 --- a/src/main/resources/data/skysterrainsmoother/recipe/red_sand_slab.json +++ b/src/main/resources/data/skysterrainsmoother/recipe/red_sand_slab.json @@ -1,7 +1,7 @@ { "type": "minecraft:crafting_shaped", "category": "building", - "group": "skysterrainsmoother/slabs", + "group": "skys/result/skysterrainsmoother/red_sand_slab", "pattern": [ "SSS" ], diff --git a/src/main/resources/data/skysterrainsmoother/recipe/sand_from_slabs.json b/src/main/resources/data/skysterrainsmoother/recipe/sand_from_slabs.json index 6fe7262..5b2808f 100644 --- a/src/main/resources/data/skysterrainsmoother/recipe/sand_from_slabs.json +++ b/src/main/resources/data/skysterrainsmoother/recipe/sand_from_slabs.json @@ -1,7 +1,7 @@ { "type": "minecraft:crafting_shaped", "category": "building", - "group": "skysterrainsmoother/full_blocks", + "group": "skys/result/minecraft/sand", "pattern": [ "SS" ], diff --git a/src/main/resources/data/skysterrainsmoother/recipe/sand_slab.json b/src/main/resources/data/skysterrainsmoother/recipe/sand_slab.json index 4944d29..517e925 100644 --- a/src/main/resources/data/skysterrainsmoother/recipe/sand_slab.json +++ b/src/main/resources/data/skysterrainsmoother/recipe/sand_slab.json @@ -1,7 +1,7 @@ { "type": "minecraft:crafting_shaped", "category": "building", - "group": "skysterrainsmoother/slabs", + "group": "skys/result/skysterrainsmoother/sand_slab", "pattern": [ "SSS" ], diff --git a/src/test/java/zone/moddev/mc/skysterrainsmoother/ProjectContractTest.java b/src/test/java/zone/moddev/mc/skysterrainsmoother/ProjectContractTest.java index 5479eb9..8564df5 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")); @@ -58,7 +58,8 @@ void bothSandColoursHaveNativeModelsAndTheirOwnLoot() throws Exception { void sandRecipesConserveVolumeAndKeepTheirPermanentIds() throws Exception { Path recipes = Paths.get("src/main/resources/data/skysterrainsmoother/recipe"); try (java.util.stream.Stream 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..d02b384 --- /dev/null +++ b/tools/test_powder_snow_resources.py @@ -0,0 +1,42 @@ +"""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) + + 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()