From 2f7e74a635297a9b909e01525fd62a70a1f683cb Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Fri, 6 Nov 2026 17:10:36 +0000 Subject: [PATCH 1/2] Bring the shared grass rules to Forge 26.3 --- .github/workflows/ci.yml | 6 +- CHANGELOG.md | 7 + README.md | 12 +- build.gradle | 18 +- ci-fixtures/tools/README.md | 8 +- ...craft-mavenizer-0.5.21-orespawn-compat.jar | Bin 2566958 -> 2566966 bytes ...aft-mavenizer-0.5.21-orespawn-compat.patch | 3 +- .../tools/minecraft-source-compatibility.json | 2 +- docs/CONFIGURATION.md | 5 + docs/GAMEPLAY.md | 1 + ....0.2601021.md => RELEASE-1.1.1.2601021.md} | 3 +- docs/RELEASE-1.1.2.2601021.md | 11 + docs/RELEASE-1.2.0.2601021.md | 10 + docs/RELEASE-1.2.0.2603001.md | 12 + docs/WORLD-UPGRADES.md | 2 +- gradle.properties | 6 +- .../gametest/SlabGameTests.java | 47 ++++ .../gametest/WorldgenGameTests.java | 4 +- ...orms_keep_their_orientation_and_water.json | 7 + ...atching_slabs_craft_back_to_one_block.json | 7 + .../mc/skysgrassslabs/SkysGrassSlabs.java | 34 ++- .../mc/skysgrassslabs/api/GrassSlabsApi.java | 234 ++++++++++++++++++ .../skysgrassslabs/block/GrassSlabBlock.java | 19 +- .../mc/skysgrassslabs/block/GrassSpread.java | 72 +++--- .../skysgrassslabs/block/SoilLifecycle.java | 2 + .../skysgrassslabs/client/ClientConfig.java | 15 ++ .../client/GrassSlabsConfigScreen.java | 71 ++++++ .../compat/LegacyWorldDataHook.java | 71 ++++-- .../config/SkysGrassSlabsConfig.java | 67 ++++- .../skysgrassslabs/mixin/ForgeHooksMixin.java | 8 + .../mixin/SimpleRegionStorageMixin.java | 7 +- .../world/GrassSlabSmoothingFeature.java | 60 ++++- .../skysgrassslabs/world/ModWorldState.java | 9 +- .../world/SmoothingBiomeModifier.java | 3 +- .../world/SmoothingFoliage.java | 55 ++++ .../world/StructureProtection.java | 65 +++++ .../assets/skysgrassslabs/lang/de_at.json | 8 +- .../assets/skysgrassslabs/lang/de_au.json | 8 +- .../assets/skysgrassslabs/lang/de_de.json | 8 +- .../assets/skysgrassslabs/lang/en_ca.json | 8 +- .../assets/skysgrassslabs/lang/en_en.json | 8 +- .../assets/skysgrassslabs/lang/en_gb.json | 8 +- .../assets/skysgrassslabs/lang/en_pt.json | 8 +- .../assets/skysgrassslabs/lang/en_us.json | 8 +- .../assets/skysgrassslabs/lang/es_es.json | 8 +- .../assets/skysgrassslabs/lang/es_mx.json | 8 +- .../assets/skysgrassslabs/lang/fr_ca.json | 8 +- .../assets/skysgrassslabs/lang/fr_fr.json | 8 +- .../assets/skysgrassslabs/lang/ja_jp.json | 8 +- .../assets/skysgrassslabs/lang/ko_kr.json | 8 +- .../assets/skysgrassslabs/lang/pt_br.json | 8 +- .../assets/skysgrassslabs/lang/pt_pt.json | 8 +- .../assets/skysgrassslabs/lang/ru_ru.json | 8 +- .../assets/skysgrassslabs/lang/zh_cn.json | 8 +- .../advancement/recipes/dirt_from_slabs.json | 32 +++ .../advancement/recipes/grass_from_slabs.json | 32 +++ .../recipe/dirt_from_slabs.json | 13 + .../recipe/grass_from_slabs.json | 13 + .../LocalizationContractTest.java | 15 +- .../skysgrassslabs/ProjectContractTest.java | 18 +- .../compat/LegacyRegistrySnapshotTest.java | 62 +++++ .../compat/SavedOverworldChunksTest.java | 47 ++++ .../skysgrassslabs/config/ConfigSaveTest.java | 86 +++++++ .../world/ModWorldStateTest.java | 57 +++++ .../installed-grassslabs/META-INF/mods.toml | 2 +- .../upgradeprobe/ClientRuntimeProbe.java | 129 ++++++++++ .../upgradeprobe/UpgradeProbeMod.java | 8 + .../resources/META-INF/mods.toml | 2 +- 68 files changed, 1487 insertions(+), 136 deletions(-) rename docs/{RELEASE-1.1.0.2601021.md => RELEASE-1.1.1.2601021.md} (91%) create mode 100644 docs/RELEASE-1.1.2.2601021.md create mode 100644 docs/RELEASE-1.2.0.2601021.md create mode 100644 docs/RELEASE-1.2.0.2603001.md create mode 100644 src/gameTest/resources/data/skysgrassslabs/test_instance/slab/shared_grass_forms_keep_their_orientation_and_water.json create mode 100644 src/gameTest/resources/data/skysgrassslabs/test_instance/slab/two_matching_slabs_craft_back_to_one_block.json create mode 100644 src/main/java/zone/moddev/mc/skysgrassslabs/api/GrassSlabsApi.java create mode 100644 src/main/java/zone/moddev/mc/skysgrassslabs/client/ClientConfig.java create mode 100644 src/main/java/zone/moddev/mc/skysgrassslabs/client/GrassSlabsConfigScreen.java create mode 100644 src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingFoliage.java create mode 100644 src/main/java/zone/moddev/mc/skysgrassslabs/world/StructureProtection.java create mode 100644 src/main/resources/data/skysgrassslabs/advancement/recipes/dirt_from_slabs.json create mode 100644 src/main/resources/data/skysgrassslabs/advancement/recipes/grass_from_slabs.json create mode 100644 src/main/resources/data/skysgrassslabs/recipe/dirt_from_slabs.json create mode 100644 src/main/resources/data/skysgrassslabs/recipe/grass_from_slabs.json create mode 100644 src/test/java/zone/moddev/mc/skysgrassslabs/compat/LegacyRegistrySnapshotTest.java create mode 100644 src/test/java/zone/moddev/mc/skysgrassslabs/compat/SavedOverworldChunksTest.java create mode 100644 src/test/java/zone/moddev/mc/skysgrassslabs/config/ConfigSaveTest.java create mode 100644 src/test/java/zone/moddev/mc/skysgrassslabs/world/ModWorldStateTest.java create mode 100644 src/upgradeIntegrationTest/java/zone/moddev/mc/skysgrassslabs/upgradeprobe/ClientRuntimeProbe.java 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 86aeb5ef2f658691a67b20ef6cfac3aeb787a8d2..d74dabc750f778b0faa4b8a0c84c1d19943573f5 100644 GIT binary patch delta 103425 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⪙Cz$fq?qu5D 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�~ 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 delta 103512 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 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