diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae6bc99..c55af7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -203,9 +203,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/SkysTerrainSmoother-0.1.1.2603001.jar - build/libs/SkysTerrainSmoother-0.1.1.2603001-sources.jar - build/libs/SkysTerrainSmoother-0.1.1.2603001-javadoc.jar + build/libs/SkysTerrainSmoother-0.1.2.2603001.jar + build/libs/SkysTerrainSmoother-0.1.2.2603001-sources.jar + build/libs/SkysTerrainSmoother-0.1.2.2603001-javadoc.jar build/release/SHA256SUMS CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ca1a8..351d552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.1.2.2603001 + +- Add falling gravel slabs for gravel slopes and sand/gravel transitions, with cutting and combining recipes. +- Smooth snow-block and packed-ice terrain with matching pieces. +- Add powder-snow slabs, steps and corners that retain sinking and freezing. +- Give new grass pieces their snowy appearance straight away, including after snow decoration. +- Keep different crafting results on separate recipe-book buttons. + + ## 0.1.1.2603001 - Gentle surface terraces use full-width slabs at smoothing levels 2 and 3, giving each rise a wider tread. diff --git a/README.md b/README.md index 643b862..01a32b7 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, gravel, hardened clay, snow and packed ice. ## Smoothing levels @@ -32,18 +32,26 @@ ceilings stay unchanged. Cave passages need three clear blocks above the floor, so low tunnels keep their headroom. To see the effect in an existing world, travel beyond the terrain you've already generated. +New grass pieces pick up the surrounding snow straight away. Snow added later +during world generation is checked again before that new chunk is finished. + ## Sand and other materials -Ordinary sand and red sand get their own horizontal slabs. Three matching sand +Ordinary sand, red sand and gravel get their own horizontal slabs. Three matching blocks in a row make six slabs, and two slabs side by side make a full block. -Like sand blocks, the slabs fall when unsupported. Sand uses slabs at every -smoothing level. +Like their full blocks, the slabs fall when unsupported. These materials use +slabs at every smoothing level. Mineralogy is supported without another add-on. When it's installed, the smoother uses its natural rock slabs and the matching steps and corners included in Building Pieces. Disabled native slab types are skipped. -Ores, gravel, wood and decorative stone aren't smoothing materials. +Powder snow gets its own slabs, steps and corners, so it stays soft and keeps +its sinking and freezing behaviour. Leather boots work on the actual partial +surface. Empty parts of a piece do not freeze you or add powder-snow fog. +See [snow and frozen terrain](docs/FROZEN-TERRAIN.md) for the recipes. + +Ores, wood and decorative stone aren't smoothing materials. For BOP grass and soils, add the [Terrain Smoother Biomes O' Plenty add-on](https://github.com/MinecraftModDevelopmentMods/SkysTerrainSmoother-BiomesOPlenty-Addon). @@ -52,9 +60,9 @@ For BOP grass and soils, add the For **Minecraft 26.3**, use **Forge 66.0.2** and **Java 25**, with: -- Sky's Grass Slabs **1.2.0.2603001**. -- Sky's Building Pieces **0.3.9.2603001**. -- Sky's Terrain Smoother **0.1.1.2603001**. +- Sky's Grass Slabs **1.2.1.2603001**. +- Sky's Building Pieces **0.3.10.2603001**. +- Sky's Terrain Smoother **0.1.2.2603001**. Install all three main jars on both the client and server. diff --git a/build.gradle b/build.gradle index abd1a2f..9e53d33 100644 --- a/build.gradle +++ b/build.gradle @@ -593,7 +593,7 @@ tasks.named('jar', Jar) { 'Implementation-Title': base.archivesName.get(), 'Implementation-Version': project.version, 'Implementation-Vendor': project.mod_authors, - 'Built-On-Java': '21', + 'Built-On-Java': project.java_version, 'Built-On': "${project.minecraft_version}-${project.forge_version}" ]) } @@ -649,6 +649,10 @@ tasks.register('verifyReleaseArtifacts') { } ZipFile zip = new ZipFile(new File(libs, releaseFiles.get()[0])) try { + def manifest = new java.util.jar.Manifest(zip.getInputStream(zip.getEntry('META-INF/MANIFEST.MF'))) + if (manifest.mainAttributes.getValue('Built-On-Java') != project.java_version.toString()) { + throw new GradleException('The production manifest has the wrong Java version') + } Set names = zip.entries().collect { it.name } as Set ['META-INF/mods.toml', 'META-INF/LICENSE', 'META-INF/LICENSE.spdx', 'META-INF/NOTICE', 'zone/moddev/mc/skysterrainsmoother/SkysTerrainSmoother.class'].each { required -> @@ -676,7 +680,7 @@ tasks.register('verifyReleaseArtifacts') { byte[] auditBytes = zip.getInputStream(entry).bytes if (entry.name.endsWith('.class') && (((auditBytes[6] & 255) << 8) | (auditBytes[7] & 255)) != 69) { - throw new GradleException("Non-Java-21 production class: ${entry.name}") + throw new GradleException("Non-Java-25 production class: ${entry.name}") } String content = new String(zip.getInputStream(entry).bytes, StandardCharsets.ISO_8859_1).toLowerCase(Locale.ROOT) @@ -1135,3 +1139,9 @@ tasks.register('integrationForwardTest') { } tasks.named('check') { dependsOn tasks.named('integrationForwardTest') } + +tasks.register('verifyCraftingResources', Exec) { + group = 'verification' + commandLine providers.gradleProperty('pythonExecutable').getOrElse('python'), 'tools/test_powder_snow_resources.py' +} +tasks.named('check') { dependsOn tasks.named('verifyCraftingResources') } diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 3a5187f..19c8d33 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -10,8 +10,8 @@ Build the exact Grass Slabs and Building Pieces source commits listed in `gradle/content-dependencies.properties` with their `assemble` tasks first. The build checks both their source commits and main/development jar checksums. -The source lock records locally qualified Grass Slabs 1.2.0.2603001 and Building Pieces -0.3.9.2603001. +The source lock records Grass Slabs 1.2.1.2603001 and Building Pieces +0.3.10.2603001, with the checksums of their released jars. The default layout uses sibling checkouts. For another layout, pass `-PgrassSlabsDirectory=` and @@ -56,10 +56,10 @@ do not prevent an ordinary build; `prepareMineralogyDevelopmentRun` explains what is needed before the combined profile can run. The jars are copied, not modified. Development and packaged runs use the same jars. -If a sibling build has moved on, keep the qualified jars under -`.gradle/qualification-dependencies/` with their original filenames. -The profiles prefer those copies, but still require the locked checksums. -An explicit runtime-jar property takes precedence over either location. +The profiles keep verified copies under `.gradle/qualification-dependencies/`, +keyed by their locked checksums. Later sibling builds can then change without +breaking Eclipse refreshes. An explicit runtime-jar property takes precedence; +it must still match the lock. New configurations use smoothing level 3. An existing saved level is kept, so choose level 3 in Mods > Config and restart if that test folder already has a @@ -75,7 +75,7 @@ resources there. Neither task opens the manual profile's saved worlds. The build generates unlock advancements from `src/main/recipe-book/recipes.json`. The runtime checks compare those entries with the registered recipes and exercise inventory discovery, crafting-table discovery and recipe-book persistence. -All four sand recipes retain their original IDs. +Sand and gravel recipes retain their existing IDs. Recipe unlocks have no shared advancement parent. This avoids a long recursive visibility chain when loading completed progress. Inventory and crafting-table diff --git a/docs/FROZEN-TERRAIN.md b/docs/FROZEN-TERRAIN.md new file mode 100644 index 0000000..88545bf --- /dev/null +++ b/docs/FROZEN-TERRAIN.md @@ -0,0 +1,25 @@ +# Snow and frozen terrain + +Snow-block and packed-ice slopes use matching Building Pieces slabs, steps and +corners. Powder snow uses separate pieces that keep its sinking, freezing and +leather-boot behaviour. All three smoothing levels work with these materials. +Existing terrain stays unchanged. + +Grass transitions also pick up snow from nearby ground and exposed cold slopes. +Snowy pieces cover the visible grass beneath them without changing that grass +block in the saved world. Removing or thawing a piece restores the normal look. + +## Crafting powder-snow pieces + +- One powder-snow bucket makes two slabs and returns an empty bucket. +- Two slabs placed diagonally make four steps. +- Two steps side by side make four corners. +- Two corners stacked in the crafting grid make one step. +- Two steps stacked in the grid make one slab. +- Two slabs and an empty bucket make a powder-snow bucket. + +Matching pieces also combine when placed together. A partial piece cannot fill +a bucket on its own; rebuild it first. Breaking a piece returns that piece. + +For solid snow slabs, place two snow blocks diagonally to make four slabs. +The vanilla recipe of three snow blocks in a row still makes snow layers. diff --git a/gradle.properties b/gradle.properties index cc6edb3..5481555 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,7 @@ release_enabled=true curseforge_project_id=1768953 mod_name=Sky's Terrain Smoother mod_license=LGPL-2.1-only -mod_version=0.1.1.2603001 +mod_version=0.1.2.2603001 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 f4cc065..6372643 100644 --- a/gradle/content-dependencies.properties +++ b/gradle/content-dependencies.properties @@ -1,13 +1,13 @@ -# Exact local content candidates. Publication remains separate. +# Released content dependencies used by this branch. grass.repository=MinecraftModDevelopmentMods/SkysGrassSlabs -grass.kind=local-candidate -grass.commit=014f8d16f0a5ef83c4cc236d37590afac56a6087 -grass.version=1.2.0.2603001 -grass.sha256=20AE5DC12C97E8DC1D348282BE50697909E98BBCE8C3BD40E4CF910E7D85F7D9 -grass.development.sha256=20AE5DC12C97E8DC1D348282BE50697909E98BBCE8C3BD40E4CF910E7D85F7D9 +grass.kind=release +grass.commit=225e5eb176cbfff7389cb2b1c0f581c5b7f62eed +grass.version=1.2.1.2603001 +grass.sha256=55E417284B1F5982E88EF4B2B69F8B766A7EA473F3AE30DE9CE3AF8E2D988DFA +grass.development.sha256=55E417284B1F5982E88EF4B2B69F8B766A7EA473F3AE30DE9CE3AF8E2D988DFA pieces.repository=MinecraftModDevelopmentMods/SkysBuildingPieces -pieces.kind=local-candidate -pieces.commit=c4df80b09d366b293cd3d06b5b92b376b34fafe3 -pieces.version=0.3.9.2603001 -pieces.sha256=A21755D614D3953D08CE1545153800DA1722A63EEC330BC868C8C1A7DF76D298 -pieces.development.sha256=A21755D614D3953D08CE1545153800DA1722A63EEC330BC868C8C1A7DF76D298 +pieces.kind=release +pieces.commit=80865bc1ce045bd3f271f35b47415d9b3acbd99e +pieces.version=0.3.10.2603001 +pieces.sha256=103D5B1DA1F123407DDEB453840AAE652F6F798D8CFB8E3ACCEA1F6FCB1E686D +pieces.development.sha256=103D5B1DA1F123407DDEB453840AAE652F6F798D8CFB8E3ACCEA1F6FCB1E686D diff --git a/gradle/mineralogy-runs.gradle b/gradle/mineralogy-runs.gradle index 7133c82..2d41c13 100644 --- a/gradle/mineralogy-runs.gradle +++ b/gradle/mineralogy-runs.gradle @@ -4,9 +4,15 @@ import java.nio.file.Files // These are optional development mods, never compile or production dependencies. def runtimeLock = new Properties() file('gradle/mineralogy-runtime.properties').withInputStream { runtimeLock.load(it) } +def retainedRuntimeJars = ['mineralogy', 'orespawn'].collectEntries { mod -> + File siblingJar = file(runtimeLock.getProperty(mod + '.development.path')) + String checksum = runtimeLock.getProperty(mod + '.development.sha256') + [(mod): file(".gradle/qualification-dependencies/${checksum}/${siblingJar.name}")] +} + def runtimeSources = ['mineralogy', 'orespawn'].collectEntries { mod -> File siblingJar = file(runtimeLock.getProperty(mod + '.development.path')) - File retainedJar = file('.gradle/qualification-dependencies/' + siblingJar.name) + File retainedJar = retainedRuntimeJars[mod] // Sibling builds can change during other work. A retained copy still has to // pass the same checksum check before it can enter a development profile. [(mod): file(providers.gradleProperty(mod + 'RuntimeJar') @@ -65,6 +71,14 @@ def installRuntimeMods = { throw new GradleException("The ordinary Mineralogy profile must not contain test mods: ${candidate}") } } + runtimeSources.each { mod, source -> + File retained = retainedRuntimeJars[mod] + if (source.canonicalFile != retained.canonicalFile) { + // Keep this verified build when another project rebuilds the sibling jar. + Files.createDirectories(retained.parentFile.toPath()) + Files.copy(source.toPath(), retained.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) + } + } // Copy only the declared jars. Never sync or clear a player's mods, config or saves. copy { from runtimeSources.values(), extraMods diff --git a/gradle/release.gradle b/gradle/release.gradle index fb0b114..607c740 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -32,7 +32,7 @@ tasks.register('verifyReleaseBundleContract') { group = 'verification' dependsOn tasks.named('writeReleaseChecksums') doLast { - File bundle = layout.buildDirectory.dir('release-bundle-test').get().asFile + File bundle = layout.buildDirectory.dir("release-bundle-test/${project.version}").get().asFile List filenames = releaseFiles.get().collect { it.toString() } copy { from layout.buildDirectory.dir('libs') diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/CaveProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/CaveProbe.java index bd3e038..1d94bb7 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/CaveProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/CaveProbe.java @@ -118,7 +118,7 @@ static int run(ServerLevel world) { checks++; for (BlockState source : new BlockState[] { Blocks.COAL_ORE.defaultBlockState(), - Blocks.GRAVEL.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState(), Blocks.INFESTED_STONE.defaultBlockState(), + Blocks.COAL_BLOCK.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState(), Blocks.INFESTED_STONE.defaultBlockState(), Blocks.CHEST.defaultBlockState() }) { fixture(world, floor, 6); world.setBlock(floor, source, 2); 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 13618e2..8469104 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/ClientProbe.java @@ -37,6 +37,7 @@ public void tick(TickEvent.ClientTickEvent.Post event) { return; } RecipePreviewModels.check(minecraft, recipeDisplays); + PowderSnowClientProbe.run(minecraft); checkBiomeTints(minecraft); zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_CLIENT_WORLD_PASS"); minecraft.stop(); @@ -57,7 +58,8 @@ public void tick(TickEvent.ClientTickEvent.Post event) { for (net.minecraft.world.level.block.Block block : new net.minecraft.world.level.block.Block[] { zone.moddev.mc.skysterrainsmoother.content.SandContent.slab, - zone.moddev.mc.skysterrainsmoother.content.SandContent.redSlab }) { + zone.moddev.mc.skysterrainsmoother.content.SandContent.redSlab, + zone.moddev.mc.skysterrainsmoother.content.SandContent.gravelSlab }) { for (net.minecraft.world.level.block.state.BlockState state : block.getStateDefinition().getPossibleStates()) { checkModel(minecraft, state); } 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 f042e16..2b1d95b 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, 0); + } + + 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, 0); + world.getChunkAt(higher).setBlockState(higher.above(), snow, 0); + world.getChunkAt(higher).setBlockState(higher, + Blocks.GRASS_BLOCK.defaultBlockState().setValue(GrassBlock.SNOWY, true), 0); + owner.setBlockState(floor, Blocks.DIAMOND_ORE.defaultBlockState(), 0); + 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..da49297 --- /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.getMaxY() - 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 1caff13..217e721 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/LateTerrainProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/LateTerrainProbe.java @@ -102,7 +102,7 @@ static int run(ServerLevel world) { for (BlockState occupied : new BlockState[] { Blocks.SHORT_GRASS.defaultBlockState(), Blocks.OAK_SAPLING.defaultBlockState(), Blocks.WATER.defaultBlockState(), Blocks.GLASS.defaultBlockState() }) { - fixture(world, floor, Blocks.GRAVEL.defaultBlockState()); + fixture(world, floor, Blocks.COAL_BLOCK.defaultBlockState()); SkysTerrainSmoother.engine.smooth(world, owner, level); world.setBlock(floor, Blocks.GRASS_BLOCK.defaultBlockState(), 2); world.setBlock(floor.offset(DX[direction], 1, DZ[direction]), clay, 2); @@ -154,6 +154,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..62b9039 --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/PowderSnowClientProbe.java @@ -0,0 +1,95 @@ +package zone.moddev.mc.skysterrainsmoother.probe; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart; +import net.minecraft.client.renderer.fog.FogData; +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.minecraftforge.client.event.ViewportEvent; +import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; +import zone.moddev.mc.skysterrainsmoother.content.PowderSnowContent; +import zone.moddev.mc.skysterrainsmoother.content.PowderSnowPieceBlock; + +/** Inspect the baked models and real viewport buses inside and outside each powder-snow 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.setLevel(client.level); + camera.setEntity(client.player); + 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 models = client.getModelManager().getBlockStateModelSet(); + var model = models.get(state); + RuntimeProbe.require(model != models.missingModel(), "Every powder orientation has a model"); + List parts = new ArrayList<>(); + model.collectParts(RandomSource.create(0), parts); + int faces = 0; + for (var part : parts) { + for (Direction face : new Direction[] {null, Direction.UP, Direction.DOWN, + Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) { + for (var quad : part.getQuads(face)) { + RuntimeProbe.require(!quad.materialInfo().isTinted() && quad.materialInfo().sprite() + .contents().name().toString().equals("minecraft:block/powder_snow"), + "Powder models use native, untinted powder snow"); + faces++; + } + } + } + RuntimeProbe.require(faces == 6, "A powder piece keeps all six exposed faces"); + 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); + camera.point(Vec3.atLowerCornerOf(position).add(inside.x, half == Half.BOTTOM ? .75 : .25, inside.z)); + 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); + ViewportEvent.ComputeFogColor.BUS.post(color); + FogData distances = new FogData(); + distances.environmentalEnd = 128; + var fog = new ViewportEvent.RenderFog(FogType.NONE, camera, 0, distances, new org.joml.Vector4f(1)); + boolean cancelled = ViewportEvent.RenderFog.BUS.post(fog); + RuntimeProbe.require(inside ? color.getRed() == .623F && color.getGreen() == .734F && color.getBlue() == .785F + && cancelled && fog.getFarPlaneDistance() == 2 + : color.getRed() == 1 && color.getGreen() == 1 && color.getBlue() == 1 && !cancelled, + "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..efdf026 --- /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.EntityTypes; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.animal.pig.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(EntityTypes.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); + pig.applyEffectsFromBlocks(pig.position(), pig.position()); + 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); + pig.applyEffectsFromBlocks(pig.position(), pig.position()); + 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.getServer().getRecipeManager().getRecipes().stream().filter(holder -> holder.value() instanceof net.minecraft.world.item.crafting.CraftingRecipe crafting && crafting.matches(bucket, world)).toList(); + require(cut.size() == 1, "One powder bucket has one cutting recipe"); + require(ItemStack.matches(new ItemStack(slab, 2), ((net.minecraft.world.item.crafting.CraftingRecipe) cut.getFirst().value()).assemble(bucket)) + && ((net.minecraft.world.item.crafting.CraftingRecipe) 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.getServer().getRecipeManager().getRecipes().stream().noneMatch(holder -> holder.value() instanceof net.minecraft.world.item.crafting.CraftingRecipe crafting && crafting.matches(noBucket, world)), + "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.getServer().getRecipeManager().getRecipes().stream().filter(holder -> holder.value() instanceof net.minecraft.world.item.crafting.CraftingRecipe crafting && crafting.matches(input, world)).toList(); + require(matches.size() == 1 && ItemStack.matches(new ItemStack(output, count), + ((net.minecraft.world.item.crafting.CraftingRecipe) matches.getFirst().value()).assemble(input)), + "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 9b25958..b4e8ef6 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,18 @@ 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().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME) + .getOrThrow(net.minecraft.world.level.biome.Biomes.SNOWY_PLAINS); + var serverWorld = (net.minecraft.server.level.ServerLevel) world; + // 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) -> biome); + } + } + } + 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 3c0d2a8..567302a 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/RuntimeProbe.java @@ -201,7 +201,7 @@ public void tick(TickEvent.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) @@ -281,11 +281,12 @@ private static int decorationChanges(ServerLevel world) { require(!world.isEmptyBlock(position.above()), "regression starts with an actual generated piece"); world.setBlock(position, replacement.defaultBlockState(), 2); ProbeWorld.repair(world, new BlockPos(owner.getPos().x() << 4, 0, owner.getPos().z() << 4)); - require(replacement == Blocks.SAND - ? world.getBlockState(position.above()) - .getBlock() == zone.moddev.mc.skysterrainsmoother.content.SandContent.slab - : world.isEmptyBlock(position.above()), - "decoration retargets sand and removes unsupported gravel/air caps"); + Block expectedCap = replacement == Blocks.SAND + ? zone.moddev.mc.skysterrainsmoother.content.SandContent.slab + : replacement == Blocks.GRAVEL + ? zone.moddev.mc.skysterrainsmoother.content.SandContent.gravelSlab : Blocks.AIR; + require(world.getBlockState(position.above()).getBlock() == expectedCap, + "decoration retargets falling terrain and removes unsupported air caps"); require(world.getBlockState(position).getBlock() == replacement, "decorator's terrain is not reverted"); checks += 3; } @@ -353,7 +354,7 @@ private static int decorationChanges(ServerLevel world) { } } - world.setBlock(beach, Blocks.GRAVEL.defaultBlockState(), 2); + world.setBlock(beach, Blocks.COAL_BLOCK.defaultBlockState(), 2); SkysTerrainSmoother.engine.smooth(world, beachOwner, 1); require(world.isEmptyBlock(beach.above()), "unsupported first pass leaves no piece"); world.setBlock(beach, Blocks.RED_SAND.defaultBlockState(), 2); @@ -467,8 +468,20 @@ private static int persistence(ServerLevel world) { index++; if (phase().equals("reload")) { - require(world.getBlockState(pos).equals(expected), - "saved fine piece retains identity and orientation"); + BlockState actual = world.getBlockState(pos); + if (actual.hasProperty(net.minecraft.world.level.block.state.properties.BlockStateProperties.SNOWY)) { + PieceBlock piece = (PieceBlock) actual.getBlock(); + require(actual.getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.SNOWY) + == piece.getActualState(actual, world, pos) + .getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.SNOWY), + "saved grass pieces retain the correct local snow appearance"); + // Snow is a calculated appearance; it does not change a piece's identity or orientation. + actual = actual.setValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.SNOWY, + expected.getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.SNOWY)); + } + require(actual.equals(expected), + "saved fine piece retains identity and orientation: expected=" + expected + + " actual=" + world.getBlockState(pos)); } else { world.setBlock(pos, expected, 2); } @@ -512,6 +525,16 @@ private static int grass(ServerLevel world) { world.getChunk(position.offset(-8, 0, -8)); world.getChunk(position.offset(8, 0, 8)); + // This test checks grass growth and orientation. Keep its climate warm; + // the separate snowy-terrain fixtures exercise cold ground and thawing. + var warmBiome = world.registryAccess().lookupOrThrow(Registries.BIOME) + .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS); + for (int chunkX = (position.getX() >> 4) - 1; chunkX <= (position.getX() >> 4) + 1; chunkX++) { + for (int chunkZ = (position.getZ() >> 4) - 1; chunkZ <= (position.getZ() >> 4) + 1; chunkZ++) { + world.getChunk(chunkX, chunkZ).fillBiomesFromNoise((quartX, quartY, quartZ) -> warmBiome); + } + } + for (Shape shape : new Shape[] { Shape.HORIZONTAL_STEP, Shape.VERTICAL_STEP, Shape.CORNER, Shape.STAIRS }) { for (int direction = 0; direction < shape.states; direction++) { BlockState dirt = Pieces.state("minecraft:dirt", shape, direction); diff --git a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SandProbe.java b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SandProbe.java index 97705f3..feee886 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SandProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SandProbe.java @@ -28,20 +28,26 @@ import zone.moddev.mc.skysterrainsmoother.content.SandContent; import zone.moddev.mc.skysterrainsmoother.content.SandSlabBlock; -/** Exercise the registered sand items, native falling entities and real recipe manager. */ +/** Exercise falling terrain pieces through native entities, placement and crafting. */ final class SandProbe { static int run(ServerLevel world) { ServerPlayer player = ProbeItems.player(world, new GameProfile( UUID.fromString("14581a55-6ff1-4c98-834f-3f41057e395b"), "SandProbe")); player.getAbilities().instabuild = false; int checks = 0; - for (int colour = 0; colour < 2; colour++) { - SandSlabBlock slab = colour == 0 ? SandContent.slab : SandContent.redSlab; - BlockPos position = new BlockPos(1200 + colour * 8, 220, 1200); + SandSlabBlock[] slabs = {SandContent.slab, SandContent.redSlab, SandContent.gravelSlab}; + for (int materialIndex = 0; materialIndex < slabs.length; materialIndex++) { + SandSlabBlock slab = slabs[materialIndex]; + BlockPos position = new BlockPos(1200 + materialIndex * 8, 220, 1200); player.setPos(position.getX() + 4, 220, position.getZ() + 4); world.getChunk(position); for (int orientation = 0; orientation < 2; orientation++) { - BlockState state = SandContent.legacyState(colour * 2 + orientation); + BlockState state = slab.defaultBlockState().setValue(SandSlabBlock.TYPE, orientation == 0 + ? net.minecraft.world.level.block.state.properties.SlabType.TOP + : net.minecraft.world.level.block.state.properties.SlabType.BOTTOM); + var mined = net.minecraft.world.level.block.Block.getDrops(state, world, position, null); + RuntimeProbe.require(mined.size() == 1 && mined.get(0).is(slab.asItem()) && mined.get(0).getCount() == 1, + "Mining either half drops its matching slab rather than a full block or flint"); for (int height = 215; height <= 224; height++) { ProbeWorld.clear(world, new BlockPos(position.getX(), height, position.getZ())); } @@ -51,13 +57,13 @@ static int run(ServerLevel world) { List falling = world.getEntitiesOfClass(FallingBlockEntity.class, new AABB(position).inflate(1)); falling.removeIf(entity -> !entity.isAlive()); - RuntimeProbe.require(falling.size() == 1, "Sand uses one native falling entity"); + RuntimeProbe.require(falling.size() == 1, "A falling slab uses one native entity"); FallingBlockEntity entity = falling.get(0); for (int tick = 0; tick < 100 && entity.isAlive(); tick++) { entity.tick(); } RuntimeProbe.require(!entity.isAlive() && world.getBlockState(position.below(4)).equals(state), - "Both sand colours and halves survive falling"); + "Both halves keep their material when falling"); world.setBlock(position, state, 2); slab.tick(state, world, position, RandomSource.create(1)); falling = world.getEntitiesOfClass(FallingBlockEntity.class, new AABB(position).inflate(1)); @@ -69,9 +75,9 @@ static int run(ServerLevel world) { entity.tick(); List drops = world.getEntitiesOfClass(ItemEntity.class, new AABB(position).inflate(2)); RuntimeProbe.require(drops.stream().anyMatch(drop -> drop.getItem().getItem() == slab.asItem()), - "An unsuccessful fall drops the correct sand colour"); + "An unsuccessful fall drops the matching slab"); drops.forEach(ItemEntity::discard); - checks += 4; + checks += 5; } world.setBlock(position.below(), Blocks.STONE.defaultBlockState(), 2); ProbeWorld.clear(world, position); @@ -79,11 +85,11 @@ static int run(ServerLevel world) { ItemStack held = new ItemStack(slab, 2); player.setItemInHand(InteractionHand.MAIN_HAND, held); RuntimeProbe.require(use(player, position.below(), Direction.UP, 1).consumesAction(), - "Bottom sand placement succeeds"); - RuntimeProbe.require(world.getBlockState(position).equals(SandContent.legacyState(colour * 2 + 1)), - "The held colour places a bottom half"); + "Bottom slab placement succeeds"); + RuntimeProbe.require(world.getBlockState(position).equals(slab.defaultBlockState()), + "The held material places a bottom half"); RuntimeProbe.require(use(player, position, Direction.UP, .5) == InteractionResult.SUCCESS - && world.getBlockState(position).equals(slab.fullState()), "Matching halves become ordinary sand"); + && world.getBlockState(position).equals(slab.fullState()), "Matching halves become their native full block"); RuntimeProbe.require(held.isEmpty(), "Joining consumes exactly two slab items"); CraftingContainer grid = new TransientCraftingContainer(new AbstractContainerMenu(null, 0) { @Override @@ -95,16 +101,62 @@ public ItemStack quickMoveStack(Player player, int slot) { public boolean stillValid(Player player) { return true; } - }, 2, 2); + }, 3, 3); grid.setItem(0, new ItemStack(slab)); grid.setItem(1, new ItemStack(slab)); ItemStack result = world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, grid.asCraftInput(), world) - .orElseThrow(() -> new AssertionError("Sand recombination recipe missing")).value().assemble(grid.asCraftInput()); + .orElseThrow(() -> new AssertionError("Slab recombination recipe missing")).value().assemble(grid.asCraftInput()); RuntimeProbe.require(ItemStack.matches(new ItemStack(slab.fullState().getBlock()), result), - "Two sand slabs craft the matching full block"); - checks += 5; + "Two slabs craft the matching full block"); + grid.clearContent(); + for (int slot = 0; slot < 3; slot++) { + grid.setItem(slot, new ItemStack(slab.fullState().getBlock())); + } + ItemStack cut = world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, grid.asCraftInput(), world) + .orElseThrow(() -> new AssertionError("Falling slab cutting recipe missing")).value().assemble(grid.asCraftInput()); + RuntimeProbe.require(ItemStack.matches(new ItemStack(slab, 6), cut), + "Three full blocks cut into six matching slabs"); + checks += 6; } zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info("TERRAIN_SMOOTHER_SAND_PASS checks=" + checks); + return checks + mixedShoreTransitions(world); + } + + private static int mixedShoreTransitions(ServerLevel world) { + BlockPos floor = new BlockPos(1304, 200, 1304); + var owner = world.getChunkAt(floor); + SandSlabBlock[] pieces = {SandContent.slab, SandContent.redSlab, SandContent.gravelSlab}; + int checks = 0; + for (int level = 1; level <= 3; level++) { + for (SandSlabBlock lower : pieces) { + for (SandSlabBlock higher : pieces) { + for (boolean snow : new boolean[] {false, true}) { + for (int blockZ = -3; blockZ <= 3; blockZ++) { + for (int blockX = -3; blockX <= 3; blockX++) { + for (int height = 0; height <= 4; height++) { + ProbeWorld.clear(world, floor.offset(blockX, height, blockZ)); + } + world.setBlock(floor.offset(blockX, 0, blockZ), lower.fullState(), 2); + } + } + world.setBlock(floor.north().above(), higher.fullState(), 2); + if (snow) { + world.setBlock(floor.above(), Blocks.SNOW.defaultBlockState(), 2); + world.setBlock(floor.north().above(2), Blocks.SNOW.defaultBlockState(), 2); + } + ProbeWorld.rebuildHeights(owner); + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.engine.smooth(world, owner, level); + RuntimeProbe.require(world.getBlockState(floor.above()).equals(lower.defaultBlockState()), + "Mixed sand/gravel shores use the lower material at level " + level + ", snow=" + snow); + RuntimeProbe.require(world.getBlockState(floor).equals(lower.fullState()), + "Smoothing leaves the shore's original support material alone"); + checks += 2; + } + } + } + } + zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother.logger.info( + "TERRAIN_MIXED_SHORES_PASS checks={} levels=3 snow=true", checks); return checks; } 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..bdbd671 --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/SnowTerrainProbe.java @@ -0,0 +1,153 @@ +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.SnowyBlock; +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("id", 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(SnowyBlock.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(world, world, target, Direction.SOUTH, target.south(), + world.getBlockState(target.south()), RandomSource.create(1)); + RuntimeProbe.require(snowy.getValue(SnowyBlock.SNOWY), + "a raised snow-covered neighbour gives the reported piece its snow cap: " + dry); + world.getChunkAt(target).setBlockState(target, dry, 0); + dry.randomTick(world, target, RandomSource.create(1)); + RuntimeProbe.require(world.getBlockState(target).getValue(SnowyBlock.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.getMinY() >> 2; quartY < world.getMaxY() >> 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 registry = world.registryAccess().lookupOrThrow(Registries.BIOME); + var cold = registry.getOrThrow(Biomes.SNOWY_PLAINS); + var warm = registry.getOrThrow(Biomes.DESERT); + int checks = 0; + try { + owner.fillBiomesFromNoise((quartX, quartY, quartZ) -> cold); + 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.getMaxY(); 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(SnowyBlock.SNOWY) + && piece.getValue(SnowyBlock.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(SnowyBlock.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(SnowyBlock.SNOWY), + "sheltered pieces keep their green appearance"); + ProbeWorld.clear(world, target.above(3)); + owner.fillBiomesFromNoise((quartX, quartY, quartZ) -> warm); + world.getBlockState(target).randomTick(world, target, RandomSource.create(1)); + RuntimeProbe.require(!world.getBlockState(target).getValue(SnowyBlock.SNOWY), + "exposed warm surfaces keep their green appearance"); + owner.fillBiomesFromNoise((quartX, quartY, quartZ) -> cold); + 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(SnowyBlock.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) -> + originalBiomes.get(new BlockPos(quartX, quartY, quartZ))); + } + 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 2114e9f..cf7c6b2 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java +++ b/src/integrationTest/java/zone/moddev/mc/skysterrainsmoother/probe/TransitionProbe.java @@ -20,7 +20,8 @@ static int run(ServerLevel world) { int checks = 0; for (BlockState source : new BlockState[] { Blocks.STONE.defaultBlockState(), - Blocks.SANDSTONE.defaultBlockState(), Blocks.SAND.defaultBlockState(), Blocks.GRASS_BLOCK.defaultBlockState() }) { + Blocks.SANDSTONE.defaultBlockState(), Blocks.SAND.defaultBlockState(), Blocks.GRASS_BLOCK.defaultBlockState(), + Blocks.SNOW_BLOCK.defaultBlockState() }) { if (source.getBlock() == Blocks.GRASS_BLOCK && !GrassSlabsApi.grassGenerationAllowed()) { continue; } diff --git a/src/integrationTest/resources/fixtures/reported_snow_gap.json b/src/integrationTest/resources/fixtures/reported_snow_gap.json new file mode 100644 index 0000000..b86581e --- /dev/null +++ b/src/integrationTest/resources/fixtures/reported_snow_gap.json @@ -0,0 +1,422 @@ +{ + "origin": [ + 7876, + 63, + -3745 + ], + "blocks": [ + { + "offset": [ + -1, + -1, + -1 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + -1, + 0, + -1 + ], + "state": { + "Name": "skysgrassslabs:grass_slab", + "Properties": { + "waterlogged": "false", + "type": "bottom", + "snowy": "false" + } + } + }, + { + "offset": [ + -1, + 1, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + -1, + 2, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + -1, + -1, + 0 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + -1, + 0, + 0 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "true" + } + } + }, + { + "offset": [ + -1, + 1, + 0 + ], + "state": { + "Name": "minecraft:snow", + "Properties": { + "layers": "1" + } + } + }, + { + "offset": [ + -1, + 2, + 0 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + -1, + -1, + 1 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + -1, + 0, + 1 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "true" + } + } + }, + { + "offset": [ + -1, + 1, + 1 + ], + "state": { + "Name": "minecraft:snow", + "Properties": { + "layers": "1" + } + } + }, + { + "offset": [ + -1, + 2, + 1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 0, + -1, + -1 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "false" + } + } + }, + { + "offset": [ + 0, + 0, + -1 + ], + "state": { + "Name": "skysbuildingpieces:grass_corner", + "Properties": { + "orientation": "bottom_south_west", + "waterlogged": "false", + "snowy": "true" + } + } + }, + { + "offset": [ + 0, + 1, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 0, + 2, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 0, + -1, + 0 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + 0, + 0, + 0 + ], + "state": { + "Name": "skysgrassslabs:grass_slab", + "Properties": { + "waterlogged": "false", + "type": "bottom", + "snowy": "false" + } + } + }, + { + "offset": [ + 0, + 1, + 0 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 0, + 2, + 0 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 0, + -1, + 1 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + 0, + 0, + 1 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "true" + } + } + }, + { + "offset": [ + 0, + 1, + 1 + ], + "state": { + "Name": "minecraft:snow", + "Properties": { + "layers": "1" + } + } + }, + { + "offset": [ + 0, + 2, + 1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 1, + -1, + -1 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "true" + } + } + }, + { + "offset": [ + 1, + 0, + -1 + ], + "state": { + "Name": "minecraft:snow", + "Properties": { + "layers": "1" + } + } + }, + { + "offset": [ + 1, + 1, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 1, + 2, + -1 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 1, + -1, + 0 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + 1, + 0, + 0 + ], + "state": { + "Name": "skysgrassslabs:grass_slab", + "Properties": { + "waterlogged": "false", + "type": "bottom", + "snowy": "true" + } + } + }, + { + "offset": [ + 1, + 1, + 0 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 1, + 2, + 0 + ], + "state": { + "Name": "minecraft:air" + } + }, + { + "offset": [ + 1, + -1, + 1 + ], + "state": { + "Name": "minecraft:dirt" + } + }, + { + "offset": [ + 1, + 0, + 1 + ], + "state": { + "Name": "minecraft:grass_block", + "Properties": { + "snowy": "true" + } + } + }, + { + "offset": [ + 1, + 1, + 1 + ], + "state": { + "Name": "minecraft:snow", + "Properties": { + "layers": "1" + } + } + }, + { + "offset": [ + 1, + 2, + 1 + ], + "state": { + "Name": "minecraft:air" + } + } + ] +} diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/SkysTerrainSmoother.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/SkysTerrainSmoother.java index a571e47..318a3d9 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/SkysTerrainSmoother.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/SkysTerrainSmoother.java @@ -29,7 +29,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.1.2603001"; + public static final String VERSION = "0.1.2.2603001"; public static final Logger logger = LogManager.getLogger(); public static final SmoothingEngine engine = new SmoothingEngine(MaterialCatalogue.instance()); public static boolean enabled = true; @@ -45,7 +45,9 @@ public SkysTerrainSmoother(FMLJavaModLoadingContext context) { FMLLoadCompleteEvent.getBus(modBus).addListener(this::complete); TerrainSmoothingFeature.register(modBus); RegisterEvent.getBus(modBus).addListener(SandContent::register); + RegisterEvent.getBus(modBus).addListener(zone.moddev.mc.skysterrainsmoother.content.PowderSnowContent::register); BuildCreativeModeTabContentsEvent.BUS.addListener(SandContent::buildCreativeTab); + BuildCreativeModeTabContentsEvent.BUS.addListener(zone.moddev.mc.skysterrainsmoother.content.PowderSnowContent::creative); ChunkEvent.Load.BUS.addListener(engine::loaded); ChunkEvent.Unload.BUS.addListener(engine::unload); TickEvent.LevelTickEvent.Post.BUS.addListener(Priority.LOWEST, engine::endWorldTick); 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..0502be6 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/client/PowderSnowFog.java @@ -0,0 +1,51 @@ +package zone.moddev.mc.skysterrainsmoother.client; + +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.listener.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.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. */ +@Mod.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.blockPosition(); + var state = world.getBlockState(position); + return state.getBlock() instanceof PowderSnowPieceBlock + && state.getShape(world, position).bounds().contains( + camera.position().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 boolean distance(ViewportEvent.RenderFog event) { + if (inside(event.getCamera())) { + var entity = event.getCamera().entity(); + boolean spectator = entity != null && entity.isSpectator(); + event.setNearPlaneDistance(spectator ? -8 : 0); + event.setFarPlaneDistance(spectator ? event.getFarPlaneDistance() * .5F : 2); + return true; + } + return false; + } + + 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..6f229fa --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowContent.java @@ -0,0 +1,59 @@ +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.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Half; +import net.minecraftforge.event.BuildCreativeModeTabContentsEvent; +import net.minecraftforge.registries.RegisterEvent; +import zone.moddev.mc.skysterrainsmoother.SkysTerrainSmoother; + +/** Powder snow keeps its own pieces so smoothing does not turn a hazard into solid snow. */ +public final class PowderSnowContent { + private static final Map BLOCKS = + new EnumMap<>(PowderSnowPieceBlock.Kind.class); + + public static void register(RegisterEvent event) { + event.register(Registries.RECIPE_SERIALIZER, registry -> registry.register( + Identifier.fromNamespaceAndPath(SkysTerrainSmoother.ID, "powder_snow_cutting"), + PowderSnowCuttingRecipe.SERIALIZER)); + 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()) { + Item.Properties properties = new Item.Properties().setId(ResourceKey.create(Registries.ITEM, id(entry.getKey()))); + registry.register(id(entry.getKey()), new BlockItem(entry.getValue(), properties)); + } + }); + } + + public static void creative(BuildCreativeModeTabContentsEvent event) { + if (event.getTabKey().equals(CreativeModeTabs.BUILDING_BLOCKS)) { + BLOCKS.values().forEach(event::accept); + } + } + + static Identifier id(PowderSnowPieceBlock.Kind kind) { + return Identifier.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..5fe7b70 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowCuttingRecipe.java @@ -0,0 +1,97 @@ +package zone.moddev.mc.skysterrainsmoother.content; + +import com.mojang.serialization.MapCodec; +import java.util.List; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +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.PlacementInfo; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.properties.Half; + +/** Cut one bucket of powder snow and return its empty bucket. */ +public final class PowderSnowCuttingRecipe extends CustomRecipe { + private static final PowderSnowCuttingRecipe INSTANCE = new PowderSnowCuttingRecipe(); + public static final RecipeSerializer SERIALIZER = new RecipeSerializer<>( + MapCodec.unit(INSTANCE), StreamCodec.unit(INSTANCE)); + + @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) { + return result(); + } + + private static ItemStack result() { + 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 PlacementInfo placementInfo() { + return PlacementInfo.create(Ingredient.of(Items.POWDER_SNOW_BUCKET)); + } + + @Override + public List display() { + return List.of(new ShapelessCraftingRecipeDisplay(List.of(Ingredient.of(Items.POWDER_SNOW_BUCKET).display()), + new SlotDisplay.ItemStackSlotDisplay(net.minecraft.world.item.ItemStackTemplate.fromNonEmptyStack(result())), + Ingredient.of(Blocks.CRAFTING_TABLE).display())); + } + + @Override + public CraftingBookCategory category() { + return CraftingBookCategory.BUILDING; + } + + @Override + public boolean isSpecial() { + return false; + } + + @Override + public String group() { + return "skys/result/skysterrainsmoother/powder_snow_slab"; + } + + @Override + public RecipeSerializer getSerializer() { + return SERIALIZER; + } +} 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..ae93da7 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/PowderSnowPieceBlock.java @@ -0,0 +1,238 @@ +package zone.moddev.mc.skysterrainsmoother.content; + +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.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 EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + 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().setId(net.minecraft.resources.ResourceKey.create( + net.minecraft.core.registries.Registries.BLOCK, PowderSnowContent.id(kind))).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 + 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, + net.minecraft.world.entity.InsideBlockEffectApplier effects, boolean moved) { + 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, effects, moved); + } + } + + @Override + protected VoxelShape getEntityInsideCollisionShape(BlockState state, BlockGetter world, BlockPos position, Entity entity) { + return outlines[index(state)]; + } + + @Override + public ItemStack pickupBlock(net.minecraft.world.entity.LivingEntity 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; + } + Direction face = context.getClickedFace(); + Vec3 normal = new Vec3(face.getStepX(), face.getStepY(), face.getStepZ()); + 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/content/SandContent.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/SandContent.java index 1eb4e63..4ae949f 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/content/SandContent.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/SandContent.java @@ -13,8 +13,10 @@ public final class SandContent { private static final Identifier SAND_SLAB = Identifier.fromNamespaceAndPath(SkysTerrainSmoother.ID, "sand_slab"); private static final Identifier RED_SAND_SLAB = Identifier.fromNamespaceAndPath(SkysTerrainSmoother.ID, "red_sand_slab"); + private static final Identifier GRAVEL_SLAB = Identifier.fromNamespaceAndPath(SkysTerrainSmoother.ID, "gravel_slab"); public static SandSlabBlock slab; public static SandSlabBlock redSlab; + public static SandSlabBlock gravelSlab; private SandContent() { } @@ -25,12 +27,15 @@ public static void register(RegisterEvent event) { SandLegacyRecovery.register(); slab = new SandSlabBlock(false, SAND_SLAB); redSlab = new SandSlabBlock(true, RED_SAND_SLAB); + gravelSlab = new SandSlabBlock(net.minecraft.world.level.block.Blocks.GRAVEL, GRAVEL_SLAB); registry.register(SAND_SLAB, slab); registry.register(RED_SAND_SLAB, redSlab); + registry.register(GRAVEL_SLAB, gravelSlab); }); event.register(ForgeRegistries.Keys.ITEMS, registry -> { registry.register(SAND_SLAB, new SandSlabItem(slab, SAND_SLAB)); registry.register(RED_SAND_SLAB, new SandSlabItem(redSlab, RED_SAND_SLAB)); + registry.register(GRAVEL_SLAB, new SandSlabItem(gravelSlab, GRAVEL_SLAB)); }); } @@ -38,6 +43,7 @@ public static void buildCreativeTab(BuildCreativeModeTabContentsEvent event) { if (CreativeModeTabs.BUILDING_BLOCKS.equals(event.getTabKey())) { event.accept(slab); event.accept(redSlab); + event.accept(gravelSlab); } } diff --git a/src/main/java/zone/moddev/mc/skysterrainsmoother/content/SandSlabBlock.java b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/SandSlabBlock.java index 65bded0..bfb05be 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/content/SandSlabBlock.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/content/SandSlabBlock.java @@ -27,29 +27,33 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerLevel; -/** A horizontal sand half. Joining it creates ordinary sand, so there is no double state. */ +/** A horizontal half of falling terrain. Joining it creates the native full block. */ public final class SandSlabBlock extends FallingBlock implements SimpleWaterloggedBlock { public static final EnumProperty TYPE = EnumProperty.create("type", SlabType.class, SlabType.TOP, SlabType.BOTTOM); public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; private static final VoxelShape LOWER_HALF = Block.box(0, 0, 0, 16, 8, 16); private static final VoxelShape UPPER_HALF = Block.box(0, 8, 0, 16, 16, 16); - private final boolean red; + private final Block fullBlock; public SandSlabBlock(boolean red, Identifier id) { - this(red, Block.Properties.ofFullCopy(red ? Blocks.RED_SAND : Blocks.SAND) + this(red ? Blocks.RED_SAND : Blocks.SAND, id); + } + + public SandSlabBlock(Block fullBlock, Identifier id) { + this(fullBlock, Block.Properties.ofFullCopy(fullBlock) .setId(ResourceKey.create(Registries.BLOCK, id)) .noOcclusion().isRedstoneConductor((state, world, position) -> false)); } - private SandSlabBlock(boolean red, Properties properties) { + private SandSlabBlock(Block fullBlock, Properties properties) { super(properties); - this.red = red; + this.fullBlock = fullBlock; registerDefaultState(stateDefinition.any().setValue(TYPE, SlabType.BOTTOM).setValue(WATERLOGGED, false)); } public BlockState fullState() { - return (red ? Blocks.RED_SAND : Blocks.SAND).defaultBlockState(); + return fullBlock.defaultBlockState(); } @Override 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 647f156..78ecb2b 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(Identifier.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( @@ -59,6 +76,10 @@ public static synchronized void ensureRegistered() { Collections.singleton(sand.defaultBlockState()), SandContent.legacyState(variant * 2 + 1), null, null, false, null)); } + MaterialCatalogue.instance().register(new TerrainMaterial(Identifier.withDefaultNamespace("gravel"), + Collections.singleton(Blocks.GRAVEL.defaultBlockState()), + SandContent.gravelSlab.defaultBlockState(), + null, null, false, null)); registerMineralogy(); registered = true; } 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 6f6ca05..11bee9a 100644 --- a/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/MaterialCatalogue.java +++ b/src/main/java/zone/moddev/mc/skysterrainsmoother/internal/MaterialCatalogue.java @@ -214,7 +214,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"); } @@ -247,8 +247,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 10f8bba..574c374 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; @@ -217,7 +218,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(BlockStateProperties.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.markUnsaved(); + } + synchronized (this) { placed += additional; } @@ -370,7 +382,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 @@ -401,6 +413,9 @@ private int smooth(LevelAccessor world, ChunkAccess owner, int level, boolean af cardinal, diagonal, level, cave); if (result != null) { + if (material.grass && result.hasProperty(BlockStateProperties.SNOWY)) { + result = withSnowAppearance(world, owner, target, result); + } buffer.add(localBlockX | localBlockZ << 4 | (floorY + 1) << 8, result, material, cave, above); } } @@ -465,6 +480,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(BlockStateProperties.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(BlockStateProperties.SNOWY, true); + } + } + boolean exposedColdSurface = world.getBiome(position).value().coldEnoughToSnow(position, world.getSeaLevel()) + && 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(BlockStateProperties.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. @@ -523,7 +563,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); @@ -558,7 +598,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; } @@ -613,10 +653,7 @@ private boolean higher(LevelAccessor world, ChunkAccess owner, int localX, int l } private boolean hasTerrainFloor(LevelAccessor world, ChunkAccess owner, BlockPos floor) { - int chunkX = floor.getX() >> 4; - int chunkZ = floor.getZ() >> 4; - ChunkAccess neighbour = chunkX == owner.getPos().x() && chunkZ == owner.getPos().z() - ? owner : loaded(world, chunkX, chunkZ); + ChunkAccess neighbour = ownerOrLoaded(world, owner, floor); if (neighbour == null) { return false; } @@ -643,6 +680,12 @@ private static boolean isOverworld(LevelAccessor world) { return world instanceof WorldGenLevel && ((WorldGenLevel) world).getLevel().dimension() == Level.OVERWORLD; } + private static ChunkAccess ownerOrLoaded(LevelAccessor world, ChunkAccess owner, BlockPos position) { + int chunkX = position.getX() >> 4; + int chunkZ = position.getZ() >> 4; + return chunkX == owner.getPos().x() && chunkZ == owner.getPos().z() ? owner : loaded(world, chunkX, chunkZ); + } + /** A missing neighbour is unavailable, never a reason to request or generate another chunk. */ private static ChunkAccess loaded(LevelAccessor world, int chunkX, int chunkZ) { if (world instanceof WorldGenRegion) { 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..29b3e7f 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,97 @@ "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" + ] + } + ] + }, + { + "recipe": "skysterrainsmoother:gravel_slab", + "owner": "skysterrainsmoother", + "ingredients": [ + { + "items": [ + "minecraft:gravel" + ] + } + ] + }, + { + "recipe": "skysterrainsmoother:gravel_from_slabs", + "owner": "skysterrainsmoother", + "ingredients": [ + { + "items": [ + "skysterrainsmoother:gravel_slab" + ] } ] } diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 6caa80e..30580f5 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -25,12 +25,12 @@ side="BOTH" [[dependencies.${mod_id}]] modId="skysgrassslabs" mandatory=true -versionRange="[1.2.0.2603001,1.3)" +versionRange="[1.2.1.2603001,1.3)" ordering="AFTER" side="BOTH" [[dependencies.${mod_id}]] modId="skysbuildingpieces" mandatory=true -versionRange="[0.3.9.2603001,0.4)" +versionRange="[0.3.10.2603001,0.4)" ordering="AFTER" side="BOTH" diff --git a/src/main/resources/assets/skysterrainsmoother/blockstates/gravel_slab.json b/src/main/resources/assets/skysterrainsmoother/blockstates/gravel_slab.json new file mode 100644 index 0000000..cca3c0d --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/blockstates/gravel_slab.json @@ -0,0 +1,10 @@ +{ + "variants": { + "type=top": { + "model": "skysterrainsmoother:block/gravel_slab_top" + }, + "type=bottom": { + "model": "skysterrainsmoother:block/gravel_slab" + } + } +} 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/items/gravel_slab.json b/src/main/resources/assets/skysterrainsmoother/items/gravel_slab.json new file mode 100644 index 0000000..3b28a0b --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/items/gravel_slab.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "skysterrainsmoother:item/gravel_slab" + } +} diff --git a/src/main/resources/assets/skysterrainsmoother/items/powder_snow_corner.json b/src/main/resources/assets/skysterrainsmoother/items/powder_snow_corner.json new file mode 100644 index 0000000..9981c25 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/items/powder_snow_corner.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "skysterrainsmoother:item/powder_snow_corner" + } +} diff --git a/src/main/resources/assets/skysterrainsmoother/items/powder_snow_slab.json b/src/main/resources/assets/skysterrainsmoother/items/powder_snow_slab.json new file mode 100644 index 0000000..3078001 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/items/powder_snow_slab.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "skysterrainsmoother:item/powder_snow_slab" + } +} diff --git a/src/main/resources/assets/skysterrainsmoother/items/powder_snow_step.json b/src/main/resources/assets/skysterrainsmoother/items/powder_snow_step.json new file mode 100644 index 0000000..aaaeda6 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/items/powder_snow_step.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "skysterrainsmoother:item/powder_snow_step" + } +} diff --git a/src/main/resources/assets/skysterrainsmoother/lang/de_at.json b/src/main/resources/assets/skysterrainsmoother/lang/de_at.json index 096ec20..39ef436 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/de_at.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/de_at.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Kiesstufn" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/de_au.json b/src/main/resources/assets/skysterrainsmoother/lang/de_au.json index 4f63544..bdcd90a 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/de_au.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/de_au.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Kiesstufe" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/de_de.json b/src/main/resources/assets/skysterrainsmoother/lang/de_de.json index 4f63544..bdcd90a 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/de_de.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/de_de.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Kiesstufe" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/en_ca.json b/src/main/resources/assets/skysterrainsmoother/lang/en_ca.json index 4fd9d74..90b5e8a 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/en_ca.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/en_ca.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Gravel Slab" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/en_en.json b/src/main/resources/assets/skysterrainsmoother/lang/en_en.json index 4fd9d74..90b5e8a 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/en_en.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/en_en.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Gravel Slab" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/en_gb.json b/src/main/resources/assets/skysterrainsmoother/lang/en_gb.json index 4fd9d74..90b5e8a 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/en_gb.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/en_gb.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Gravel Slab" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/en_pt.json b/src/main/resources/assets/skysterrainsmoother/lang/en_pt.json index 4fd9d74..90b5e8a 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/en_pt.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/en_pt.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Gravel Slab" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/en_us.json b/src/main/resources/assets/skysterrainsmoother/lang/en_us.json index 4fd9d74..90b5e8a 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/en_us.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/en_us.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Gravel Slab" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/es_es.json b/src/main/resources/assets/skysterrainsmoother/lang/es_es.json index 0674145..7839f45 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/es_es.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/es_es.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Losa de grava" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/es_mx.json b/src/main/resources/assets/skysterrainsmoother/lang/es_mx.json index 0674145..7839f45 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/es_mx.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/es_mx.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Losa de grava" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/fr_ca.json b/src/main/resources/assets/skysterrainsmoother/lang/fr_ca.json index 3eda14b..69a248a 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/fr_ca.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/fr_ca.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Dalle de gravier" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/fr_fr.json b/src/main/resources/assets/skysterrainsmoother/lang/fr_fr.json index 3eda14b..69a248a 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/fr_fr.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/fr_fr.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Dalle de gravier" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/ja_jp.json b/src/main/resources/assets/skysterrainsmoother/lang/ja_jp.json index bff3a64..61db960 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/ja_jp.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/ja_jp.json @@ -6,5 +6,9 @@ "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": "粉雪の角", + "block.skysterrainsmoother.gravel_slab": "砂利のハーフブロック" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/ko_kr.json b/src/main/resources/assets/skysterrainsmoother/lang/ko_kr.json index e0574fe..89a4307 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/ko_kr.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/ko_kr.json @@ -6,5 +6,9 @@ "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": "가루눈 모서리", + "block.skysterrainsmoother.gravel_slab": "자갈 반 블록" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/pt_br.json b/src/main/resources/assets/skysterrainsmoother/lang/pt_br.json index b9dc4ed..e25d4c4 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/pt_br.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/pt_br.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Laje de Cascalho" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/pt_pt.json b/src/main/resources/assets/skysterrainsmoother/lang/pt_pt.json index 022ca1d..0b92845 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/pt_pt.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/pt_pt.json @@ -6,5 +6,9 @@ "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", + "block.skysterrainsmoother.gravel_slab": "Degrau de Cascalho" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/ru_ru.json b/src/main/resources/assets/skysterrainsmoother/lang/ru_ru.json index 04b2eab..763c68c 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/ru_ru.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/ru_ru.json @@ -6,5 +6,9 @@ "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": "Уголок из рыхлого снега", + "block.skysterrainsmoother.gravel_slab": "Гравийная плита" } diff --git a/src/main/resources/assets/skysterrainsmoother/lang/zh_cn.json b/src/main/resources/assets/skysterrainsmoother/lang/zh_cn.json index f913162..520179c 100644 --- a/src/main/resources/assets/skysterrainsmoother/lang/zh_cn.json +++ b/src/main/resources/assets/skysterrainsmoother/lang/zh_cn.json @@ -6,5 +6,9 @@ "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": "细雪角块", + "block.skysterrainsmoother.gravel_slab": "沙砾台阶" } diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/gravel_slab.json b/src/main/resources/assets/skysterrainsmoother/models/block/gravel_slab.json new file mode 100644 index 0000000..a5f2ce7 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/gravel_slab.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/slab", + "textures": { + "bottom": "minecraft:block/gravel", + "top": "minecraft:block/gravel", + "side": "minecraft:block/gravel" + } +} diff --git a/src/main/resources/assets/skysterrainsmoother/models/block/gravel_slab_top.json b/src/main/resources/assets/skysterrainsmoother/models/block/gravel_slab_top.json new file mode 100644 index 0000000..c41e3fc --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/block/gravel_slab_top.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/slab_top", + "textures": { + "bottom": "minecraft:block/gravel", + "top": "minecraft:block/gravel", + "side": "minecraft:block/gravel" + } +} 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/gravel_slab.json b/src/main/resources/assets/skysterrainsmoother/models/item/gravel_slab.json new file mode 100644 index 0000000..e798d61 --- /dev/null +++ b/src/main/resources/assets/skysterrainsmoother/models/item/gravel_slab.json @@ -0,0 +1,3 @@ +{ + "parent": "skysterrainsmoother:block/gravel_slab" +} 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..74ae956 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,10 @@ "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", + "skysterrainsmoother:gravel_slab" ] } diff --git a/src/main/resources/data/skysterrainsmoother/loot_table/blocks/gravel_slab.json b/src/main/resources/data/skysterrainsmoother/loot_table/blocks/gravel_slab.json new file mode 100644 index 0000000..b86c955 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/loot_table/blocks/gravel_slab.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "skysterrainsmoother:gravel_slab" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} 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/gravel_from_slabs.json b/src/main/resources/data/skysterrainsmoother/recipe/gravel_from_slabs.json new file mode 100644 index 0000000..03e5378 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/gravel_from_slabs.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "group": "skys/result/minecraft/gravel", + "pattern": [ + "SS" + ], + "key": { + "S": "skysterrainsmoother:gravel_slab" + }, + "result": { + "id": "minecraft:gravel" + } +} diff --git a/src/main/resources/data/skysterrainsmoother/recipe/gravel_slab.json b/src/main/resources/data/skysterrainsmoother/recipe/gravel_slab.json new file mode 100644 index 0000000..c1d44f3 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/gravel_slab.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "group": "skys/result/skysterrainsmoother/gravel_slab", + "pattern": [ + "SSS" + ], + "key": { + "S": "minecraft:gravel" + }, + "result": { + "id": "skysterrainsmoother:gravel_slab", + "count": 6 + } +} 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..54a1a1b --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_corner.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "XX" + ], + "key": { + "X": "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..0781343 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_from_slabs.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shapeless", + "ingredients": [ + "skysterrainsmoother:powder_snow_slab", + "skysterrainsmoother:powder_snow_slab", + "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..52c6f79 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_slab.json @@ -0,0 +1,3 @@ +{ + "type": "skysterrainsmoother:powder_snow_cutting" +} 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..3d2c9e1 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_slab_from_steps.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "X", + "X" + ], + "key": { + "X": "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..8d9ffe4 --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_step.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "X ", + " X" + ], + "key": { + "X": "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..0b6530f --- /dev/null +++ b/src/main/resources/data/skysterrainsmoother/recipe/powder_snow_step_from_corners.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "X", + "X" + ], + "key": { + "X": "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 0249b12..d7b1951 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 02a64f9..01cc2c1 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 b8d96f3..7535d86 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 5f14f93..b0944d1 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 99216ce..72f859e 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.1.2603001", metadata.getProperty("mod_version")); + assertEquals("0.1.2.2603001", metadata.getProperty("mod_version")); assertEquals(metadata.getProperty("mod_version"), SkysTerrainSmoother.VERSION); assertEquals("26.3", metadata.getProperty("minecraft_version")); assertEquals("66.0.2", metadata.getProperty("forge_version")); @@ -36,7 +36,7 @@ void targetMetadataAndToolchainMatchTheNativeRuntime() throws Exception { @Test void bothSandColoursHaveNativeModelsAndTheirOwnLoot() throws Exception { Path root = Paths.get("src/main/resources"); - for (String sand : new String[] { "sand", "red_sand" }) { + for (String sand : new String[] { "sand", "red_sand", "gravel" }) { Path assets = root.resolve("assets/skysterrainsmoother"); JsonObject variants = json(assets.resolve("blockstates/" + sand + "_slab.json")).getAsJsonObject("variants"); assertEquals(2, variants.size()); @@ -64,9 +64,10 @@ 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" }) { + for (String sand : new String[] { "sand", "red_sand", "gravel" }) { JsonObject cut = json(recipes.resolve(sand + "_slab.json")); assertEquals("SSS", cut.getAsJsonArray("pattern").get(0).getAsString()); assertEquals("minecraft:" + sand, cut.getAsJsonObject("key").get("S").getAsString()); @@ -93,8 +94,11 @@ void eighteenUtf8LanguagesRetainNamesAndConfigHelp() throws Exception { assertFalse(source.contains("\r")); assertTrue(source.endsWith("\n")); JsonObject values = json(path); - assertEquals(8, values.size()); - for (String sand : new String[] { "sand", "red_sand" }) { + assertEquals(12, values.size()); + for (String shape : new String[] {"slab", "step", "corner"}) { + assertFalse(values.get("block.skysterrainsmoother.powder_snow_" + shape).getAsString().isBlank()); + } + for (String sand : new String[] { "sand", "red_sand", "gravel" }) { assertFalse(values.get("block.skysterrainsmoother." + sand + "_slab").getAsString().trim().isEmpty()); } } diff --git a/src/test/java/zone/moddev/mc/skysterrainsmoother/SandStateTest.java b/src/test/java/zone/moddev/mc/skysterrainsmoother/SandStateTest.java index a26ffeb..e42c105 100644 --- a/src/test/java/zone/moddev/mc/skysterrainsmoother/SandStateTest.java +++ b/src/test/java/zone/moddev/mc/skysterrainsmoother/SandStateTest.java @@ -16,6 +16,20 @@ import static org.junit.jupiter.api.Assertions.*; class SandStateTest { + @Test + void gravelKeepsItsNativeMaterialAndBothHorizontalHalves() { + SandSlabBlock gravel = new SandSlabBlock(net.minecraft.world.level.block.Blocks.GRAVEL, + Identifier.parse("skysterrainsmoother:test_gravel_slab")); + assertSame(net.minecraft.world.level.block.Blocks.GRAVEL, gravel.fullState().getBlock()); + assertEquals(4, gravel.getStateDefinition().getPossibleStates().size()); + assertFalse(SandSlabBlock.TYPE.getPossibleValues().contains(SlabType.DOUBLE)); + for (SlabType half : new SlabType[] {SlabType.BOTTOM, SlabType.TOP}) { + BlockState state = gravel.defaultBlockState().setValue(SandSlabBlock.TYPE, half); + assertEquals(.5, state.getShape(null, BlockPos.ZERO).bounds().getYsize()); + assertFalse(state.hasBlockEntity()); + } + } + @BeforeAll static void boot() { MinecraftTestBootstrap.registerVanilla(); diff --git a/tools/generate_powder_snow.py b/tools/generate_powder_snow.py new file mode 100644 index 0000000..16f68e3 --- /dev/null +++ b/tools/generate_powder_snow.py @@ -0,0 +1,104 @@ +"""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['assets/' + MOD + '/items/' + name + '.json'] = { + 'model': {'type': 'minecraft:model', 'model': MOD + ':item/' + name}} + 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'} + 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]) + # Ingredients use identifiers or tags; object-form ingredients are no longer accepted. + for relative, body in files.items(): + if '/recipe/' in relative: + if 'key' in body: + body['key'] = {key: value['item'] for key, value in body['key'].items()} + if 'ingredients' in body: + body['ingredients'] = [value['item'] for value in body['ingredients']] + 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..5fd8bd1 --- /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(42, 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()