diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f04f21..54a224c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,9 +111,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/SkysGrassSlabs-1.1.2.2603001.jar - build/libs/SkysGrassSlabs-1.1.2.2603001-sources.jar - build/libs/SkysGrassSlabs-1.1.2.2603001-javadoc.jar + build/libs/SkysGrassSlabs-1.2.0.2603001.jar + build/libs/SkysGrassSlabs-1.2.0.2603001-sources.jar + build/libs/SkysGrassSlabs-1.2.0.2603001-javadoc.jar build/release/SHA256SUMS CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index fd38986..a722c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.2.0.2603001 - Minecraft 26.3 + +- Craft two matching dirt or grass slabs back into a full block. +- Change Grass Slabs settings from Forge’s Mods menu. +- Let Terrain Smoother handle generation when installed. Grass Slabs still controls whether it includes grass. +- Protect existing chunks and structures, and keep small plants where a new grass slab fits below them. + ## 1.1.2.2603001 - Minecraft 26.3 - Ported the complete feature set to Minecraft 26.3 and Forge 66. diff --git a/README.md b/README.md index 1bbbc82..5952457 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ thin layer of turf. Grass spreads naturally between full blocks and slabs, while new Overworld terrain uses grass slabs to soften suitable one block steps. -This branch is for Minecraft 26.3. Version `1.1.2.2603001` requires Forge -66.0.0 or a compatible Forge 66 build and Java 25. +This branch is for Minecraft 26.3. Version `1.2.0.2603001` requires Forge +66.0.2 or a compatible Forge 66 build and Java 25. ## Installation @@ -17,7 +17,8 @@ or updating world changing mods. ## Highlights - Place dirt, grass and dirt path slabs in either half of a block. -- Combine two matching slabs into the corresponding vanilla block. +- Combine two matching slabs into the corresponding vanilla block, + or craft them back into a block. - Let grass spread between full blocks, slabs and turf. - Use a shovel on dirt or grass slabs to create lowered dirt path slabs. - Cut turf from grass without consuming the shovel. @@ -29,12 +30,15 @@ or updating world changing mods. - Recover selected grass slabs and grass carpet when replacing a compatible older mod. +Sky's Terrain Smoother handles generation when installed. Grass Slabs' own +generation setting still decides whether grass is included. + ## Guides - [Gameplay and recipes](docs/GAMEPLAY.md) - [Configuration](docs/CONFIGURATION.md) - [Upgrading an older world](docs/WORLD-UPGRADES.md) -- [Release notes](docs/RELEASE-1.1.2.2603001.md) +- [Release notes](docs/RELEASE-1.2.0.2603001.md) ## License diff --git a/build.gradle b/build.gradle index 3ce60a4..a5dee9c 100644 --- a/build.gradle +++ b/build.gradle @@ -37,15 +37,15 @@ def mavenizerLicense = mavenizerToolsDirectory.file('LICENSE-MAVENIZER.txt') def mavenizerReadme = mavenizerToolsDirectory.file('README.md') def mavenizerFixtureChecksums = [ (mavenizerCompatibilityJar.asFile): - '4150926F33A696B2088AB2675C1C3B45745CE4F9F2DEB31CCD88C9ADC834062F', + '9DA1F44821D89B16B3E181CA89057F44CD3FC3F3383EA5A16442872E67E30F35', (mavenizerRuleManifest.asFile): - '829400D74ECB1BF0CC6107D9980B8328365E3F56D9A5342002D8DF340571BDB7', + 'C765A5F65FB9CD176F3CE08CB0CFCA814D06E649940E2BF169CD8943AFC30B26', (mavenizerSourcePatch.asFile): - '9F0C34529D41C9CD5804C32608FE548D09A0AC3A95D337EE2EF4377680C1B2DD', + 'BBD82C6BD96206DEA2FE2673F4D7E76FBA92884032455BBFD5CCD59045C5E271', (mavenizerLicense.asFile): '20C17D8B8C48A600800DFD14F95D5CB9FF47066A9641DDEAB48DC54AEC96E331', (mavenizerReadme.asFile): - 'A35B80BA8D7557A31E0378787C06FE86C8C627061BF313A6886205F62A46C09F' + 'A611E2BF14E29761B99BF7D3F0FD44AF151DD09A69968C0CFAEC384BD0E7A666' ] // ForgeGradle invokes Mavenizer while configuring the project. Verify the @@ -550,7 +550,7 @@ def verifyMavenizerCompatibilityFixture = tasks.register('verifyMavenizerCompati } def parsed = new JsonSlurper().parse(mavenizerRuleManifest.asFile) - String target = 'net.minecraftforge:forge:26.3-66.0.0' + String target = 'net.minecraftforge:forge:26.3-66.0.2' if (parsed.schema != 1 || parsed.targets.keySet() != [target] as Set) { throw new GradleException('Mavenizer target-rule manifest has unexpected targets or schema') } @@ -605,7 +605,7 @@ def verifyMavenizerCompatibilityFixture = tasks.register('verifyMavenizerCompati String licenseText = mavenizerLicense.asFile.getText('UTF-8') if (!readme.contains('6968241ce7a0a902cdc1c534b976e8373a423091') || !readme.contains('Temurin `25.0.3+9`') - || !readme.contains('26.3-66.0.0')) { + || !readme.contains('26.3-66.0.2')) { throw new GradleException('Mavenizer provenance, target, or Java 25 build instructions are incomplete') } if (!patchText.contains('SourceCompatibilityPatcher.java') @@ -686,7 +686,7 @@ def releaseFiles = provider { tasks.register('verifyReleaseMetadata') { group = 'verification' doLast { - if (project.minecraft_version != '26.3' || project.forge_version != '66.0.0' || + if (project.minecraft_version != '26.3' || project.forge_version != '66.0.2' || project.mapping_channel != 'official' || project.mapping_version != '26.3' || project.loader_name != 'forge' || project.loader_code != '1' || project.java_version != '25' || project.java_toolchain_version != '25.0.3+9' || @@ -770,7 +770,7 @@ tasks.register('verifyReleaseArtifacts') { String metadata = zip.getInputStream(zip.getEntry('META-INF/mods.toml')) .getText(StandardCharsets.UTF_8.name()) if (!metadata.contains('versionRange="[26.3,26.4,)"') || - !metadata.contains('versionRange="[66,)"')) { + !metadata.contains('versionRange="[66.0.2,67)"')) { throw new GradleException('Packaged Forge metadata targets the wrong version') } @@ -802,6 +802,7 @@ tasks.register('writeReleaseChecksums') { group = 'verification' dependsOn tasks.named('verifyReleaseArtifacts') def output = layout.buildDirectory.file('release/SHA256SUMS') + inputs.files(releaseFiles.map { names -> names.collect { layout.buildDirectory.file("libs/${it}") } }) outputs.file(output) doLast { File libs = layout.buildDirectory.dir('libs').get().asFile @@ -1174,6 +1175,7 @@ tasks.named('eclipse').configure { } tasks.withType(AbstractArchiveTask).configureEach { + includeEmptyDirs = false preserveFileTimestamps = false reproducibleFileOrder = true } diff --git a/ci-fixtures/tools/README.md b/ci-fixtures/tools/README.md index a57c127..6607f17 100644 --- a/ci-fixtures/tools/README.md +++ b/ci-fixtures/tools/README.md @@ -2,7 +2,7 @@ This directory contains a build-only derivative of MinecraftForge's MinecraftMavenizer `0.5.21`. It is used only while ForgeGradle prepares the -exact Forge `26.3-66.0.0` development dependency and is excluded from every +exact Forge `26.3-66.0.2` development dependency and is excluded from every OreSpawn publication artifact. ## Provenance and licence @@ -21,13 +21,13 @@ declaration and one accessor in that record. Missing, partial, duplicate or ambiguous states fail preparation. Targets without an explicit rule set are left unchanged. -Forge `26.3-66.0.0` compiles without source compatibility edits. Its explicit +Forge `26.3-66.0.2` compiles without source compatibility edits. Its explicit zero-rule entry proves that result is intentional and target-qualified rather than an accidental fall-through. A marker beside Mavenizer's output records the target and manifest SHA-256. Reprocessing the same source is idempotent. The derivative also propagates Gradle offline mode when the build sets -`ORESPAWN_MAVENIZER_OFFLINE=true`. Mavenizer, Sky's Grass Slabs and Minecraft 26.3 all +`ORESPAWN_MAVENIZER_OFFLINE=true`. Mavenizer, IronAgeFurniture and Minecraft 26.3 all run and compile with Java 25. ## Rebuild @@ -39,7 +39,7 @@ run and compile with Java 25. 4. Run `./gradlew clean build --no-daemon` (or `gradlew.bat` on Windows). 5. Copy `build/libs/minecraft-mavenizer-0.5.21.jar` to `minecraft-mavenizer-0.5.21-orespawn-compat.jar`. -6. Run OreSpawn's `verifyMavenizerCompatibilityFixture` task. The build script +6. Run IronAgeFurniture's `verifyMavenizerCompatibilityFixture` task. The build script contains the authoritative checksums and also verifies the embedded manifest, Java class version and licence/provenance files. diff --git a/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar b/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar index 86aeb5e..d74dabc 100644 Binary files a/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar and b/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar differ diff --git a/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.patch b/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.patch index 5a947f4..7d2ad63 100644 --- a/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.patch +++ b/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.patch @@ -371,7 +371,7 @@ index 0000000..3451901 +{ + "schema": 1, + "targets": { -+ "net.minecraftforge:forge:26.3-66.0.0": { ++ "net.minecraftforge:forge:26.3-66.0.2": { + "expectedApplications": 0, + "rules": [] + } @@ -379,4 +379,3 @@ index 0000000..3451901 +} -- 2.55.0.windows.3 - diff --git a/ci-fixtures/tools/minecraft-source-compatibility.json b/ci-fixtures/tools/minecraft-source-compatibility.json index 3451901..f5105e6 100644 --- a/ci-fixtures/tools/minecraft-source-compatibility.json +++ b/ci-fixtures/tools/minecraft-source-compatibility.json @@ -1,7 +1,7 @@ { "schema": 1, "targets": { - "net.minecraftforge:forge:26.3-66.0.0": { + "net.minecraftforge:forge:26.3-66.0.2": { "expectedApplications": 0, "rules": [] } diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 9e186b1..270cf7b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -13,9 +13,14 @@ kept. An existing TOML file always takes priority. `worldgen.generateGrassSlabs=true` +In the Mods list, select Sky’s Grass Slabs and open Config to edit these +choices. They take effect the next time Minecraft starts. + When enabled, suitable one block slopes in newly generated Overworld chunks may receive grass slabs. Set this to `false` to leave new terrain unchanged. The setting never adds or removes slabs in chunks that already exist. +If Sky’s Terrain Smoother is installed, it handles generation for both mods. +This setting still controls whether that smoother places grass pieces. ## Legacy slab replacement diff --git a/docs/GAMEPLAY.md b/docs/GAMEPLAY.md index 1b4b240..fed1216 100644 --- a/docs/GAMEPLAY.md +++ b/docs/GAMEPLAY.md @@ -44,6 +44,7 @@ enabled is removed without dropping an item. - Three dirt blocks in a horizontal row make six dirt slabs. - Three grass blocks in a horizontal row make six grass slabs. +- Two matching dirt or grass slabs side by side make their original block. - A dirt block and a recognised seed make a grass block. - A dirt slab and a recognised seed make a grass slab. - A grass block or grass slab together with a compatible shovel makes one diff --git a/docs/RELEASE-1.1.0.2601021.md b/docs/RELEASE-1.1.1.2601021.md similarity index 91% rename from docs/RELEASE-1.1.0.2601021.md rename to docs/RELEASE-1.1.1.2601021.md index 8a3d255..029a941 100644 --- a/docs/RELEASE-1.1.0.2601021.md +++ b/docs/RELEASE-1.1.1.2601021.md @@ -1,4 +1,4 @@ -# Sky's Grass Slabs 1.1.0 for Minecraft 26.1.2 +# Sky's Grass Slabs 1.1.1 for Minecraft 26.1.2 Sky's Grass Slabs adds dirt, grass and lowered dirt path slabs, together with a thin layer of turf. Grass moves naturally between full blocks, slabs and @@ -15,6 +15,7 @@ grass slab for a smoother slope. - Grass spreading between full blocks, slabs and turf. - Optional recovery for supported blocks and items from older worlds. - Names in 18 supported language variants. +- Corrected translated names for all four items. World smoothing changes only newly generated chunks and can be disabled in the common configuration. Make a complete backup before upgrading an existing diff --git a/docs/RELEASE-1.1.2.2601021.md b/docs/RELEASE-1.1.2.2601021.md new file mode 100644 index 0000000..ea7f008 --- /dev/null +++ b/docs/RELEASE-1.1.2.2601021.md @@ -0,0 +1,11 @@ +# Sky's Grass Slabs 1.1.2 for Minecraft 26.1.2 + +This update corrects the snowy appearance of grass slabs. When a grass slab +shows snowy side edges, its exposed top now uses the matching snow texture +instead of remaining green. + +All blocks, recipes, configuration options and world data remain compatible +with the previous Forge 26.1.2 release. + +Requires Minecraft 26.1.2, Forge 64.0.9 or a compatible Forge 64 build, and +Java 25. diff --git a/docs/RELEASE-1.2.0.2601021.md b/docs/RELEASE-1.2.0.2601021.md new file mode 100644 index 0000000..1807839 --- /dev/null +++ b/docs/RELEASE-1.2.0.2601021.md @@ -0,0 +1,10 @@ +# Sky's Grass Slabs 1.2.0.2601021 + +This version is for Minecraft 26.1.2 with Forge 64.0.9 and Java 25. + +Open Config from the Mods list to change the settings, then restart Minecraft. +Two matching grass or dirt slabs can be crafted back into a full block. + +Grass can spread between these slabs and grass pieces from Sky's Building Pieces. +If Sky's Terrain Smoother is installed, it handles new terrain for both mods. +The grass setting still controls whether it places grass pieces. Existing chunks stay as they are. diff --git a/docs/RELEASE-1.2.0.2603001.md b/docs/RELEASE-1.2.0.2603001.md new file mode 100644 index 0000000..8a62603 --- /dev/null +++ b/docs/RELEASE-1.2.0.2603001.md @@ -0,0 +1,12 @@ +# Sky's Grass Slabs 1.2.0.2603001 + +This version is for Minecraft 26.3 with Forge 66.0.2 and Java 25. + +Open Config from the Mods list to change the settings, then restart Minecraft. +Two matching grass or dirt slabs can be crafted back into a full block. + +Grass can spread between these slabs and grass pieces from Sky's Building Pieces. +If Sky's Terrain Smoother is installed, it handles new terrain for both mods. +The grass setting still controls whether it places grass pieces. Existing chunks stay as they are. + +Grass slabs already saved in a world stay in place when trees generate across a chunk border. diff --git a/docs/WORLD-UPGRADES.md b/docs/WORLD-UPGRADES.md index 1d48e18..11e885e 100644 --- a/docs/WORLD-UPGRADES.md +++ b/docs/WORLD-UPGRADES.md @@ -6,7 +6,7 @@ world has been checked in game. ## Sky's Grass Slabs content -Install `SkysGrassSlabs-1.1.2.2603001.jar` before opening a world that used an +Install `SkysGrassSlabs-1.2.0.2603001.jar` before opening a world that used an earlier release. The permanent dirt slab, grass slab, path slab and turf IDs are unchanged. diff --git a/gradle.properties b/gradle.properties index be97f5a..9395c90 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,9 +26,9 @@ minecraft_version=26.3 # as they do not follow standard versioning conventions. minecraft_version_range=[26.3,26.4,) # The Forge version must agree with the Minecraft version to get a valid artifact -forge_version=66.0.0 +forge_version=66.0.2 # The Forge version range can use any version of Forge as bounds or match the loader version range -forge_version_range=[66,) +forge_version_range=[66.0.2,67) # The loader version range can only use the major version of Forge/FML as bounds loader_version_range=[66,) # The mapping channel to use for mappings. @@ -61,7 +61,7 @@ mod_name=Sky's Grass Slabs # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=LGPL-2.1-only # The mod version. See https://semver.org/ -mod_version=1.1.2.2603001 +mod_version=1.2.0.2603001 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/gameTest/java/zone/moddev/mc/skysgrassslabs/gametest/SlabGameTests.java b/src/gameTest/java/zone/moddev/mc/skysgrassslabs/gametest/SlabGameTests.java index c182f5f..fd69df1 100644 --- a/src/gameTest/java/zone/moddev/mc/skysgrassslabs/gametest/SlabGameTests.java +++ b/src/gameTest/java/zone/moddev/mc/skysgrassslabs/gametest/SlabGameTests.java @@ -28,6 +28,7 @@ import net.minecraft.world.item.enchantment.Enchantments; import net.minecraft.world.item.crafting.CraftingInput; import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.level.LightLayer; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.BonemealSource; @@ -47,6 +48,7 @@ import net.minecraftforge.event.entity.EntityJoinLevelEvent; import net.minecraftforge.event.entity.player.PlayerInteractEvent; import zone.moddev.mc.skysgrassslabs.SkysGrassSlabs; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; import zone.moddev.mc.skysgrassslabs.block.DirtSlabBlock; import zone.moddev.mc.skysgrassslabs.block.GrassSlabBlock; import zone.moddev.mc.skysgrassslabs.block.GrassSpread; @@ -187,19 +189,27 @@ public static void pathProfilesAndDecayMatchContract(GameTestHelper helper) { @GameTest(structure = EMPTY) public static void grassLifecycleInteroperatesWithVanilla(GameTestHelper helper) { - setClockTime(helper, 6000); BlockPos dirtPos = helper.absolutePos(new BlockPos(1, 2, 1)); BlockPos sourcePos = dirtPos.east(); - helper.getLevel().setBlock(dirtPos.west().above(), Blocks.GLOWSTONE.defaultBlockState(), 3); helper.getLevel().setBlock(dirtPos, ModBlocks.DIRT_SLAB.get().defaultBlockState(), 3); helper.getLevel().setBlock(sourcePos, Blocks.GRASS_BLOCK.defaultBlockState(), 3); BlockPos grassPos = helper.absolutePos(new BlockPos(1, 2, 3)); BlockPos vanillaDirt = grassPos.east(); - helper.getLevel().setBlock(grassPos.west().above(), Blocks.GLOWSTONE.defaultBlockState(), 3); helper.getLevel().setBlock(grassPos, ModBlocks.GRASS_SLAB.get().defaultBlockState(), 3); helper.getLevel().setBlock(vanillaDirt, Blocks.DIRT.defaultBlockState(), 3); - helper.runAfterDelay(20, () -> { + // Other tests share the world clock. Light the four exposed faces independently of daylight. + List soilPositions = List.of(dirtPos, sourcePos, grassPos, vanillaDirt); + for (BlockPos pos : soilPositions) { + helper.getLevel().setBlock(pos.above(2), Blocks.LIGHT.defaultBlockState(), Block.UPDATE_ALL); + } + + helper.startSequence().thenWaitUntil(() -> { + for (BlockPos pos : soilPositions) { + int light = helper.getLevel().getBrightness(LightLayer.BLOCK, pos.above()); + require(helper, light >= 9, "Grass test block light has not reached " + pos + ": " + light); + } + }).thenExecute(() -> { ((DirtSlabBlock) ModBlocks.DIRT_SLAB.get()).randomTick( helper.getLevel().getBlockState(dirtPos), helper.getLevel(), dirtPos, new FixedRandom(2, 3, 1)); @@ -225,8 +235,7 @@ public static void grassLifecycleInteroperatesWithVanilla(GameTestHelper helper) require(helper, helper.getLevel().getBlockState(grassPos).is(ModBlocks.DIRT_SLAB.get()), "covered grass slab did not decay"); - helper.succeed(); - }); + }).thenSucceed(); } @GameTest(structure = EMPTY) @@ -709,6 +718,52 @@ private static boolean matchesSeedRecipe(CraftingRecipe recipe, GameTestHelper h .is(ModBlocks.GRASS_SLAB_ITEM.get()); } + @GameTest(structure = EMPTY) + public static void sharedGrassFormsKeepTheirOrientationAndWater(GameTestHelper helper) { + for (SlabType half : new SlabType[] {SlabType.BOTTOM, SlabType.TOP}) { + for (boolean waterlogged : new boolean[] {false, true}) { + BlockState dirt = ModBlocks.DIRT_SLAB.get().defaultBlockState() + .setValue(SlabBlock.TYPE, half).setValue(SlabBlock.WATERLOGGED, waterlogged); + BlockState grass = GrassSlabsApi.grassFor(dirt); + require(helper, grass != null && grass.is(ModBlocks.GRASS_SLAB.get()), + "The shared catalogue is missing a native grass slab form"); + require(helper, grass.getValue(SlabBlock.TYPE) == half + && grass.getValue(SlabBlock.WATERLOGGED) == waterlogged, + "The shared catalogue changed a slab's orientation or water"); + require(helper, GrassSlabsApi.dirtFor(grass) == dirt + && GrassSlabsApi.isSpreadingGrassForm(grass) == !waterlogged, + "The shared catalogue changed the dirt form or wet grass behaviour"); + } + } + helper.succeed(); + } + + @GameTest(structure = EMPTY) + public static void twoMatchingSlabsCraftBackToOneBlock(GameTestHelper helper) { + verifyRecombination(helper, "dirt_from_slabs", + new ItemStack(ModBlocks.DIRT_SLAB_ITEM.get()), Blocks.DIRT); + verifyRecombination(helper, "grass_from_slabs", + new ItemStack(ModBlocks.GRASS_SLAB_ITEM.get()), Blocks.GRASS_BLOCK); + helper.succeed(); + } + + private static void verifyRecombination(GameTestHelper helper, String recipeName, + ItemStack slab, Block fullBlock) { + CraftingRecipe recipe = (CraftingRecipe) helper.getLevel().recipeAccess() + .byKey(ResourceKey.create(Registries.RECIPE, + Identifier.fromNamespaceAndPath(SkysGrassSlabs.MOD_ID, recipeName))) + .orElseThrow().value(); + CraftingInput grid = craftingGrid(2, 1, slab.copy(), slab.copy()); + require(helper, recipe.matches(grid, helper.getLevel()), + "Matching slabs did not fit the recombination recipe"); + ItemStack result = recipe.assemble(grid); + require(helper, result.is(fullBlock.asItem()) && result.getCount() == 1, + "Two slabs must give back exactly one full block"); + CraftingInput singleSlab = craftingGrid(1, 1, slab.copy()); + require(helper, !recipe.matches(singleSlab, helper.getLevel()), + "A single slab must not give back a full block"); + } + private static CraftingInput craftingGrid(int width, int height, ItemStack... stacks) { NonNullList items = NonNullList.withSize(width * height, ItemStack.EMPTY); for (int slot = 0; slot < stacks.length; ++slot) { diff --git a/src/gameTest/java/zone/moddev/mc/skysgrassslabs/gametest/WorldgenGameTests.java b/src/gameTest/java/zone/moddev/mc/skysgrassslabs/gametest/WorldgenGameTests.java index a7a4f57..2a6d63f 100644 --- a/src/gameTest/java/zone/moddev/mc/skysgrassslabs/gametest/WorldgenGameTests.java +++ b/src/gameTest/java/zone/moddev/mc/skysgrassslabs/gametest/WorldgenGameTests.java @@ -53,6 +53,8 @@ public static void smoothingIsBorderSafeAndIdempotent(GameTestHelper helper) { BlockPos center = new BlockPos(owner.getMinBlockX() + 8, y, owner.getMinBlockZ() + 8); makeOneBlockRise(helper, center, center.east()); + helper.getLevel().setBlock(center.above(), Blocks.SHORT_GRASS.defaultBlockState(), + Block.UPDATE_ALL); BlockPos border = new BlockPos(owner.getMinBlockX(), y, owner.getMinBlockZ() + 8); makeOneBlockRise(helper, border, border.west()); @@ -78,7 +80,7 @@ public static void smoothingIsBorderSafeAndIdempotent(GameTestHelper helper) { RandomSource.create(19780401L), center), "controlled feature pass made no changes"); require(helper, helper.getLevel().getBlockState(center.above()) - .is(ModBlocks.GRASS_SLAB.get()), "one-block transition was not smoothed"); + .is(ModBlocks.GRASS_SLAB.get()), "short grass prevented a valid one-block transition"); require(helper, helper.getLevel().getBlockState(border.above()) .is(ModBlocks.GRASS_SLAB.get()), "owning-chunk border transition was not smoothed"); require(helper, helper.getLevel().getBlockState(border.west().above()) diff --git a/src/gameTest/resources/data/skysgrassslabs/test_instance/slab/shared_grass_forms_keep_their_orientation_and_water.json b/src/gameTest/resources/data/skysgrassslabs/test_instance/slab/shared_grass_forms_keep_their_orientation_and_water.json new file mode 100644 index 0000000..8b52219 --- /dev/null +++ b/src/gameTest/resources/data/skysgrassslabs/test_instance/slab/shared_grass_forms_keep_their_orientation_and_water.json @@ -0,0 +1,7 @@ +{ + "type": "minecraft:function", + "environment": "minecraft:default", + "function": "skysgrassslabs:slab/shared_grass_forms_keep_their_orientation_and_water", + "max_ticks": 100, + "structure": "forge:empty8x4x8" +} diff --git a/src/gameTest/resources/data/skysgrassslabs/test_instance/slab/two_matching_slabs_craft_back_to_one_block.json b/src/gameTest/resources/data/skysgrassslabs/test_instance/slab/two_matching_slabs_craft_back_to_one_block.json new file mode 100644 index 0000000..09e09d9 --- /dev/null +++ b/src/gameTest/resources/data/skysgrassslabs/test_instance/slab/two_matching_slabs_craft_back_to_one_block.json @@ -0,0 +1,7 @@ +{ + "type": "minecraft:function", + "environment": "minecraft:default", + "function": "skysgrassslabs:slab/two_matching_slabs_craft_back_to_one_block", + "max_ticks": 100, + "structure": "forge:empty8x4x8" +} diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java b/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java index 97acbf7..d15f3e4 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java @@ -2,10 +2,16 @@ import java.lang.reflect.InvocationTargetException; import java.util.Arrays; +import net.minecraft.world.level.block.SlabBlock; +import net.minecraft.world.level.block.state.properties.SlabType; +import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.event.server.ServerStartedEvent; import net.minecraftforge.eventbus.api.bus.BusGroup; import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.fml.loading.FMLEnvironment; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import zone.moddev.mc.skysgrassslabs.compat.BuildingBricksCompat; @@ -14,6 +20,8 @@ import zone.moddev.mc.skysgrassslabs.compat.LegacyMigrationHandler; import zone.moddev.mc.skysgrassslabs.compat.LegacyWorldDataHook; import zone.moddev.mc.skysgrassslabs.compat.MissingMappingHandler; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; +import zone.moddev.mc.skysgrassslabs.client.ClientConfig; import zone.moddev.mc.skysgrassslabs.config.SkysGrassSlabsConfig; import zone.moddev.mc.skysgrassslabs.event.CommonEvents; import zone.moddev.mc.skysgrassslabs.init.ModBlocks; @@ -27,7 +35,7 @@ public final class SkysGrassSlabs { /** Stable Forge mod identifier and resource namespace. */ public static final String MOD_ID = "skysgrassslabs"; public static final String NAME = "Sky's Grass Slabs"; - public static final String VERSION = "1.1.2.2603001"; + public static final String VERSION = "1.2.0.2603001"; public static final Logger LOGGER = LogManager.getLogger(); /** Registers content, configuration, world generation, and persistent state. */ @@ -46,10 +54,34 @@ public SkysGrassSlabs(FMLJavaModLoadingContext context) { GrassSlabsMigrationHandler.register(); CommonEvents.register(); registerGameTests(modBusGroup); + FMLCommonSetupEvent.getBus(modBusGroup).addListener(this::setupGrassForms); + FMLLoadCompleteEvent.getBus(modBusGroup).addListener(this::finishGrassRegistration); + if (FMLEnvironment.dist == Dist.CLIENT) { + ClientConfig.register(context); + } + ServerStartedEvent.BUS.addListener(this::onServerStarted); MissingMappingHandler.register(); } + private void setupGrassForms(FMLCommonSetupEvent event) { + SkysGrassSlabsConfig.activateSettingsForThisRun(); + GrassSlabsApi.configurationLoaded(); + for (SlabType half : new SlabType[] {SlabType.BOTTOM, SlabType.TOP}) { + for (boolean waterlogged : new boolean[] {false, true}) { + GrassSlabsApi.registerGrassForm( + ModBlocks.DIRT_SLAB.get().defaultBlockState() + .setValue(SlabBlock.TYPE, half).setValue(SlabBlock.WATERLOGGED, waterlogged), + ModBlocks.GRASS_SLAB.get().defaultBlockState() + .setValue(SlabBlock.TYPE, half).setValue(SlabBlock.WATERLOGGED, waterlogged)); + } + } + } + + private void finishGrassRegistration(FMLLoadCompleteEvent event) { + event.enqueueWork(GrassSlabsApi::freeze); + } + private static void registerGameTests(BusGroup modBusGroup) { try { Class bootstrap = Class.forName( diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/api/GrassSlabsApi.java b/src/main/java/zone/moddev/mc/skysgrassslabs/api/GrassSlabsApi.java new file mode 100644 index 0000000..3408f5b --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/api/GrassSlabsApi.java @@ -0,0 +1,234 @@ +package zone.moddev.mc.skysgrassslabs.api; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import net.minecraft.util.RandomSource; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SlabBlock; +import net.minecraft.world.level.block.SnowyBlock; +import net.minecraft.world.phys.AABB; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import zone.moddev.mc.skysgrassslabs.block.GrassSpread; +import zone.moddev.mc.skysgrassslabs.config.SkysGrassSlabsConfig; + +/** Optional grass and terrain integration. Register forms before load completion. */ +public final class GrassSlabsApi { + public static final int VERSION = 1; + + private static final Map GRASS = new LinkedHashMap<>(); + private static final Map DIRT = new LinkedHashMap<>(); + private static final Set NON_SPREADING = new HashSet<>(); + private static final Map SUPPORTS = new LinkedHashMap<>(); + private static final Map> SPREAD_RULES = + new LinkedHashMap<>(); + + private static String smoothingOwner; + private static BooleanSupplier ownerEnabled = () -> true; + private static boolean configurationLoaded; + private static boolean frozen; + + private GrassSlabsApi() { + } + + /** Claim once during construction, before common setup closes ownership registration. */ + public static void claimSmoothing(String owner, boolean enabled) { + claimSmoothing(owner, () -> enabled); + } + + /** Forge loads configuration after construction, so owners may supply their setting lazily. */ + public static synchronized void claimSmoothing(String owner, BooleanSupplier enabled) { + if (configurationLoaded || frozen || owner == null || owner.isEmpty() + || enabled == null || smoothingOwner != null) { + throw new IllegalStateException("Smoothing must be claimed once before common setup"); + } + + smoothingOwner = owner; + ownerEnabled = enabled; + } + + public static boolean hasSmoothingOwner() { + return smoothingOwner != null; + } + + public static boolean ownerAllowsGeneration() { + return smoothingOwner == null || ownerEnabled.getAsBoolean(); + } + + /** Respect the player's grass switch even when another mod owns terrain generation. */ + public static boolean grassGenerationAllowed() { + return SkysGrassSlabsConfig.isSmoothingActive(); + } + + /** Register each orientation and water state; snow is a derived visual property. */ + public static synchronized void registerGrassForm(BlockState dirt, BlockState grass) { + checkRegistrationOpen(); + checkForms(dirt, grass); + dirt = canonical(dirt); + grass = canonical(grass); + if (DIRT.containsKey(dirt) || GRASS.containsKey(grass)) { + throw new IllegalArgumentException("Conflicting grass form mapping"); + } + + DIRT.put(dirt, grass); + GRASS.put(grass, dirt); + } + + /** Additional skins can share dirt without replacing its ordinary regrowth. */ + public static synchronized void registerGrassVariant(BlockState dirt, BlockState grass, + boolean spreads) { + checkRegistrationOpen(); + checkForms(dirt, grass); + dirt = canonical(dirt); + grass = canonical(grass); + if (GRASS.containsKey(grass)) { + throw new IllegalArgumentException("Conflicting grass variant mapping"); + } + + GRASS.put(grass, dirt); + if (!spreads) { + NON_SPREADING.add(grass); + } + } + + public static boolean isGrassForm(BlockState state) { + return state != null && GRASS.containsKey(canonical(state)); + } + + public static boolean isSpreadingGrassForm(BlockState state) { + return isGrassForm(state) && !NON_SPREADING.contains(canonical(state)) + && !isWet(state); + } + + public static BlockState grassFor(BlockState dirt) { + return dirt == null ? null : DIRT.get(canonical(dirt)); + } + + /** Give source-aware rules first choice, then use the default grass for this dirt. */ + public static BlockState grassFor(BlockState dirt, BlockState source) { + if (dirt == null) { + return null; + } + for (BiFunction rule : SPREAD_RULES.values()) { + BlockState result = rule.apply(canonical(source), canonical(dirt)); + if (result != null) { + return result; + } + } + + return grassFor(dirt); + } + + public static BlockState dirtFor(BlockState grass) { + return grass == null ? null : GRASS.get(canonical(grass)); + } + + public static synchronized void registerSpreadRule(String owner, + BiFunction rule) { + checkRegistrationOpen(); + if (owner == null || owner.isEmpty() || rule == null || SPREAD_RULES.containsKey(owner)) { + throw new IllegalArgumentException("Invalid or conflicting grass spread rule"); + } + + SPREAD_RULES.put(owner, rule); + } + + /** Register native grass that may need dirtifying beneath a full-cover piece. */ + public static synchronized void registerGrassSupport(BlockState grass, BlockState dirt) { + checkRegistrationOpen(); + checkForms(dirt, grass); + if (SUPPORTS.containsKey(canonical(grass))) { + throw new IllegalArgumentException("Conflicting grass support mapping"); + } + + SUPPORTS.put(canonical(grass), canonical(dirt)); + } + + public static synchronized void configurationLoaded() { + configurationLoaded = true; + } + + public static synchronized void freeze() { + frozen = true; + } + + public static void tickDirt(Level world, BlockPos position, BlockState state, RandomSource random) { + GrassSpread.tickDirtSlab(world, position, state, random); + } + + /** Retain the registered shape and water state when grass turns back into dirt. */ + public static void tickGrass(Level world, BlockPos position, BlockState state, RandomSource random) { + if (world.isClientSide()) { + return; + } + + repairSupport(world, position); + if (isWet(state) + || !GrassSpread.canRemainGrass(world, position)) { + BlockState dirt = dirtFor(state); + if (dirt != null) { + world.setBlock(position, dirt, 3); + } + } else if (isSpreadingGrassForm(state)) { + GrassSpread.spreadFrom(world, position, random, + coversSupport(state, world, position) ? position.below() : null); + } + } + + public static void repairSupport(Level world, BlockPos position) { + if (world.isClientSide() || !world.isLoaded(position.below()) + || !coversSupport(world.getBlockState(position), world, position)) { + return; + } + + BlockState support = world.getBlockState(position.below()); + BlockState dirt = support.getBlock() == Blocks.GRASS_BLOCK + ? Blocks.DIRT.defaultBlockState() : SUPPORTS.get(canonical(support)); + if (dirt != null) { + // The support keeps its full shape. Send its new appearance to clients, + // but do not let neighbour-shape updates load chunks across a generation border. + world.setBlock(position.below(), dirt, 2 | 16); + } + } + + /** Steps and corners leave grass exposed; a slab or turf covers the whole footprint. */ + public static boolean coversSupport(BlockState piece, BlockGetter world, BlockPos position) { + VoxelShape shape = piece.getShape(world, position); + if (shape.isEmpty()) { + return false; + } + + AABB bounds = shape.bounds(); + return bounds.minX == 0 && bounds.maxX == 1 && bounds.minZ == 0 && bounds.maxZ == 1; + } + + private static BlockState canonical(BlockState state) { + return state != null && state.hasProperty(SnowyBlock.SNOWY) + ? state.setValue(SnowyBlock.SNOWY, false) : state; + } + + private static boolean isWet(BlockState state) { + return state.hasProperty(SlabBlock.WATERLOGGED) && state.getValue(SlabBlock.WATERLOGGED) + || !state.getFluidState().isEmpty(); + } + + private static void checkForms(BlockState dirt, BlockState grass) { + if (dirt == null || grass == null || canonical(dirt).equals(canonical(grass)) + || dirt.hasBlockEntity() || grass.hasBlockEntity()) { + throw new IllegalArgumentException("Grass forms need distinct states without block entities"); + } + } + + private static void checkRegistrationOpen() { + if (frozen) { + throw new IllegalStateException("Grass form registration is closed"); + } + } +} diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSlabBlock.java b/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSlabBlock.java index bee94d8..0d3c078 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSlabBlock.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSlabBlock.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.function.BiConsumer; import net.minecraft.util.RandomSource; import javax.annotation.Nullable; @@ -12,6 +13,7 @@ import net.minecraft.core.registries.Registries; import net.minecraft.data.worldgen.placement.VegetationPlacements; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; import net.minecraft.tags.BlockTags; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.BlockGetter; @@ -29,8 +31,11 @@ import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.level.block.state.properties.SlabType; import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraftforge.common.IPlantable; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; +import zone.moddev.mc.skysgrassslabs.compat.LegacyWorldDataHook; import zone.moddev.mc.skysgrassslabs.init.ModBlocks; /** Grass slab with target aware spreading and top slab vegetation behaviour. */ @@ -101,6 +106,16 @@ public void randomTick(BlockState state, ServerLevel level, BlockPos pos, Random GrassSpread.spreadFrom(level, pos, random, pos.below()); } + @Override + public boolean onTreeGrow(BlockState state, LevelReader level, + BiConsumer placeBlock, RandomSource random, BlockPos pos, + TreeFeature tree) { + // Trees decorating a chunk can reach slabs that were already saved. + return level instanceof WorldGenRegion region + && region.getLevel().dimension() == Level.OVERWORLD + && LegacyWorldDataHook.isSavedOverworldChunk(pos.getX() >> 4, pos.getZ() >> 4); + } + @Override public boolean canSustainPlant(BlockState state, BlockGetter level, BlockPos pos, Direction direction, IPlantable plantable) { @@ -181,8 +196,6 @@ public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos } private static void dirtifyGrassSupport(Level level, BlockPos pos) { - if (!level.isClientSide() && level.getBlockState(pos.below()).is(Blocks.GRASS_BLOCK)) { - level.setBlock(pos.below(), Blocks.DIRT.defaultBlockState(), Block.UPDATE_CLIENTS); - } + GrassSlabsApi.repairSupport(level, pos); } } diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSpread.java b/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSpread.java index 6a624b8..b2cc2ca 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSpread.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSpread.java @@ -5,7 +5,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.SectionPos; -import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; import net.minecraft.tags.FluidTags; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.SlabBlock; @@ -13,21 +13,18 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.SlabType; import net.minecraft.world.level.lighting.LightEngine; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; import zone.moddev.mc.skysgrassslabs.init.ModBlocks; /** Shared vanilla-shaped grass propagation for slabs and turf. */ public final class GrassSpread { private static final int SPREAD_ATTEMPTS = 4; - public static boolean canRemainGrass(ServerLevel level, BlockPos pos) { - BlockPos above = pos.above(); - BlockState cover = level.getBlockState(above); - return level.getMaxLocalRawBrightness(above) >= 4 - || LightEngine.getLightDampeningInto(lightSourceState(level.getBlockState(pos)), cover, - Direction.UP, cover.getLightDampening()) < 15; + public static boolean canRemainGrass(Level level, BlockPos pos) { + return SoilLifecycle.canRemainGrass(level.getBlockState(pos), level, pos); } - public static boolean hasSpreadLight(ServerLevel level, BlockPos pos) { + public static boolean hasSpreadLight(Level level, BlockPos pos) { BlockPos above = pos.above(); BlockState cover = level.getBlockState(above); return level.getMaxLocalRawBrightness(above) >= 9 @@ -36,7 +33,7 @@ public static boolean hasSpreadLight(ServerLevel level, BlockPos pos) { && !level.getFluidState(above).is(FluidTags.WATER); } - public static void spreadFrom(ServerLevel level, BlockPos source, RandomSource random, + public static void spreadFrom(Level level, BlockPos source, RandomSource random, @Nullable BlockPos excludedTarget) { if (!hasLoadedArea(level, source, 3) || !hasSpreadLight(level, source)) { return; @@ -48,11 +45,11 @@ public static void spreadFrom(ServerLevel level, BlockPos source, RandomSource r || !level.isLoaded(target)) { continue; } - growTarget(level, target); + growTarget(level, target, level.getBlockState(source)); } } - public static void tickDirtSlab(ServerLevel level, BlockPos target, BlockState state, + public static void tickDirtSlab(Level level, BlockPos target, BlockState state, RandomSource random) { if (!hasLoadedArea(level, target, 3) || !targetIsViable(level, target)) { return; @@ -64,37 +61,42 @@ public static void tickDirtSlab(ServerLevel level, BlockPos target, BlockState s continue; } if (isViableSource(level, source)) { - level.setBlockAndUpdate(target, snowyState(level, target, - ModBlocks.grassStateLike(state))); - return; + BlockState grass = GrassSlabsApi.grassFor(state, level.getBlockState(source)); + if (grass != null) { + level.setBlockAndUpdate(target, snowyState(level, target, grass)); + return; + } } } } - public static boolean growTarget(ServerLevel level, BlockPos target) { + public static boolean growTarget(Level level, BlockPos target) { + return growTarget(level, target, Blocks.GRASS_BLOCK.defaultBlockState()); + } + + private static boolean growTarget(Level level, BlockPos target, BlockState source) { if (!targetIsViable(level, target)) { return false; } BlockState state = level.getBlockState(target); - if (state.is(Blocks.DIRT)) { + if (state.is(ModBlocks.DIRT_SLAB.get()) + && state.getValue(SlabBlock.TYPE) == SlabType.DOUBLE) { return level.setBlockAndUpdate(target, Blocks.GRASS_BLOCK.defaultBlockState()); } - if (state.is(ModBlocks.DIRT_SLAB.get())) { - if (state.getValue(SlabBlock.TYPE) == SlabType.DOUBLE) { - return level.setBlockAndUpdate(target, Blocks.GRASS_BLOCK.defaultBlockState()); - } - return level.setBlockAndUpdate(target, - snowyState(level, target, ModBlocks.grassStateLike(state))); + + BlockState grass = GrassSlabsApi.grassFor(state, source); + if (grass == null && state.is(Blocks.DIRT)) { + grass = Blocks.GRASS_BLOCK.defaultBlockState(); } - return false; + return grass != null && level.setBlockAndUpdate(target, snowyState(level, target, grass)); } - public static boolean isViableSource(ServerLevel level, BlockPos pos) { + public static boolean isViableSource(Level level, BlockPos pos) { BlockState state = level.getBlockState(pos); if (!canRemainGrass(level, pos) || !hasSpreadLight(level, pos)) { return false; } - if (state.is(Blocks.GRASS_BLOCK)) { + if (state.is(Blocks.GRASS_BLOCK) || GrassSlabsApi.isSpreadingGrassForm(state)) { return true; } if (state.is(ModBlocks.GRASS_SLAB.get())) { @@ -104,17 +106,18 @@ public static boolean isViableSource(ServerLevel level, BlockPos pos) { && level.getBlockState(pos.below()).is(Blocks.DIRT); } - private static boolean targetIsViable(ServerLevel level, BlockPos target) { + private static boolean targetIsViable(Level level, BlockPos target) { BlockState state = level.getBlockState(target); - boolean dirt = state.is(Blocks.DIRT) - || state.is(ModBlocks.DIRT_SLAB.get()) - && !state.getValue(SlabBlock.WATERLOGGED); - if (!dirt) { + boolean dirt = state.is(Blocks.DIRT) || GrassSlabsApi.grassFor(state) != null + || state.is(ModBlocks.DIRT_SLAB.get()); + if (!dirt || !state.getFluidState().isEmpty()) { return false; } BlockPos above = target.above(); BlockState cover = level.getBlockState(above); - if (cover.is(ModBlocks.TURF.get()) || cover.is(ModBlocks.GRASS_SLAB.get())) { + if (cover.is(ModBlocks.TURF.get()) || GrassSlabsApi.isGrassForm(cover) + && (!state.isCollisionShapeFullBlock(level, target) + || GrassSlabsApi.coversSupport(cover, level, above))) { return false; } return level.getMaxLocalRawBrightness(above) >= 4 @@ -127,21 +130,22 @@ private static BlockState lightSourceState(BlockState state) { return state.is(ModBlocks.DIRT_SLAB.get()) || state.is(ModBlocks.GRASS_SLAB.get()) || state.is(ModBlocks.TURF.get()) + || GrassSlabsApi.isGrassForm(state) ? Blocks.AIR.defaultBlockState() : state; } - private static BlockState snowyState(ServerLevel level, BlockPos pos, BlockState state) { + private static BlockState snowyState(Level level, BlockPos pos, BlockState state) { return state.hasProperty(SnowyBlock.SNOWY) ? state.setValue(SnowyBlock.SNOWY, SnowySlabAppearance.hasNearbySnow(level, pos)) : state; } - private static boolean withinBuildHeight(ServerLevel level, BlockPos pos) { + private static boolean withinBuildHeight(Level level, BlockPos pos) { return pos.getY() >= level.getMinY() && pos.getY() <= level.getMaxY(); } - private static boolean hasLoadedArea(ServerLevel level, BlockPos center, int range) { + private static boolean hasLoadedArea(Level level, BlockPos center, int range) { int minX = SectionPos.blockToSectionCoord(center.getX() - range); int maxX = SectionPos.blockToSectionCoord(center.getX() + range); int minZ = SectionPos.blockToSectionCoord(center.getZ() - range); diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/block/SoilLifecycle.java b/src/main/java/zone/moddev/mc/skysgrassslabs/block/SoilLifecycle.java index a850052..07d21dd 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/block/SoilLifecycle.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/block/SoilLifecycle.java @@ -9,6 +9,7 @@ import net.minecraft.world.level.block.SnowLayerBlock; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.lighting.LightEngine; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; import zone.moddev.mc.skysgrassslabs.init.ModBlocks; /** Vanilla light and water rules adapted for slab identities. */ @@ -37,6 +38,7 @@ static boolean canRemainGrass(BlockState state, LevelReader level, BlockPos pos) // block's own collision shape. BlockState lightState = state.is(ModBlocks.GRASS_SLAB.get()) || state.is(ModBlocks.TURF.get()) + || GrassSlabsApi.isGrassForm(state) ? Blocks.AIR.defaultBlockState() : state; int blocked = LightEngine.getLightDampeningInto(lightState, cover, Direction.UP, cover.getLightDampening()); diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/client/ClientConfig.java b/src/main/java/zone/moddev/mc/skysgrassslabs/client/ClientConfig.java new file mode 100644 index 0000000..ae8d9a8 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/client/ClientConfig.java @@ -0,0 +1,15 @@ +package zone.moddev.mc.skysgrassslabs.client; + +import net.minecraftforge.client.ConfigScreenHandler; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; + +public final class ClientConfig { + private ClientConfig() { + } + + public static void register(FMLJavaModLoadingContext context) { + context.registerExtensionPoint(ConfigScreenHandler.ConfigScreenFactory.class, + () -> new ConfigScreenHandler.ConfigScreenFactory( + (minecraft, parent) -> new GrassSlabsConfigScreen(parent))); + } +} diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/client/GrassSlabsConfigScreen.java b/src/main/java/zone/moddev/mc/skysgrassslabs/client/GrassSlabsConfigScreen.java new file mode 100644 index 0000000..467fc55 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/client/GrassSlabsConfigScreen.java @@ -0,0 +1,71 @@ +package zone.moddev.mc.skysgrassslabs.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.network.chat.Component; +import zone.moddev.mc.skysgrassslabs.SkysGrassSlabs; +import zone.moddev.mc.skysgrassslabs.config.SkysGrassSlabsConfig; + +/** Save generation and recovery choices for the next Minecraft start. */ +public final class GrassSlabsConfigScreen extends Screen { + private static final String GENERATION_KEY = "config.skysgrassslabs.generateGrassSlabs"; + private static final String REPLACEMENT_KEY = "config.skysgrassslabs.forceReplaceBuildingBricksSlabs"; + private static final String GRASS_SLABS_KEY = "config.skysgrassslabs.forceReplaceGrassSlabsModContent"; + + private final Screen parent; + private boolean generateGrass; + private boolean replaceLegacy; + private boolean replaceGrassSlabs; + + public GrassSlabsConfigScreen(Screen parent) { + super(Component.literal(SkysGrassSlabs.NAME)); + this.parent = parent; + generateGrass = SkysGrassSlabsConfig.configuredGrassGeneration(); + replaceLegacy = SkysGrassSlabsConfig.configuredLegacyReplacement(); + replaceGrassSlabs = SkysGrassSlabsConfig.configuredGrassSlabsReplacement(); + } + + @Override + protected void init() { + int left = width / 2 - 150; + addRenderableWidget(Button.builder(label(GENERATION_KEY, generateGrass), button -> { + generateGrass = !generateGrass; + button.setMessage(label(GENERATION_KEY, generateGrass)); + }).bounds(left, 65, 300, 20).tooltip(tooltip(GENERATION_KEY)).build()); + addRenderableWidget(Button.builder(label(REPLACEMENT_KEY, replaceLegacy), button -> { + replaceLegacy = !replaceLegacy; + button.setMessage(label(REPLACEMENT_KEY, replaceLegacy)); + }).bounds(left, 95, 300, 20).tooltip(tooltip(REPLACEMENT_KEY)).build()); + addRenderableWidget(Button.builder(label(GRASS_SLABS_KEY, replaceGrassSlabs), button -> { + replaceGrassSlabs = !replaceGrassSlabs; + button.setMessage(label(GRASS_SLABS_KEY, replaceGrassSlabs)); + }).bounds(left, 125, 300, 20).tooltip(tooltip(GRASS_SLABS_KEY)).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), button -> { + SkysGrassSlabsConfig.saveForNextStart(generateGrass, replaceLegacy, replaceGrassSlabs); + minecraft.gui.setScreen(parent); + }).bounds(width / 2 - 100, height - 35, 200, 20).build()); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTicks) { + graphics.centeredText(font, title, width / 2, 25, 0xFFFFFFFF); + super.extractRenderState(graphics, mouseX, mouseY, partialTicks); + } + + @Override + public void onClose() { + // Escape returns without saving the pending button values. + minecraft.gui.setScreen(parent); + } + + private static Component label(String key, boolean enabled) { + return Component.literal(I18n.get(key) + ": " + I18n.get(enabled ? "options.on" : "options.off")); + } + + private static Tooltip tooltip(String key) { + return Tooltip.create(Component.translatable(key + ".tooltip")); + } +} diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyWorldDataHook.java b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyWorldDataHook.java index d695e06..35b0820 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyWorldDataHook.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyWorldDataHook.java @@ -44,12 +44,14 @@ public final class LegacyWorldDataHook { private static final Logger LOGGER = LogManager.getLogger(); private static final BitSet SUPPORTED_BLOCK_IDS = new BitSet(); private static final Set LEGACY_CHUNKS = ConcurrentHashMap.newKeySet(); + private static final Set SAVED_OVERWORLD_CHUNKS = ConcurrentHashMap.newKeySet(); private static final Set SKY_IDS = new LinkedHashSet<>(); private static final Set HISTORICAL_IDS = new LinkedHashSet<>(); private static final String PRESERVE_CHUNK_MARKER = "SkysGrassSlabsLegacyPreserveChunk"; private static final String SIDECAR_NAME = "skysgrassslabs_legacy_registry.dat"; private static volatile boolean legacyWorldActive; + private static CompoundTag legacyFmlData = new CompoundTag(); private static boolean registered; static { @@ -90,9 +92,9 @@ public static void captureLegacyLevelData(LevelStorageSource.LevelStorageAccess } Path levelPath = levelDirectory.path(); if (root.contains("FML")) { - prepareLegacyWorld(levelPath.toFile(), root.getCompoundOrEmpty("FML")); + prepareLegacyWorld(levelPath.toFile(), root.getCompoundOrEmpty("FML"), true); } else if (root.contains("fml")) { - prepareLegacyWorld(levelPath.toFile(), root.getCompoundOrEmpty("fml")); + prepareLegacyWorld(levelPath.toFile(), root.getCompoundOrEmpty("fml"), false); } else { prepareLegacyWorld(levelPath.resolve("level.dat").toFile()); } @@ -101,20 +103,24 @@ public static void captureLegacyLevelData(LevelStorageSource.LevelStorageAccess public static void onServerAboutToStart(ServerAboutToStartEvent event) { File levelDat = event.getServer().getWorldPath(LevelResource.LEVEL_DATA_FILE).toFile(); prepareLegacyWorld(levelDat); + snapshotSavedOverworldChunks(levelDat.getParentFile()); } static synchronized void prepareLegacyWorld(File levelDat) { legacyWorldActive = false; + legacyFmlData = new CompoundTag(); LEGACY_CHUNKS.clear(); if (!levelDat.isFile()) { + readSidecar(levelDat.getParentFile()); return; } try (FileInputStream input = new FileInputStream(levelDat)) { CompoundTag root = NbtIo.readCompressed(input, NbtAccounter.unlimitedHeap()); if (root.contains("FML")) { - CompoundTag registries = root.getCompoundOrEmpty("FML") - .getCompoundOrEmpty("Registries"); + CompoundTag fmlData = root.getCompoundOrEmpty("FML"); + CompoundTag registries = fmlData.getCompoundOrEmpty("Registries"); if (registries.contains("minecraft:blocks")) { + legacyFmlData = fmlData.copy(); CompoundTag blocks = registries.getCompoundOrEmpty("minecraft:blocks"); install(levelDat.getParentFile(), blocks); writeSidecar(levelDat.getParentFile(), blocks); @@ -124,17 +130,21 @@ static synchronized void prepareLegacyWorld(File levelDat) { } catch (IOException exception) { LOGGER.warn("Could not inspect '{}' for legacy Sky's Grass Slabs registry data", levelDat, exception); - return; } readSidecar(levelDat.getParentFile()); } - private static synchronized void prepareLegacyWorld(File worldDirectory, CompoundTag fmlData) { + private static synchronized void prepareLegacyWorld(File worldDirectory, CompoundTag fmlData, + boolean uppercase) { legacyWorldActive = false; + legacyFmlData = new CompoundTag(); LEGACY_CHUNKS.clear(); if (fmlData.contains("Registries")) { CompoundTag registries = fmlData.getCompoundOrEmpty("Registries"); if (registries.contains("minecraft:blocks")) { + if (uppercase) { + legacyFmlData = fmlData.copy(); + } CompoundTag blocks = registries.getCompoundOrEmpty("minecraft:blocks"); install(worldDirectory, blocks); writeSidecar(worldDirectory, blocks); @@ -144,14 +154,22 @@ private static synchronized void prepareLegacyWorld(File worldDirectory, Compoun readSidecar(worldDirectory); } + /** Keep the old numeric registry alongside Forge's current lowercase snapshot. */ + public static synchronized void writeLegacyLevelData(CompoundTag levelTag) { + if (!legacyFmlData.isEmpty() && !levelTag.contains("FML")) { + levelTag.put("FML", legacyFmlData.copy()); + } + } + private static void readSidecar(File worldDirectory) { File sidecar = sidecar(worldDirectory); if (!sidecar.isFile()) { return; } try (FileInputStream input = new FileInputStream(sidecar)) { - install(worldDirectory, NbtIo.readCompressed(input, - NbtAccounter.unlimitedHeap()).getCompoundOrEmpty("Blocks")); + CompoundTag snapshot = NbtIo.readCompressed(input, NbtAccounter.unlimitedHeap()); + snapshot.getCompound("FML").ifPresent(tag -> legacyFmlData = tag.copy()); + install(worldDirectory, snapshot.getCompoundOrEmpty("Blocks")); } catch (IOException exception) { LOGGER.warn("Could not read legacy Sky's Grass Slabs registry sidecar '{}'", sidecar, exception); @@ -162,7 +180,7 @@ private static void install(File worldDirectory, CompoundTag blockSnapshot) { int mapped = installLegacyBlockStates(blockSnapshot); legacyWorldActive = mapped > 0; if (legacyWorldActive) { - int indexed = indexLegacyChunks(worldDirectory); + int indexed = indexRegionChunks(worldDirectory, LEGACY_CHUNKS); LOGGER.info("Prepared {} legacy slab states and protected {} existing Overworld " + "chunks from '{}'", mapped, indexed, worldDirectory); } @@ -179,6 +197,9 @@ private static void writeSidecar(File worldDirectory, CompoundTag blockSnapshot) Files.createDirectories(parent.toPath()); CompoundTag root = new CompoundTag(); root.put("Blocks", blockSnapshot.copy()); + if (!legacyFmlData.isEmpty()) { + root.put("FML", legacyFmlData.copy()); + } try (FileOutputStream output = new FileOutputStream(temporary)) { NbtIo.writeCompressed(root, output); } @@ -235,6 +256,20 @@ public static boolean isLegacyChunk(int chunkX, int chunkZ) { return legacyWorldActive && LEGACY_CHUNKS.contains(chunkKey(chunkX, chunkZ)); } + static void snapshotSavedOverworldChunks(File worldDirectory) { + if (legacyWorldActive) { + SAVED_OVERWORLD_CHUNKS.clear(); + SAVED_OVERWORLD_CHUNKS.addAll(LEGACY_CHUNKS); + } else { + indexRegionChunks(worldDirectory, SAVED_OVERWORLD_CHUNKS); + } + } + + /** A saved chunk may still go through decoration when the world next opens. */ + public static boolean isSavedOverworldChunk(int chunkX, int chunkZ) { + return SAVED_OVERWORLD_CHUNKS.contains(chunkKey(chunkX, chunkZ)) || isLegacyChunk(chunkX, chunkZ); + } + private static int installLegacyBlockStates(CompoundTag blockSnapshot) { SUPPORTED_BLOCK_IDS.clear(); Map supported = new LinkedHashMap<>(); @@ -321,15 +356,15 @@ private static boolean containsSupportedBlock(CompoundTag level) { return false; } - private static int indexLegacyChunks(File worldDirectory) { - LEGACY_CHUNKS.clear(); - indexLegacyRegionDirectory(new File(worldDirectory, "region")); - indexLegacyRegionDirectory(worldDirectory.toPath().resolve("dimensions") - .resolve("minecraft").resolve("overworld").resolve("region").toFile()); - return LEGACY_CHUNKS.size(); + private static int indexRegionChunks(File worldDirectory, Set chunks) { + chunks.clear(); + indexRegionDirectory(new File(worldDirectory, "region"), chunks); + indexRegionDirectory(worldDirectory.toPath().resolve("dimensions") + .resolve("minecraft").resolve("overworld").resolve("region").toFile(), chunks); + return chunks.size(); } - private static void indexLegacyRegionDirectory(File regionDirectory) { + private static void indexRegionDirectory(File regionDirectory, Set chunks) { File[] regionFiles = regionDirectory.listFiles((directory, name) -> name.startsWith("r.") && (name.endsWith(".mca") || name.endsWith(".mcr"))); if (regionFiles == null) { @@ -362,12 +397,12 @@ private static void indexLegacyRegionDirectory(File regionDirectory) { int offset = index * 4; if ((locations[offset] | locations[offset + 1] | locations[offset + 2] | locations[offset + 3]) != 0) { - LEGACY_CHUNKS.add(chunkKey(regionX * 32 + (index & 31), + chunks.add(chunkKey(regionX * 32 + (index & 31), regionZ * 32 + (index >> 5))); } } } catch (IOException exception) { - LOGGER.warn("Could not inspect legacy chunk locations in '{}'", regionFile, + LOGGER.warn("Could not inspect saved chunk locations in '{}'", regionFile, exception); } } diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/config/SkysGrassSlabsConfig.java b/src/main/java/zone/moddev/mc/skysgrassslabs/config/SkysGrassSlabsConfig.java index a785cb8..3d98768 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/config/SkysGrassSlabsConfig.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/config/SkysGrassSlabsConfig.java @@ -5,12 +5,15 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import com.electronwill.nightconfig.core.CommentedConfig; import org.apache.commons.lang3.tuple.Pair; import net.minecraftforge.common.ForgeConfigSpec; import net.minecraftforge.fml.config.ModConfig; +import net.minecraftforge.fml.event.config.ModConfigEvent; import net.minecraftforge.fml.loading.FMLPaths; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import zone.moddev.mc.skysgrassslabs.SkysGrassSlabs; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; /** Common configuration and one-time migration from the 1.10 configuration file. */ public final class SkysGrassSlabsConfig { @@ -23,7 +26,12 @@ public final class SkysGrassSlabsConfig { private static final Common COMMON; private static final ForgeConfigSpec SPEC; + private static CommentedConfig loadedValues; private static volatile boolean compatibilitySuppressed; + private static boolean settingsActivated; + private static boolean activeGrassGeneration; + private static boolean activeLegacyReplacement; + private static boolean activeGrassSlabsReplacement; static { Pair pair = new ForgeConfigSpec.Builder().configure(Common::new); @@ -33,6 +41,11 @@ public final class SkysGrassSlabsConfig { public static void register(FMLJavaModLoadingContext context) { context.registerConfig(ModConfig.Type.COMMON, SPEC, FILE_NAME); + ModConfigEvent.Loading.getBus(context.getModBusGroup()).addListener(event -> { + if (event.getConfig().getSpec() == SPEC) { + loadedValues = event.getConfig().getConfigData(); + } + }); } public static void migrateLegacyConfig() { @@ -94,19 +107,66 @@ private static boolean readLegacyBoolean(Path file, String key, boolean fallback } public static boolean generateGrassSlabs() { - return COMMON.generateGrassSlabs.get(); + return settingsActivated ? activeGrassGeneration : configuredGrassGeneration(); } public static boolean forceReplaceBuildingBricksSlabs() { - return COMMON.forceReplaceBuildingBricksSlabs.get(); + return settingsActivated ? activeLegacyReplacement : configuredLegacyReplacement(); } public static boolean forceReplaceGrassSlabsModContent() { + return settingsActivated ? activeGrassSlabsReplacement : configuredGrassSlabsReplacement(); + } + + public static boolean configuredGrassSlabsReplacement() { return COMMON.forceReplaceGrassSlabsModContent.get(); } + /** Keep a running world on its startup settings when the Config screen saves new choices. */ + public static void activateSettingsForThisRun() { + activeGrassGeneration = configuredGrassGeneration(); + activeLegacyReplacement = configuredLegacyReplacement(); + activeGrassSlabsReplacement = configuredGrassSlabsReplacement(); + settingsActivated = true; + } + + public static boolean configuredGrassGeneration() { + return COMMON.generateGrassSlabs.get(); + } + + public static boolean configuredLegacyReplacement() { + return COMMON.forceReplaceBuildingBricksSlabs.get(); + } + + public static void saveForNextStart(boolean generateGrass, boolean replaceLegacy) { + saveForNextStart(generateGrass, replaceLegacy, configuredGrassSlabsReplacement()); + } + + public static void saveForNextStart(boolean generateGrass, boolean replaceLegacy, + boolean replaceGrassSlabs) { + if (loadedValues == null) { + throw new IllegalStateException("Grass Slabs settings have not loaded yet"); + } + // Forge autosaves each edit. Publish the complete choices together so + // its file watcher cannot reload between the three setting changes. + CommentedConfig pending = CommentedConfig.copy(loadedValues); + CommentedConfig worldgen = CommentedConfig.copy(loadedValues.get("worldgen")); + CommentedConfig compatibility = CommentedConfig.copy(loadedValues.get("compat")); + worldgen.set(GENERATE_GRASS_SLABS, generateGrass); + compatibility.set(FORCE_REPLACE_BUILDINGBRICKS_SLABS, replaceLegacy); + compatibility.set(FORCE_REPLACE_GRASS_SLABS_MOD_CONTENT, replaceGrassSlabs); + pending.set("worldgen", worldgen); + pending.set("compat", compatibility); + loadedValues.putAll(pending); + // Refresh the screen's choices while retaining the active session's + // startup settings until Minecraft is restarted. + SPEC.afterReload(); + SPEC.save(); + } + public static boolean isSmoothingActive() { - return generateGrassSlabs() && !compatibilitySuppressed; + return generateGrassSlabs() && !compatibilitySuppressed + && GrassSlabsApi.ownerAllowsGeneration(); } public static void suppressSmoothingForThisRun() { @@ -115,6 +175,7 @@ public static void suppressSmoothingForThisRun() { static void resetRunStateForTests() { compatibilitySuppressed = false; + settingsActivated = false; } private static final class Common { diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/mixin/ForgeHooksMixin.java b/src/main/java/zone/moddev/mc/skysgrassslabs/mixin/ForgeHooksMixin.java index ff4a8b5..399d816 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/mixin/ForgeHooksMixin.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/mixin/ForgeHooksMixin.java @@ -1,6 +1,8 @@ package zone.moddev.mc.skysgrassslabs.mixin; +import net.minecraft.nbt.CompoundTag; import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.WorldData; import net.minecraftforge.common.ForgeHooks; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -17,4 +19,10 @@ abstract class ForgeHooksMixin { LevelStorageSource.LevelDirectory levelDirectory, CallbackInfo callback) { LegacyWorldDataHook.captureLegacyLevelData(access, levelDirectory); } + + @Inject(method = "writeAdditionalLevelSaveData", at = @At("TAIL")) + private static void skysgrassslabs$preserveLegacyLevelData( + WorldData worldData, CompoundTag levelTag, CallbackInfo callback) { + LegacyWorldDataHook.writeLegacyLevelData(levelTag); + } } diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/mixin/SimpleRegionStorageMixin.java b/src/main/java/zone/moddev/mc/skysgrassslabs/mixin/SimpleRegionStorageMixin.java index 729d7c4..9085799 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/mixin/SimpleRegionStorageMixin.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/mixin/SimpleRegionStorageMixin.java @@ -23,11 +23,12 @@ abstract class SimpleRegionStorageMixin { @Inject( method = "upgradeChunkTag(Lnet/minecraft/nbt/CompoundTag;ILnet/minecraft/nbt/CompoundTag;I)Lnet/minecraft/nbt/CompoundTag;", - at = @At("RETURN"), cancellable = true) + // Older compatibility mixins may cancel later return callbacks. + at = @At("RETURN"), order = 900) private void skysgrassslabs$finalizeLegacyChunk(CompoundTag chunk, int fallbackDataVersion, @Nullable CompoundTag context, int targetDataVersion, CallbackInfoReturnable callback) { - callback.setReturnValue(LegacyWorldDataHook.finalizeLegacyChunk( - callback.getReturnValue())); + // The hook updates the tag in place, so other mods can finish upgrading it too. + LegacyWorldDataHook.finalizeLegacyChunk(callback.getReturnValue()); } } diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/world/GrassSlabSmoothingFeature.java b/src/main/java/zone/moddev/mc/skysgrassslabs/world/GrassSlabSmoothingFeature.java index 326edb2..64fcd1d 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/world/GrassSlabSmoothingFeature.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/world/GrassSlabSmoothingFeature.java @@ -9,6 +9,7 @@ import net.minecraft.server.level.WorldGenRegion; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.DoublePlantBlock; import net.minecraft.world.level.block.SlabBlock; import net.minecraft.world.level.block.SnowyBlock; import net.minecraft.world.level.block.state.BlockState; @@ -18,6 +19,7 @@ import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.feature.Feature; import net.minecraft.util.RandomSource; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; import zone.moddev.mc.skysgrassslabs.compat.LegacyWorldDataHook; import zone.moddev.mc.skysgrassslabs.config.SkysGrassSlabsConfig; import zone.moddev.mc.skysgrassslabs.init.ModBlocks; @@ -39,7 +41,7 @@ public MapCodec codec() { @Override public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) { - if (!SkysGrassSlabsConfig.isSmoothingActive() + if (GrassSlabsApi.hasSmoothingOwner() || !SkysGrassSlabsConfig.isSmoothingActive() || level.getLevel().dimension() != Level.OVERWORLD) { return false; } @@ -47,7 +49,12 @@ public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ? level.getChunk(region.getCenter().x(), region.getCenter().z()) : level.getChunk(origin); ChunkPos ownerPos = owner.getPos(); - if (LegacyWorldDataHook.isLegacyChunk(ownerPos.x(), ownerPos.z())) { + if (LegacyWorldDataHook.isSavedOverworldChunk(ownerPos.x(), ownerPos.z())) { + return false; + } + StructureProtection structures = StructureProtection.read(owner, position -> + level.hasChunk(position.x(), position.z()) ? level.getChunk(position.x(), position.z()) : null); + if (!structures.canInspectSafely()) { return false; } @@ -55,6 +62,7 @@ public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource Arrays.fill(buffer.heights, MISSING); Arrays.fill(buffer.grass, false); Arrays.fill(buffer.candidates, false); + Arrays.fill(buffer.originalTargets, null); BlockPos.MutableBlockPos cursor = buffer.cursor; for (int haloZ = 0; haloZ < HALO_WIDTH; ++haloZ) { @@ -75,7 +83,9 @@ public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource if (surfaceY != MISSING) { cursor.set(chunk.getPos().getMinBlockX() + x, surfaceY, chunk.getPos().getMinBlockZ() + z); - buffer.grass[index] = chunk.getBlockState(cursor).is(Blocks.GRASS_BLOCK); + BlockState surface = chunk.getBlockState(cursor); + buffer.grass[index] = surface.is(Blocks.GRASS_BLOCK) + || surface.is(Blocks.DIRT) && isGrassCap(chunk.getBlockState(cursor.above())); } } } @@ -91,7 +101,11 @@ public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource } cursor.set(minX + localX, y + 1, minZ + localZ); BlockState target = owner.getBlockState(cursor); - boolean clear = target.isAir() && owner.getBlockEntityNbt(cursor) == null; + if (structures.contains(cursor) || structures.contains(cursor.below()) + || target.getBlock() instanceof DoublePlantBlock && structures.contains(cursor.above())) { + continue; + } + boolean clear = SmoothingFoliage.canReplace(level, owner, cursor); boolean dry = target.getFluidState().isEmpty(); cursor.setY(y); BlockState support = owner.getBlockState(cursor); @@ -100,11 +114,15 @@ public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource int south = haloIndex(localX + 1, localZ + 2); int west = haloIndex(localX, localZ + 1); int east = haloIndex(localX + 2, localZ + 1); - buffer.candidates[localZ << 4 | localX] = SmoothingDecision.shouldPlace( + int index = localZ << 4 | localX; + buffer.candidates[index] = SmoothingDecision.shouldPlace( y, buffer.heights[north], buffer.heights[south], buffer.heights[west], buffer.heights[east], buffer.grass[center], buffer.grass[north], buffer.grass[south], buffer.grass[west], buffer.grass[east], clear, dry, supported); + if (buffer.candidates[index]) { + buffer.originalTargets[index] = target; + } } } @@ -121,12 +139,21 @@ public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource int localZ = index >>> 4; int y = buffer.heights[haloIndex(localX + 1, localZ + 1)]; cursor.set(minX + localX, y + 1, minZ + localZ); - if (!level.ensureCanWrite(cursor) || !owner.getBlockState(cursor).isAir() - || !owner.getFluidState(cursor).isEmpty() - || owner.getBlockEntityNbt(cursor) != null) { + BlockState target = buffer.originalTargets[index]; + boolean tallPlant = target.getBlock() instanceof DoublePlantBlock; + if (!level.ensureCanWrite(cursor) || !level.ensureCanWrite(cursor.below()) + || tallPlant && !level.ensureCanWrite(cursor.above()) + || owner.getBlockState(cursor) != target + || !SmoothingFoliage.canReplace(level, owner, cursor)) { continue; } owner.setBlockState(cursor, slab, 0); + if (owner.getBlockState(cursor) != slab) { + continue; + } + if (tallPlant) { + owner.setBlockState(cursor.above(), Blocks.AIR.defaultBlockState(), 0); + } cursor.setY(y); if (owner.getBlockState(cursor).is(Blocks.GRASS_BLOCK)) { owner.setBlockState(cursor, Blocks.DIRT.defaultBlockState(), 0); @@ -142,12 +169,26 @@ private static int surfaceY(ChunkAccess chunk, int localX, int localZ, int minY = chunk.getMinY(); int x = chunk.getPos().getMinBlockX() + localX; int z = chunk.getPos().getMinBlockZ() + localZ; - while (y >= minY && chunk.getBlockState(cursor.set(x, y, z)).isAir()) { + while (y >= minY) { + BlockState state = chunk.getBlockState(cursor.set(x, y, z)); + if (!state.isAir() && !SmoothingFoliage.isPlant(state)) { + break; + } + --y; + } + if (y > minY && isGrassCap(chunk.getBlockState(cursor.set(x, y, z))) + && chunk.getBlockState(cursor.below()).is(Blocks.DIRT)) { --y; } return y >= minY ? y : MISSING; } + private static boolean isGrassCap(BlockState state) { + return state.is(ModBlocks.GRASS_SLAB.get()) + && state.getValue(SlabBlock.TYPE) == SlabType.BOTTOM + && !state.getValue(SlabBlock.WATERLOGGED); + } + private static int haloIndex(int x, int z) { return z * HALO_WIDTH + x; } @@ -156,6 +197,7 @@ private static final class DecisionBuffer { private final int[] heights = new int[HALO_WIDTH * HALO_WIDTH]; private final boolean[] grass = new boolean[HALO_WIDTH * HALO_WIDTH]; private final boolean[] candidates = new boolean[256]; + private final BlockState[] originalTargets = new BlockState[256]; private final BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); } } diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/world/ModWorldState.java b/src/main/java/zone/moddev/mc/skysgrassslabs/world/ModWorldState.java index 2205622..89a2263 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/world/ModWorldState.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/world/ModWorldState.java @@ -16,7 +16,6 @@ import net.minecraft.world.level.saveddata.SavedData; import net.minecraft.world.level.saveddata.SavedDataType; import net.minecraft.world.level.storage.LevelResource; -import net.minecraft.util.datafix.DataFixTypes; import zone.moddev.mc.skysgrassslabs.SkysGrassSlabs; /** Persistent schema and aggregate historical-slab migration totals. */ @@ -24,10 +23,12 @@ public final class ModWorldState extends SavedData { public static final String DATA_NAME = "skysgrassslabs_world_state"; public static final int SCHEMA_VERSION = 1; public static final int MIGRATION_VERSION = 1; - private static final SavedDataType TYPE = new SavedDataType<>( + // Older saves may have no DataVersion. The vanilla map fixer would move + // our counters out of the data section, so let our codec read them directly. + static final SavedDataType TYPE = new SavedDataType<>( Identifier.fromNamespaceAndPath(SkysGrassSlabs.MOD_ID, DATA_NAME), ModWorldState::new, CompoundTag.CODEC.xmap(ModWorldState::new, ModWorldState::saveTag), - DataFixTypes.SAVED_DATA_MAP_DATA); + null); private long migratedChunks; private long migratedGrassBlocks; @@ -327,7 +328,7 @@ private void load(CompoundTag tag) { } } - private CompoundTag saveTag() { + CompoundTag saveTag() { CompoundTag tag = new CompoundTag(); tag.putInt("schema_version", SCHEMA_VERSION); tag.putInt("buildingbricks_migration_version", MIGRATION_VERSION); diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingBiomeModifier.java b/src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingBiomeModifier.java index fb1e494..46c0b69 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingBiomeModifier.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingBiomeModifier.java @@ -10,6 +10,7 @@ import net.minecraft.world.level.biome.BiomeGenerationSettings; import net.minecraftforge.common.world.BiomeModifier; import net.minecraftforge.common.world.ModifiableBiomeInfo.BiomeInfo; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; /** Adds the smoother before vegetation in every non-Nether, non-End biome. */ public final class SmoothingBiomeModifier implements BiomeModifier { @@ -22,7 +23,7 @@ private SmoothingBiomeModifier() { @Override public void modify(Holder biome, Phase phase, BiomeInfo.Builder builder) { Holder smoothing = WorldgenBootstrap.placedFeature(); - if (phase != Phase.AFTER_EVERYTHING || smoothing == null + if (phase != Phase.AFTER_EVERYTHING || smoothing == null || GrassSlabsApi.hasSmoothingOwner() || biome.is(BiomeTags.IS_NETHER) || biome.is(BiomeTags.IS_END)) { return; } diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingFoliage.java b/src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingFoliage.java new file mode 100644 index 0000000..5a07590 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingFoliage.java @@ -0,0 +1,55 @@ +package zone.moddev.mc.skysgrassslabs.world; + +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.DoublePlantBlock; +import net.minecraft.world.level.block.state.properties.DoubleBlockHalf; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.chunk.ChunkAccess; + +/** Small native plants may give way to a transition in a newly generated chunk. */ +final class SmoothingFoliage { + static boolean isPlant(BlockState state) { + Block block = state.getBlock(); + return block == Blocks.SHORT_GRASS || block == Blocks.FERN || block == Blocks.DEAD_BUSH + || block == Blocks.DANDELION || block == Blocks.POPPY || block == Blocks.BLUE_ORCHID + || block == Blocks.ALLIUM || block == Blocks.AZURE_BLUET || block == Blocks.RED_TULIP + || block == Blocks.ORANGE_TULIP || block == Blocks.WHITE_TULIP || block == Blocks.PINK_TULIP + || block == Blocks.OXEYE_DAISY || block == Blocks.CORNFLOWER + || block == Blocks.LILY_OF_THE_VALLEY || block == Blocks.TALL_GRASS + || block == Blocks.LARGE_FERN || block == Blocks.SUNFLOWER || block == Blocks.LILAC + || block == Blocks.ROSE_BUSH || block == Blocks.PEONY; + } + + static boolean canReplace(LevelAccessor world, ChunkAccess owner, BlockPos position) { + BlockState state = owner.getBlockState(position); + if (state.hasBlockEntity() || owner.getBlockEntity(position) != null + || owner.getBlockEntityNbt(position) != null + || !state.getFluidState().isEmpty()) { + return false; + } + if (state.isAir()) { + return true; + } + if (!isPlant(state)) { + return false; + } + if (!(state.getBlock() instanceof DoublePlantBlock)) { + return true; + } + if (position.getY() >= owner.getMaxY() || state.getValue(DoublePlantBlock.HALF) != DoubleBlockHalf.LOWER) { + return false; + } + + BlockState upper = owner.getBlockState(position.above()); + return upper.getBlock() == state.getBlock() + && upper.getValue(DoublePlantBlock.HALF) == DoubleBlockHalf.UPPER + && owner.getBlockEntity(position.above()) == null + && owner.getBlockEntityNbt(position.above()) == null; + } + + private SmoothingFoliage() { + } +} diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/world/StructureProtection.java b/src/main/java/zone/moddev/mc/skysgrassslabs/world/StructureProtection.java new file mode 100644 index 0000000..a17c083 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/world/StructureProtection.java @@ -0,0 +1,65 @@ +package zone.moddev.mc.skysgrassslabs.world; + +import it.unimi.dsi.fastutil.longs.LongSet; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +/** Read known structure bounds without asking the generator for another chunk. */ +final class StructureProtection { + private final List bounds = new ArrayList<>(); + private boolean unresolved; + + static StructureProtection read(ChunkAccess owner, Function loadedChunk) { + StructureProtection result = new StructureProtection(); + Set visited = new HashSet<>(); + visited.add(owner.getPos().pack()); + result.addStarts(owner); + for (LongSet references : owner.getAllReferences().values()) { + for (long reference : references) { + if (!visited.add(reference)) { + continue; + } + ChunkAccess startChunk = loadedChunk.apply(ChunkPos.unpack(reference)); + if (startChunk == null) { + // The reference may point beyond the loaded generation halo. + // Leave this chunk alone instead of loading it to find out. + result.unresolved = true; + } else { + result.addStarts(startChunk); + } + } + } + return result; + } + + boolean canInspectSafely() { + return !unresolved; + } + + boolean contains(BlockPos position) { + for (BoundingBox box : bounds) { + if (box.isInside(position)) { + return true; + } + } + return false; + } + + private void addStarts(ChunkAccess chunk) { + for (StructureStart start : chunk.getAllStarts().values()) { + if (start != null && start.isValid()) { + BoundingBox box = start.getBoundingBox(); + bounds.add(new BoundingBox(box.minX(), box.minY(), box.minZ(), + box.maxX(), box.maxY(), box.maxZ())); + } + } + } +} diff --git a/src/main/resources/assets/skysgrassslabs/lang/de_at.json b/src/main/resources/assets/skysgrassslabs/lang/de_at.json index 41f3d83..08ced0b 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/de_at.json +++ b/src/main/resources/assets/skysgrassslabs/lang/de_at.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Eanstufn", "block.skysgrassslabs.grass_slab": "Grosstufn", "block.skysgrassslabs.path_slab": "Steigstufn", - "block.skysgrassslabs.turf": "Grassodn" + "block.skysgrassslabs.turf": "Grassodn", + "config.skysgrassslabs.generateGrassSlabs": "Grasstufen erzeugen", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Alte Stufen ersetzen", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Fügt Grasplatten an neuen Hängen der Oberwelt hinzu. Mit Terrain Smoother steuert dies nur Gras. Bestehende Chunks bleiben unverändert. Minecraft neu starten.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Kompatibilitätseinstellung für alte Welten. In normalen 1.21.1-Modpacks ausgeschaltet lassen. Alte Platten werden automatisch wiederhergestellt. Minecraft neu starten.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Grass Slabs-Inhalte ersetzen", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Wandelt unterstützte Inhalte von Grass Slabs um, solange die Mod installiert ist. Nach dem Entfernen bleibt die automatische Wiederherstellung aktiv. Minecraft neu starten." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/de_au.json b/src/main/resources/assets/skysgrassslabs/lang/de_au.json index 6f1858f..d5427b1 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/de_au.json +++ b/src/main/resources/assets/skysgrassslabs/lang/de_au.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Erdstufe", "block.skysgrassslabs.grass_slab": "Grasblockstufe", "block.skysgrassslabs.path_slab": "Trampelpfadstufe", - "block.skysgrassslabs.turf": "Grassode" + "block.skysgrassslabs.turf": "Grassode", + "config.skysgrassslabs.generateGrassSlabs": "Grasstufen erzeugen", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Alte Stufen ersetzen", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Fügt Grasplatten an neuen Hängen der Oberwelt hinzu. Mit Terrain Smoother steuert dies nur Gras. Bestehende Chunks bleiben unverändert. Minecraft neu starten.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Kompatibilitätseinstellung für alte Welten. In normalen 1.21.1-Modpacks ausgeschaltet lassen. Alte Platten werden automatisch wiederhergestellt. Minecraft neu starten.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Grass Slabs-Inhalte ersetzen", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Wandelt unterstützte Inhalte von Grass Slabs um, solange die Mod installiert ist. Nach dem Entfernen bleibt die automatische Wiederherstellung aktiv. Minecraft neu starten." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/de_de.json b/src/main/resources/assets/skysgrassslabs/lang/de_de.json index 6f1858f..d5427b1 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/de_de.json +++ b/src/main/resources/assets/skysgrassslabs/lang/de_de.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Erdstufe", "block.skysgrassslabs.grass_slab": "Grasblockstufe", "block.skysgrassslabs.path_slab": "Trampelpfadstufe", - "block.skysgrassslabs.turf": "Grassode" + "block.skysgrassslabs.turf": "Grassode", + "config.skysgrassslabs.generateGrassSlabs": "Grasstufen erzeugen", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Alte Stufen ersetzen", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Fügt Grasplatten an neuen Hängen der Oberwelt hinzu. Mit Terrain Smoother steuert dies nur Gras. Bestehende Chunks bleiben unverändert. Minecraft neu starten.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Kompatibilitätseinstellung für alte Welten. In normalen 1.21.1-Modpacks ausgeschaltet lassen. Alte Platten werden automatisch wiederhergestellt. Minecraft neu starten.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Grass Slabs-Inhalte ersetzen", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Wandelt unterstützte Inhalte von Grass Slabs um, solange die Mod installiert ist. Nach dem Entfernen bleibt die automatische Wiederherstellung aktiv. Minecraft neu starten." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/en_ca.json b/src/main/resources/assets/skysgrassslabs/lang/en_ca.json index b747612..9dc8f48 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/en_ca.json +++ b/src/main/resources/assets/skysgrassslabs/lang/en_ca.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Dirt Slab", "block.skysgrassslabs.grass_slab": "Grass Slab", "block.skysgrassslabs.path_slab": "Grass Path Slab", - "block.skysgrassslabs.turf": "Turf" + "block.skysgrassslabs.turf": "Turf", + "config.skysgrassslabs.generateGrassSlabs": "Generate grass slabs", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Replace legacy slabs", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Add grass slabs to new Overworld slopes. With Terrain Smoother, this controls grass only. Existing chunks stay unchanged. Restart Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Legacy compatibility setting. Leave off in normal Minecraft 1.21.1 packs. Recovery of old slabs is automatic. Restart Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Replace Grass Slabs content", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Convert supported Grass Slabs content while that mod is installed. Automatic recovery after removal stays enabled. Restart Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/en_en.json b/src/main/resources/assets/skysgrassslabs/lang/en_en.json index b747612..9dc8f48 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/en_en.json +++ b/src/main/resources/assets/skysgrassslabs/lang/en_en.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Dirt Slab", "block.skysgrassslabs.grass_slab": "Grass Slab", "block.skysgrassslabs.path_slab": "Grass Path Slab", - "block.skysgrassslabs.turf": "Turf" + "block.skysgrassslabs.turf": "Turf", + "config.skysgrassslabs.generateGrassSlabs": "Generate grass slabs", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Replace legacy slabs", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Add grass slabs to new Overworld slopes. With Terrain Smoother, this controls grass only. Existing chunks stay unchanged. Restart Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Legacy compatibility setting. Leave off in normal Minecraft 1.21.1 packs. Recovery of old slabs is automatic. Restart Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Replace Grass Slabs content", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Convert supported Grass Slabs content while that mod is installed. Automatic recovery after removal stays enabled. Restart Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/en_gb.json b/src/main/resources/assets/skysgrassslabs/lang/en_gb.json index b747612..9dc8f48 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/en_gb.json +++ b/src/main/resources/assets/skysgrassslabs/lang/en_gb.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Dirt Slab", "block.skysgrassslabs.grass_slab": "Grass Slab", "block.skysgrassslabs.path_slab": "Grass Path Slab", - "block.skysgrassslabs.turf": "Turf" + "block.skysgrassslabs.turf": "Turf", + "config.skysgrassslabs.generateGrassSlabs": "Generate grass slabs", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Replace legacy slabs", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Add grass slabs to new Overworld slopes. With Terrain Smoother, this controls grass only. Existing chunks stay unchanged. Restart Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Legacy compatibility setting. Leave off in normal Minecraft 1.21.1 packs. Recovery of old slabs is automatic. Restart Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Replace Grass Slabs content", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Convert supported Grass Slabs content while that mod is installed. Automatic recovery after removal stays enabled. Restart Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/en_pt.json b/src/main/resources/assets/skysgrassslabs/lang/en_pt.json index b747612..9dc8f48 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/en_pt.json +++ b/src/main/resources/assets/skysgrassslabs/lang/en_pt.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Dirt Slab", "block.skysgrassslabs.grass_slab": "Grass Slab", "block.skysgrassslabs.path_slab": "Grass Path Slab", - "block.skysgrassslabs.turf": "Turf" + "block.skysgrassslabs.turf": "Turf", + "config.skysgrassslabs.generateGrassSlabs": "Generate grass slabs", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Replace legacy slabs", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Add grass slabs to new Overworld slopes. With Terrain Smoother, this controls grass only. Existing chunks stay unchanged. Restart Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Legacy compatibility setting. Leave off in normal Minecraft 1.21.1 packs. Recovery of old slabs is automatic. Restart Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Replace Grass Slabs content", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Convert supported Grass Slabs content while that mod is installed. Automatic recovery after removal stays enabled. Restart Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/en_us.json b/src/main/resources/assets/skysgrassslabs/lang/en_us.json index b747612..9dc8f48 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/en_us.json +++ b/src/main/resources/assets/skysgrassslabs/lang/en_us.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Dirt Slab", "block.skysgrassslabs.grass_slab": "Grass Slab", "block.skysgrassslabs.path_slab": "Grass Path Slab", - "block.skysgrassslabs.turf": "Turf" + "block.skysgrassslabs.turf": "Turf", + "config.skysgrassslabs.generateGrassSlabs": "Generate grass slabs", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Replace legacy slabs", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Add grass slabs to new Overworld slopes. With Terrain Smoother, this controls grass only. Existing chunks stay unchanged. Restart Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Legacy compatibility setting. Leave off in normal Minecraft 1.21.1 packs. Recovery of old slabs is automatic. Restart Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Replace Grass Slabs content", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Convert supported Grass Slabs content while that mod is installed. Automatic recovery after removal stays enabled. Restart Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/es_es.json b/src/main/resources/assets/skysgrassslabs/lang/es_es.json index 5958db0..fcfb9f7 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/es_es.json +++ b/src/main/resources/assets/skysgrassslabs/lang/es_es.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Losa de tierra", "block.skysgrassslabs.grass_slab": "Losa de césped", "block.skysgrassslabs.path_slab": "Losa de camino de hierba", - "block.skysgrassslabs.turf": "Tepe de césped" + "block.skysgrassslabs.turf": "Tepe de césped", + "config.skysgrassslabs.generateGrassSlabs": "Generar losas de césped", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Reemplazar losas antiguas", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Añade losas de hierba a pendientes nuevas del mundo normal. Con Terrain Smoother, solo controla la hierba. Los chunks existentes no cambian. Reinicia Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Opción de compatibilidad con mundos antiguos. Déjala desactivada en packs normales de Minecraft 1.21.1. Las losas antiguas se recuperan automáticamente. Reinicia Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Reemplazar contenido de Grass Slabs", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Convierte el contenido compatible de Grass Slabs mientras está instalado. La recuperación automática sigue activa al quitarlo. Reinicia Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/es_mx.json b/src/main/resources/assets/skysgrassslabs/lang/es_mx.json index a7c45b7..7bd714f 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/es_mx.json +++ b/src/main/resources/assets/skysgrassslabs/lang/es_mx.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Losa de tierra", "block.skysgrassslabs.grass_slab": "Losa de pasto", "block.skysgrassslabs.path_slab": "Losa de sendero de pasto", - "block.skysgrassslabs.turf": "Tapete de pasto" + "block.skysgrassslabs.turf": "Tapete de pasto", + "config.skysgrassslabs.generateGrassSlabs": "Generar losas de césped", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Reemplazar losas antiguas", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Añade losas de hierba a pendientes nuevas del mundo normal. Con Terrain Smoother, solo controla la hierba. Los chunks existentes no cambian. Reinicia Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Opción de compatibilidad con mundos antiguos. Déjala desactivada en packs normales de Minecraft 1.21.1. Las losas antiguas se recuperan automáticamente. Reinicia Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Reemplazar contenido de Grass Slabs", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Convierte el contenido compatible de Grass Slabs mientras está instalado. La recuperación automática sigue activa al quitarlo. Reinicia Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/fr_ca.json b/src/main/resources/assets/skysgrassslabs/lang/fr_ca.json index 1aa1323..23b0a6f 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/fr_ca.json +++ b/src/main/resources/assets/skysgrassslabs/lang/fr_ca.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Dalle de terre", "block.skysgrassslabs.grass_slab": "Dalle de gazon", "block.skysgrassslabs.path_slab": "Dalle de sentier de gazon", - "block.skysgrassslabs.turf": "Plaque de gazon" + "block.skysgrassslabs.turf": "Plaque de gazon", + "config.skysgrassslabs.generateGrassSlabs": "Générer des dalles d'herbe", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Remplacer les anciennes dalles", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Ajoute des dalles d'herbe aux nouvelles pentes du monde normal. Avec Terrain Smoother, cette option ne contrôle que l'herbe. Les chunks existants ne changent pas. Redémarrez Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Option de compatibilité avec les anciens mondes. Laissez-la désactivée dans les packs 1.21.1 habituels. Les anciennes dalles sont récupérées automatiquement. Redémarrez Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Remplacer le contenu de Grass Slabs", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Convertit le contenu compatible de Grass Slabs quand ce mod est installé. La récupération automatique reste active après son retrait. Redémarrez Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/fr_fr.json b/src/main/resources/assets/skysgrassslabs/lang/fr_fr.json index 7498268..2d76bbd 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/fr_fr.json +++ b/src/main/resources/assets/skysgrassslabs/lang/fr_fr.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Dalle de terre", "block.skysgrassslabs.grass_slab": "Dalle d'herbe", "block.skysgrassslabs.path_slab": "Dalle de chemin d'herbe", - "block.skysgrassslabs.turf": "Plaque de gazon" + "block.skysgrassslabs.turf": "Plaque de gazon", + "config.skysgrassslabs.generateGrassSlabs": "Générer des dalles d'herbe", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Remplacer les anciennes dalles", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Ajoute des dalles d'herbe aux nouvelles pentes du monde normal. Avec Terrain Smoother, cette option ne contrôle que l'herbe. Les chunks existants ne changent pas. Redémarrez Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Option de compatibilité avec les anciens mondes. Laissez-la désactivée dans les packs 1.21.1 habituels. Les anciennes dalles sont récupérées automatiquement. Redémarrez Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Remplacer le contenu de Grass Slabs", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Convertit le contenu compatible de Grass Slabs quand ce mod est installé. La récupération automatique reste active après son retrait. Redémarrez Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/ja_jp.json b/src/main/resources/assets/skysgrassslabs/lang/ja_jp.json index caaa616..4af58e1 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/ja_jp.json +++ b/src/main/resources/assets/skysgrassslabs/lang/ja_jp.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "土のハーフブロック", "block.skysgrassslabs.grass_slab": "草ブロックのハーフブロック", "block.skysgrassslabs.path_slab": "草の道のハーフブロック", - "block.skysgrassslabs.turf": "芝生" + "block.skysgrassslabs.turf": "芝生", + "config.skysgrassslabs.generateGrassSlabs": "草のハーフブロックを生成", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "旧MODのハーフブロックを置換", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "新しく生成されるオーバーワールドの斜面に草のハーフブロックを置きます。Terrain Smoother導入時は草だけを設定します。既存のチャンクは変わりません。Minecraftの再起動が必要です。", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "古いワールド用の互換設定です。通常のMinecraft 1.21.1環境では無効のままにしてください。古いハーフブロックは自動で復元されます。再起動が必要です。", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Grass Slabs のブロックを置換", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Grass Slabs が導入されている間、対応するブロックを変換します。削除後の自動復元は有効のままです。Minecraft の再起動が必要です。" } diff --git a/src/main/resources/assets/skysgrassslabs/lang/ko_kr.json b/src/main/resources/assets/skysgrassslabs/lang/ko_kr.json index 74ed7d9..501de7f 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/ko_kr.json +++ b/src/main/resources/assets/skysgrassslabs/lang/ko_kr.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "흙 반 블록", "block.skysgrassslabs.grass_slab": "잔디 블록 반 블록", "block.skysgrassslabs.path_slab": "잔디 길 반 블록", - "block.skysgrassslabs.turf": "잔디" + "block.skysgrassslabs.turf": "잔디", + "config.skysgrassslabs.generateGrassSlabs": "잔디 반 블록 생성", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "이전 모드 반 블록 교체", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "새로 생성되는 오버월드 경사면에 잔디 반 블록을 놓습니다. Terrain Smoother가 있으면 잔디에만 적용됩니다. 기존 청크는 바뀌지 않습니다. Minecraft를 다시 시작해야 합니다.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "이전 월드용 호환 설정입니다. 일반적인 Minecraft 1.21.1 팩에서는 꺼 두세요. 이전 반 블록은 자동으로 복구됩니다. 다시 시작해야 합니다.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Grass Slabs 콘텐츠 교체", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Grass Slabs가 설치되어 있을 때 지원되는 콘텐츠를 변환합니다. 모드를 제거한 뒤에도 자동 복구는 유지됩니다. Minecraft를 다시 시작하세요." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/pt_br.json b/src/main/resources/assets/skysgrassslabs/lang/pt_br.json index 0a95546..34da4b0 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/pt_br.json +++ b/src/main/resources/assets/skysgrassslabs/lang/pt_br.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Laje de Terra", "block.skysgrassslabs.grass_slab": "Laje de Bloco de Grama", "block.skysgrassslabs.path_slab": "Laje de Caminho de Grama", - "block.skysgrassslabs.turf": "Placa de Grama" + "block.skysgrassslabs.turf": "Placa de Grama", + "config.skysgrassslabs.generateGrassSlabs": "Gerar lajes de relva", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Substituir lajes antigas", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Adiciona lajes de grama às novas encostas do mundo normal. Com Terrain Smoother, controla apenas a grama. Os chunks existentes não mudam. Reinicie o Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Opção de compatibilidade com mundos antigos. Deixe desativada em pacotes normais do Minecraft 1.21.1. As lajes antigas são recuperadas automaticamente. Reinicie o Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Substituir conteúdo de Grass Slabs", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Converte o conteúdo compatível de Grass Slabs enquanto o mod está instalado. A recuperação automática continua ativa após a remoção. Reinicie o Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/pt_pt.json b/src/main/resources/assets/skysgrassslabs/lang/pt_pt.json index 92b4305..4f94759 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/pt_pt.json +++ b/src/main/resources/assets/skysgrassslabs/lang/pt_pt.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Degrau de Terra", "block.skysgrassslabs.grass_slab": "Degrau de Bloco de Relva", "block.skysgrassslabs.path_slab": "Degrau de Caminho de Relva", - "block.skysgrassslabs.turf": "Placa de Relva" + "block.skysgrassslabs.turf": "Placa de Relva", + "config.skysgrassslabs.generateGrassSlabs": "Gerar lajes de relva", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Substituir lajes antigas", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Acrescenta degraus de relva às novas encostas do mundo normal. Com Terrain Smoother, controla apenas a relva. Os chunks existentes não mudam. Reinicie o Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Opção de compatibilidade com mundos antigos. Deixe desativada nos packs normais do Minecraft 1.21.1. Os degraus antigos são recuperados automaticamente. Reinicie o Minecraft.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Substituir conteúdo de Grass Slabs", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Converte o conteúdo compatível de Grass Slabs enquanto o mod está instalado. A recuperação automática continua ativa após a remoção. Reinicie o Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/ru_ru.json b/src/main/resources/assets/skysgrassslabs/lang/ru_ru.json index c3a5199..54bcf39 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/ru_ru.json +++ b/src/main/resources/assets/skysgrassslabs/lang/ru_ru.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "Земляная плита", "block.skysgrassslabs.grass_slab": "Дёрновая плита", "block.skysgrassslabs.path_slab": "Плита тропы", - "block.skysgrassslabs.turf": "Дёрн" + "block.skysgrassslabs.turf": "Дёрн", + "config.skysgrassslabs.generateGrassSlabs": "Генерировать дёрновые плиты", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "Заменять старые плиты", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "Добавляет дёрновые плиты на новые склоны обычного мира. С Terrain Smoother настройка управляет только травой. Существующие чанки не меняются. Перезапустите Minecraft.", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "Совместимость со старыми мирами. В обычных сборках Minecraft 1.21.1 оставьте выключенной. Старые плиты восстанавливаются автоматически. Требуется перезапуск.", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "Заменять содержимое Grass Slabs", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "Преобразует поддерживаемое содержимое установленного Grass Slabs. После удаления мода автоматическое восстановление остаётся включённым. Перезапустите Minecraft." } diff --git a/src/main/resources/assets/skysgrassslabs/lang/zh_cn.json b/src/main/resources/assets/skysgrassslabs/lang/zh_cn.json index bf3ba40..01a4b39 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/zh_cn.json +++ b/src/main/resources/assets/skysgrassslabs/lang/zh_cn.json @@ -2,5 +2,11 @@ "block.skysgrassslabs.dirt_slab": "泥土台阶", "block.skysgrassslabs.grass_slab": "草方块台阶", "block.skysgrassslabs.path_slab": "草径台阶", - "block.skysgrassslabs.turf": "草皮" + "block.skysgrassslabs.turf": "草皮", + "config.skysgrassslabs.generateGrassSlabs": "生成草方块台阶", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs": "替换旧版台阶", + "config.skysgrassslabs.generateGrassSlabs.tooltip": "在新生成的主世界斜坡上放置草方块台阶。安装Terrain Smoother后,此选项只控制草地。已有区块不会改变。需要重启Minecraft。", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip": "旧世界兼容设置。普通Minecraft 1.21.1整合包请保持关闭。旧台阶会自动恢复。需要重启Minecraft。", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent": "替换 Grass Slabs 内容", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip": "安装 Grass Slabs 时转换受支持的内容。移除该模组后仍会自动恢复旧内容。需要重启 Minecraft。" } diff --git a/src/main/resources/data/skysgrassslabs/advancement/recipes/dirt_from_slabs.json b/src/main/resources/data/skysgrassslabs/advancement/recipes/dirt_from_slabs.json new file mode 100644 index 0000000..f6b5fec --- /dev/null +++ b/src/main/resources/data/skysgrassslabs/advancement/recipes/dirt_from_slabs.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "skysgrassslabs:dirt_from_slabs" + ] + }, + "criteria": { + "has_slab": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "items": "skysgrassslabs:dirt_slab" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipes": "skysgrassslabs:dirt_from_slabs" + } + } + }, + "requirements": [ + [ + "has_slab", + "has_the_recipe" + ] + ] +} diff --git a/src/main/resources/data/skysgrassslabs/advancement/recipes/grass_from_slabs.json b/src/main/resources/data/skysgrassslabs/advancement/recipes/grass_from_slabs.json new file mode 100644 index 0000000..8303165 --- /dev/null +++ b/src/main/resources/data/skysgrassslabs/advancement/recipes/grass_from_slabs.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "rewards": { + "recipes": [ + "skysgrassslabs:grass_from_slabs" + ] + }, + "criteria": { + "has_slab": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ + { + "items": "skysgrassslabs:grass_slab" + } + ] + } + }, + "has_the_recipe": { + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipes": "skysgrassslabs:grass_from_slabs" + } + } + }, + "requirements": [ + [ + "has_slab", + "has_the_recipe" + ] + ] +} diff --git a/src/main/resources/data/skysgrassslabs/recipe/dirt_from_slabs.json b/src/main/resources/data/skysgrassslabs/recipe/dirt_from_slabs.json new file mode 100644 index 0000000..b95367a --- /dev/null +++ b/src/main/resources/data/skysgrassslabs/recipe/dirt_from_slabs.json @@ -0,0 +1,13 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "SS" + ], + "key": { + "S": "skysgrassslabs:dirt_slab" + }, + "result": { + "id": "minecraft:dirt" + }, + "category": "building" +} diff --git a/src/main/resources/data/skysgrassslabs/recipe/grass_from_slabs.json b/src/main/resources/data/skysgrassslabs/recipe/grass_from_slabs.json new file mode 100644 index 0000000..e7dea4a --- /dev/null +++ b/src/main/resources/data/skysgrassslabs/recipe/grass_from_slabs.json @@ -0,0 +1,13 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "SS" + ], + "key": { + "S": "skysgrassslabs:grass_slab" + }, + "result": { + "id": "minecraft:grass_block" + }, + "category": "building" +} diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/LocalizationContractTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/LocalizationContractTest.java index 19f2e4e..1bd1427 100644 --- a/src/test/java/zone/moddev/mc/skysgrassslabs/LocalizationContractTest.java +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/LocalizationContractTest.java @@ -27,11 +27,19 @@ public class LocalizationContractTest { private static final Path LANG_DIR = Path.of("src/main/resources/assets/skysgrassslabs/lang"); - private static final List KEYS = Arrays.asList( + private static final List BLOCK_KEYS = Arrays.asList( "block.skysgrassslabs.dirt_slab", "block.skysgrassslabs.grass_slab", "block.skysgrassslabs.path_slab", "block.skysgrassslabs.turf"); + private static final List CONFIG_KEYS = Arrays.asList( + "config.skysgrassslabs.generateGrassSlabs", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs", + "config.skysgrassslabs.generateGrassSlabs.tooltip", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent", + "config.skysgrassslabs.forceReplaceGrassSlabsModContent.tooltip"); + private static final List KEYS = Stream.concat(BLOCK_KEYS.stream(), CONFIG_KEYS.stream()).toList(); private static final List LOCALES = Arrays.asList( "de_at", "de_au", "de_de", "en_ca", "en_en", "en_gb", "en_pt", "en_us", "es_es", "es_mx", "fr_ca", "fr_fr", "ja_jp", "ko_kr", "pt_br", "pt_pt", @@ -50,7 +58,7 @@ public void allEighteenLocalesHaveIdenticalOrderedKeys() throws Exception { for (String locale : LOCALES) { Map translations = read(locale); assertEquals(locale, KEYS, new ArrayList<>(translations.keySet())); - assertEquals(locale, 4, translations.size()); + assertEquals(locale, 10, translations.size()); assertFalse(locale, translations.values().stream().anyMatch(String::isBlank)); } } @@ -104,7 +112,8 @@ public void translationsAndRegionalDifferencesRemainIntact() throws Exception { } private static void assertValues(String locale, String... values) throws Exception { - assertEquals(locale, Arrays.asList(values), new ArrayList<>(read(locale).values())); + Map translations = read(locale); + assertEquals(locale, Arrays.asList(values), BLOCK_KEYS.stream().map(translations::get).toList()); } private static Map read(String locale) throws Exception { diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java index 8100b5a..3874369 100644 --- a/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java @@ -19,7 +19,7 @@ public void metadataUsesStableIdentityWithoutOreSpawnDependency() throws Excepti assertTrue(properties.contains("mod_id=skysgrassslabs")); assertTrue(properties.contains("minecraft_version=26.3")); - assertTrue(properties.contains("forge_version=66.0.0")); + assertTrue(properties.contains("forge_version=66.0.2")); assertTrue(properties.contains("mapping_channel=official")); assertTrue(properties.contains("mapping_version=26.3")); assertTrue(properties.contains("java_toolchain_version=25.0.3+9")); @@ -36,14 +36,14 @@ public void playerAndMaintainerDocumentsExist() { assertTrue(Files.isRegularFile(Path.of("docs/GAMEPLAY.md"))); assertTrue(Files.isRegularFile(Path.of("docs/WORLD-UPGRADES.md"))); assertFalse(Files.exists(Path.of("docs/REPOSITORY.md"))); - assertTrue(Files.isRegularFile(Path.of("docs/RELEASE-1.1.2.2603001.md"))); + assertTrue(Files.isRegularFile(Path.of("docs/RELEASE-1.2.0.2603001.md"))); assertTrue(Files.isRegularFile(Path.of("docs/BETA-0.2.0.118021.md"))); } @Test public void releaseIdentityAndLicenseAreStable() throws Exception { String properties = Files.readString(Path.of("gradle.properties"), StandardCharsets.UTF_8); - assertTrue(properties.contains("mod_version=1.1.2.2603001")); + assertTrue(properties.contains("mod_version=1.2.0.2603001")); assertTrue(properties.contains("mod_license=LGPL-2.1-only")); assertEquals("LGPL-2.1-only", Files.readString(Path.of("LICENSE.spdx"), StandardCharsets.UTF_8).trim()); assertTrue(Files.readString(Path.of("NOTICE"), StandardCharsets.UTF_8) @@ -112,7 +112,7 @@ public void commonConfigAndWorldStateUsePermanentKeys() throws Exception { assertTrue(config.contains("push(\"compat\")")); assertTrue(config.contains("define(FORCE_REPLACE_BUILDINGBRICKS_SLABS, false)")); assertTrue(config.contains("define(FORCE_REPLACE_GRASS_SLABS_MOD_CONTENT, false)")); - assertTrue(main.contains("VERSION = \"1.1.2.2603001\"")); + assertTrue(main.contains("VERSION = \"1.2.0.2603001\"")); assertTrue(state.contains("skysgrassslabs_world_state")); assertTrue(state.contains("SCHEMA_VERSION = 1")); assertTrue(state.contains("schema_version")); @@ -194,7 +194,7 @@ public void legacyUpgradeHooksAreNarrowAndAvoidUnsafe() throws Exception { assertTrue(chunkMixin.contains("upgradeChunkTag")); assertTrue(bridge.contains("new Dynamic<>(NbtOps.INSTANCE")); assertTrue(bridge.contains("skysgrassslabs_legacy_registry.dat")); - assertTrue(bridge.contains("indexLegacyChunks")); + assertTrue(bridge.contains("indexRegionChunks(worldDirectory, LEGACY_CHUNKS)")); assertTrue(bridge.contains("SUPPORTED_BLOCK_IDS")); assertFalse(bridge.contains("import sun.misc")); assertFalse(bridge.contains("Unsafe.class")); @@ -269,7 +269,7 @@ public void forge66LifecycleAndDataPackContractsArePresent() throws Exception { assertTrue(pack.contains("121")); for (String recipeName : new String[] {"dirt_slab", "grass_slab", - "grass_block_from_seeds", "grass_slab_from_seeds", "turf"}) { + "grass_block_from_seeds", "grass_slab_from_seeds", "dirt_from_slabs", "grass_from_slabs", "turf"}) { String recipe = Files.readString(Path.of( "src/main/resources/data/skysgrassslabs/recipe/" + recipeName + ".json"), StandardCharsets.UTF_8); @@ -299,9 +299,9 @@ public void continuousIntegrationTargetsTheStableBranchAndArtifacts() throws Exc } String ci = Files.readString(Path.of(".github/workflows/ci.yml"), StandardCharsets.UTF_8); - assertTrue(ci.contains("SkysGrassSlabs-1.1.2.2603001.jar")); - assertTrue(ci.contains("SkysGrassSlabs-1.1.2.2603001-sources.jar")); - assertTrue(ci.contains("SkysGrassSlabs-1.1.2.2603001-javadoc.jar")); + assertTrue(ci.contains("SkysGrassSlabs-1.2.0.2603001.jar")); + assertTrue(ci.contains("SkysGrassSlabs-1.2.0.2603001-sources.jar")); + assertTrue(ci.contains("SkysGrassSlabs-1.2.0.2603001-javadoc.jar")); assertTrue(ci.contains("if-no-files-found: error")); assertEquals(0, ci.lines() .filter(line -> line.contains("java-version: '8.0.502+7'")) diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/compat/LegacyRegistrySnapshotTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/compat/LegacyRegistrySnapshotTest.java new file mode 100644 index 0000000..09fe825 --- /dev/null +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/compat/LegacyRegistrySnapshotTest.java @@ -0,0 +1,62 @@ +package zone.moddev.mc.skysgrassslabs.compat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class LegacyRegistrySnapshotTest { + @Rule + public final TemporaryFolder folders = new TemporaryFolder(); + + @Test + public void copiesTheCompleteSnapshotWithoutReplacingCurrentForgeData() throws IOException { + File world = folders.newFolder("world"); + File emptyWorld = folders.newFolder("empty-world"); + File levelFile = new File(world, "level.dat"); + CompoundTag registries = new CompoundTag(); + registries.put("minecraft:blocks", new CompoundTag()); + registries.put("minecraft:items", new CompoundTag()); + CompoundTag legacy = new CompoundTag(); + legacy.put("Registries", registries); + legacy.putString("OtherModData", "Keep this too"); + CompoundTag source = new CompoundTag(); + source.put("FML", legacy); + try (var output = Files.newOutputStream(levelFile.toPath())) { + NbtIo.writeCompressed(source, output); + } + + try { + LegacyWorldDataHook.prepareLegacyWorld(levelFile); + CompoundTag current = new CompoundTag(); + current.putString("Marker", "Current Forge data"); + CompoundTag saved = new CompoundTag(); + saved.put("fml", current); + LegacyWorldDataHook.writeLegacyLevelData(saved); + assertEquals(legacy, saved.getCompoundOrEmpty("FML")); + assertEquals(current, saved.getCompoundOrEmpty("fml")); + + saved.remove("FML"); + try (var output = Files.newOutputStream(levelFile.toPath())) { + NbtIo.writeCompressed(saved, output); + } + LegacyWorldDataHook.prepareLegacyWorld(levelFile); + LegacyWorldDataHook.writeLegacyLevelData(saved); + assertEquals(legacy, saved.getCompoundOrEmpty("FML")); + + LegacyWorldDataHook.prepareLegacyWorld(new File(emptyWorld, "level.dat")); + CompoundTag unrelatedSave = new CompoundTag(); + LegacyWorldDataHook.writeLegacyLevelData(unrelatedSave); + assertFalse(unrelatedSave.contains("FML")); + } finally { + LegacyWorldDataHook.prepareLegacyWorld(new File(emptyWorld, "level.dat")); + } + } +} diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/compat/SavedOverworldChunksTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/compat/SavedOverworldChunksTest.java new file mode 100644 index 0000000..5acfa9a --- /dev/null +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/compat/SavedOverworldChunksTest.java @@ -0,0 +1,47 @@ +package zone.moddev.mc.skysgrassslabs.compat; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class SavedOverworldChunksTest { + @Rule + public final TemporaryFolder folders = new TemporaryFolder(); + + @Test + public void savedChunksAreProtectedWithoutALegacyRegistry() throws IOException { + Path world = folders.newFolder("world").toPath(); + Path emptyWorld = folders.newFolder("empty-world").toPath(); + writeRegionHeader(world.resolve("region/r.-1.2.mca"), 31, 0); + writeRegionHeader(world.resolve("dimensions/minecraft/overworld/region/r.1.0.mca"), 2, 3); + writeRegionHeader(world.resolve("DIM-1/region/r.0.0.mca"), 0, 0); + + try { + LegacyWorldDataHook.snapshotSavedOverworldChunks(world.toFile()); + assertTrue(LegacyWorldDataHook.isSavedOverworldChunk(-1, 64)); + assertTrue(LegacyWorldDataHook.isSavedOverworldChunk(34, 3)); + assertFalse(LegacyWorldDataHook.isSavedOverworldChunk(-2, 64)); + assertFalse(LegacyWorldDataHook.isSavedOverworldChunk(0, 0)); + + LegacyWorldDataHook.snapshotSavedOverworldChunks(emptyWorld.toFile()); + assertFalse(LegacyWorldDataHook.isSavedOverworldChunk(-1, 64)); + } finally { + LegacyWorldDataHook.snapshotSavedOverworldChunks(emptyWorld.toFile()); + } + } + + private static void writeRegionHeader(Path path, int localX, int localZ) throws IOException { + ByteBuffer header = ByteBuffer.allocate(8192); + int slot = localZ * 32 + localX; + header.putInt(slot * Integer.BYTES, (2 << 8) | 1); + Files.createDirectories(path.getParent()); + Files.write(path, header.array()); + } +} diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/config/ConfigSaveTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/config/ConfigSaveTest.java new file mode 100644 index 0000000..830258a --- /dev/null +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/config/ConfigSaveTest.java @@ -0,0 +1,86 @@ +package zone.moddev.mc.skysgrassslabs.config; + +import com.electronwill.nightconfig.core.CommentedConfig; +import com.electronwill.nightconfig.core.utils.CommentedConfigWrapper; +import com.electronwill.nightconfig.core.utils.ObservedMap; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import net.minecraftforge.common.ForgeConfigSpec; +import org.junit.After; +import org.junit.Test; +import static org.junit.Assert.*; + +public class ConfigSaveTest { + @After + public void clearRunSettings() throws Exception { + SkysGrassSlabsConfig.resetRunStateForTests(); + field("loadedValues").set(null, null); + } + + @Test + public void autosaveNeverPublishesOnlyPartOfThePlayersChoices() throws Exception { + ForgeConfigSpec spec = (ForgeConfigSpec) field("SPEC").get(null); + CommentedConfig values = CommentedConfig.inMemory(); + spec.setConfig(values); + AutosaveView watched = new AutosaveView(values); + spec.setConfig(watched); + field("loadedValues").set(null, watched); + values.set("compat.unrelated", "kept"); + values.setComment("compat.unrelated", "A pack setting"); + values.set("other.setting", 42); + SkysGrassSlabsConfig.activateSettingsForThisRun(); + watched.saves.clear(); + + SkysGrassSlabsConfig.saveForNextStart(false, true, true); + + assertFalse("The edit reaches the watched config", watched.saves.isEmpty()); + for (List saved : watched.saves) { + assertEquals("A watcher must never receive an intermediate set of choices", + List.of(false, true, true), saved); + } + assertFalse(SkysGrassSlabsConfig.configuredGrassGeneration()); + assertTrue(SkysGrassSlabsConfig.configuredLegacyReplacement()); + assertTrue(SkysGrassSlabsConfig.configuredGrassSlabsReplacement()); + assertTrue(SkysGrassSlabsConfig.generateGrassSlabs()); + assertFalse(SkysGrassSlabsConfig.forceReplaceBuildingBricksSlabs()); + assertFalse(SkysGrassSlabsConfig.forceReplaceGrassSlabsModContent()); + assertEquals("kept", values.get("compat.unrelated")); + assertEquals("A pack setting", values.getComment("compat.unrelated")); + assertEquals(42, (int) values.get("other.setting")); + } + + private static Field field(String name) throws Exception { + Field field = SkysGrassSlabsConfig.class.getDeclaredField(name); + field.setAccessible(true); + return field; + } + + /** Mirrors NightConfig's autosave callbacks without relying on watcher timing. */ + private static final class AutosaveView extends CommentedConfigWrapper { + private final List> saves = new ArrayList<>(); + + AutosaveView(CommentedConfig values) { + super(values); + } + + @Override + public T set(List path, Object value) { + T previous = super.set(path, value); + recordSave(); + return previous; + } + + @Override + public Map valueMap() { + return new ObservedMap<>(super.valueMap(), this::recordSave); + } + + private void recordSave() { + saves.add(List.of(get("worldgen.generateGrassSlabs"), + get("compat.forceReplaceBuildingBricksSlabs"), + get("compat.forceReplaceGrassSlabsModContent"))); + } + } +} diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/world/ModWorldStateTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/world/ModWorldStateTest.java new file mode 100644 index 0000000..3612e58 --- /dev/null +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/world/ModWorldStateTest.java @@ -0,0 +1,57 @@ +package zone.moddev.mc.skysgrassslabs.world; + +import static org.junit.Assert.assertEquals; + +import java.nio.file.Files; +import java.nio.file.Path; +import net.minecraft.SharedConstants; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.world.level.storage.SavedDataStorage; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ModWorldStateTest { + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void readsUnversionedModDataAndKeepsItsCountersOnReload() throws Exception { + SharedConstants.tryDetectVersion(); + + ModWorldState original = new ModWorldState(); + original.recordChunk(); + original.recordGrassBlocks(11, 0); + original.recordGrassBlocks(13, 1); + original.recordDirtBlocks(17, 0); + original.recordGrassItems(19); + original.recordDirtItems(23); + original.recordUnsupported("buildingbricks:vertical_dirt_slab", 2); + + Path dataDirectory = temporary.newFolder("world-data").toPath(); + CompoundTag savedFile = new CompoundTag(); + CompoundTag expected = original.saveTag(); + // Old Grass Slabs versions stored their own data without a vanilla DataVersion. + savedFile.put("data", expected); + Path namespaceDirectory = Files.createDirectory(dataDirectory.resolve("skysgrassslabs")); + NbtIo.writeCompressed(savedFile, namespaceDirectory.resolve(ModWorldState.DATA_NAME + ".dat")); + + try (SavedDataStorage first = open(dataDirectory)) { + ModWorldState migrated = first.computeIfAbsent(ModWorldState.TYPE); + assertEquals(expected, migrated.saveTag()); + migrated.setDirty(); + first.saveAndJoin(); + } + try (SavedDataStorage second = open(dataDirectory)) { + ModWorldState reloaded = second.computeIfAbsent(ModWorldState.TYPE); + assertEquals(expected, reloaded.saveTag()); + } + } + + private static SavedDataStorage open(Path dataDirectory) { + return new SavedDataStorage(dataDirectory, DataFixers.getDataFixer(), RegistryAccess.EMPTY); + } +} diff --git a/src/upgradeIntegrationTest/installed-grassslabs/META-INF/mods.toml b/src/upgradeIntegrationTest/installed-grassslabs/META-INF/mods.toml index 0c4c0c5..4cec88a 100644 --- a/src/upgradeIntegrationTest/installed-grassslabs/META-INF/mods.toml +++ b/src/upgradeIntegrationTest/installed-grassslabs/META-INF/mods.toml @@ -24,7 +24,7 @@ side="SERVER" [[dependencies.skysgrassslabsupgradeprobe]] modId="skysgrassslabs" mandatory=true -versionRange="[1.1.2.2603001]" +versionRange="[1.2.0.2603001]" ordering="AFTER" side="SERVER" diff --git a/src/upgradeIntegrationTest/java/zone/moddev/mc/skysgrassslabs/upgradeprobe/ClientRuntimeProbe.java b/src/upgradeIntegrationTest/java/zone/moddev/mc/skysgrassslabs/upgradeprobe/ClientRuntimeProbe.java new file mode 100644 index 0000000..a57b60b --- /dev/null +++ b/src/upgradeIntegrationTest/java/zone/moddev/mc/skysgrassslabs/upgradeprobe/ClientRuntimeProbe.java @@ -0,0 +1,129 @@ +package zone.moddev.mc.skysgrassslabs.upgradeprobe; + +import java.nio.file.Files; +import java.util.List; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.BackupConfirmScreen; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.client.ConfigScreenHandler; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.fml.ModList; +import org.lwjgl.sdl.SDLVideo; +import zone.moddev.mc.skysgrassslabs.SkysGrassSlabs; +import zone.moddev.mc.skysgrassslabs.config.SkysGrassSlabsConfig; +import zone.moddev.mc.skysgrassslabs.init.ModBlocks; + +/** Checks the real client without putting the test code in the release jar. */ +final class ClientRuntimeProbe { + private static final String WORLD = "qualification"; + private final String phase = System.getProperty("skysgrassslabs.clientProbePhase"); + private boolean windowHidden; + private boolean finished; + private boolean confirmedExperimentalWorld; + private int worldTicks; + + static void register() { + TickEvent.ClientTickEvent.Post.BUS.addListener(new ClientRuntimeProbe()::tick); + } + + private void tick(TickEvent.ClientTickEvent.Post event) { + Minecraft client = Minecraft.getInstance(); + if (!windowHidden) { + SDLVideo.SDL_HideWindow(client.getWindow().handle()); + windowHidden = true; + } + if (finished) { + return; + } + if (!Files.isRegularFile(client.gameDirectory.toPath().resolve("saves").resolve(WORLD).resolve("level.dat"))) { + throw new IllegalStateException("The disposable client fixture is missing"); + } + confirmExperimentalWorld(client.gui.screen()); + if (client.level == null || client.player == null || ++worldTicks < 20) { + return; + } + checkTint(client); + checkConfig(client); + finished = true; + SkysGrassSlabs.LOGGER.info("GRASS_SLABS_CLIENT_WORLD_PASS phase={}", phase); + client.stop(); + } + + private void confirmExperimentalWorld(Screen screen) { + if (confirmedExperimentalWorld || screen == null || !(screen.getTitle().getContents() instanceof TranslatableContents title) + || !title.getKey().equals("selectWorld.backupQuestion.experimental")) { + return; + } + Component proceed; + if (screen instanceof BackupConfirmScreen) { + proceed = Component.translatable("selectWorld.backupJoinSkipButton"); + } else if (screen instanceof ConfirmScreen) { + proceed = CommonComponents.GUI_PROCEED; + } else { + return; + } + Button button = screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast) + .filter(child -> child.getMessage().equals(proceed)).findFirst().orElseThrow(); + // Opening the world can tick the client again before this callback returns. + confirmedExperimentalWorld = true; + button.onPress(null); + } + + private static void checkTint(Minecraft client) { + var position = client.player.blockPosition(); + int grass = tint(client, position, Blocks.GRASS_BLOCK.defaultBlockState()); + int slab = tint(client, position, ModBlocks.GRASS_SLAB.get().defaultBlockState()); + int turf = tint(client, position, ModBlocks.TURF.get().defaultBlockState()); + require(grass != -1 && slab == grass && turf == grass, "Slab and turf should use the biome's grass colour"); + } + + private static int tint(Minecraft client, BlockPos position, + BlockState state) { + var source = client.getBlockColors().getTintSource(state, 0); + if (source == null) { + throw new IllegalStateException("No grass tint source for " + state); + } + return source.colorInWorld(state, client.level, position); + } + + private void checkConfig(Minecraft client) { + require("fresh".equals(phase) || "reload".equals(phase), "Unexpected client test phase"); + boolean first = phase.equals("fresh"); + require(SkysGrassSlabsConfig.generateGrassSlabs() == first, "The saved generation setting was not loaded"); + Screen parent = client.gui.screen(); + var mod = ModList.getModContainerById(SkysGrassSlabs.MOD_ID).orElseThrow().getModInfo(); + Screen config = ConfigScreenHandler.getScreenFactoryFor(mod).orElseThrow().apply(client, parent); + client.gui.setScreen(config); + List