From e9c47dc7a07ae7ce903efbfbba24171622cad21c Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 19 Oct 2026 11:00:54 +0100 Subject: [PATCH] Bring shared grass rules and legacy recovery to 1.12 --- .github/workflows/ci.yml | 6 +- CHANGELOG.md | 7 ++ README.md | 8 +- build.gradle | 13 ++ docs/CONFIGURATION.md | 4 + docs/GAMEPLAY.md | 1 + docs/INTEGRATION.md | 25 ++++ docs/RELEASE-1.1.0.112021.md | 13 ++ docs/WORLD-UPGRADES.md | 2 +- gradle.properties | 2 +- .../resources/mcmod.info | 2 +- .../integrationtest/IntegrationTestMod.java | 5 +- .../ReleasedUpgradeChecks.java | 105 ++++++++++++++++ .../mc/skysgrassslabs/SkysGrassSlabs.java | 16 ++- .../mc/skysgrassslabs/api/GrassSlabsApi.java | 107 ++++++++++++++++ .../skysgrassslabs/block/GrassSlabBlock.java | 4 +- .../mc/skysgrassslabs/block/GrassSpread.java | 25 ++-- .../compat/BuildingBricksCompat.java | 3 +- .../compat/LegacyMigrationHandler.java | 118 ++++++++++++------ .../config/SkysGrassSlabsConfig.java | 4 +- .../mc/skysgrassslabs/init/ModRecipes.java | 6 + .../skysgrassslabs/ProjectContractTest.java | 4 +- .../block/GrassFormsContractTest.java | 44 +++++++ 23 files changed, 457 insertions(+), 67 deletions(-) create mode 100644 docs/INTEGRATION.md create mode 100644 docs/RELEASE-1.1.0.112021.md create mode 100644 src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/ReleasedUpgradeChecks.java create mode 100644 src/main/java/zone/moddev/mc/skysgrassslabs/api/GrassSlabsApi.java create mode 100644 src/test/java/zone/moddev/mc/skysgrassslabs/block/GrassFormsContractTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5060c52..9f9c6fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,9 +56,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/SkysGrassSlabs-1.0.1.112021.jar - build/libs/SkysGrassSlabs-1.0.1.112021-sources.jar - build/libs/SkysGrassSlabs-1.0.1.112021-javadoc.jar + build/libs/SkysGrassSlabs-1.1.0.112021.jar + build/libs/SkysGrassSlabs-1.1.0.112021-sources.jar + build/libs/SkysGrassSlabs-1.1.0.112021-javadoc.jar build/release/SHA256SUMS CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d32396c..bc85bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.1.0.112021 - Minecraft 1.12.2 + +- Added optional shared grass behaviour with Sky's Building Pieces. +- Let Sky's Terrain Smoother own the combined generation pass when installed. +- Preserved mixed legacy and Sky saves, custom item data and migration history. +- Added recipes to combine two dirt or grass slabs into their full block. + ## 1.0.1.112021 - Minecraft 1.12.2 - Ported the complete dirt, grass, path slab and turf feature set to Minecraft diff --git a/README.md b/README.md index 2326254..a38ab02 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,12 @@ Sky's Grass Slabs adds dirt, grass and lowered path slabs, along with a 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 1.12.2. Version `1.0.1.112021` requires Forge +This branch is for Minecraft 1.12.2. Version `1.1.0.112021` requires Forge 14.23.5.2859 or a compatible Forge 14.23.5 build and Java 8. +The mod works on its own. Sky's Building Pieces and Sky's Terrain Smoother are +optional; neither is needed to update an existing Grass Slabs world. + ## Installation Place the mod jar in the `mods` folder. It must be installed on both the client @@ -30,7 +33,8 @@ or updating world changing mods. - [Gameplay and recipes](docs/GAMEPLAY.md) - [Configuration](docs/CONFIGURATION.md) - [Upgrading an older world](docs/WORLD-UPGRADES.md) -- [Release notes](docs/RELEASE-1.0.1.112021.md) +- [Optional content integration](docs/INTEGRATION.md) +- [Release notes](docs/RELEASE-1.1.0.112021.md) ## License diff --git a/build.gradle b/build.gradle index b4691e4..67ac76d 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,11 @@ ext.functional_version = versionParts[0..2].join('.') ext.display_version = project.mod_version ext.release_tag = project.mod_version +def pinnedJava8 = providers.environmentVariable('SGS_JAVA8_HOME') +def exactJava8Spec = pinnedJava8.isPresent() ? + org.gradle.jvm.toolchain.internal.SpecificInstallationToolchainSpec.fromJavaHome( + project.services.get(org.gradle.api.internal.provider.PropertyFactory), file(pinnedJava8.get())) : null + java { toolchain.languageVersion = JavaLanguageVersion.of(8) withSourcesJar() @@ -48,6 +53,7 @@ tasks.withType(JavaCompile).configureEach { targetCompatibility = JavaVersion.VERSION_1_8 options.encoding = 'UTF-8' options.compilerArgs.addAll(['-Xmaxerrs', '1000']) + if (pinnedJava8.isPresent()) javaCompiler = javaToolchains.compilerFor(exactJava8Spec) } tasks.named('compileTestJava', JavaCompile) { @@ -57,9 +63,15 @@ tasks.named('compileTestJava', JavaCompile) { tasks.withType(Test).configureEach { useJUnitPlatform() workingDir = project.projectDir + if (pinnedJava8.isPresent()) javaLauncher = javaToolchains.launcherFor(exactJava8Spec) +} + +tasks.withType(JavaExec).configureEach { + if (pinnedJava8.isPresent()) javaLauncher = javaToolchains.launcherFor(exactJava8Spec) } tasks.withType(Javadoc).configureEach { + if (pinnedJava8.isPresent()) javadocTool = javaToolchains.javadocToolFor(exactJava8Spec) failOnError = false options.encoding = 'UTF-8' options.addStringOption('Xdoclint:none', '-quiet') @@ -404,6 +416,7 @@ tasks.register('clientIntegrationTest') { def java8Launcher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(8) } +if (pinnedJava8.isPresent()) java8Launcher = javaToolchains.launcherFor(exactJava8Spec) def forgeInstallerName = "forge-${project.minecraft_version}-${project.forge_version}-installer.jar" def forgeServerName = "forge-${project.minecraft_version}-${project.forge_version}.jar" diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f6c1275..b6e1cb1 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -13,6 +13,10 @@ When enabled, suitable one block slopes in newly generated Overworld chunks may receive grass slabs. Set this to `false` to leave new terrain unchanged. The setting never adds or removes slabs in chunks that already exist. +With Sky's Terrain Smoother installed, this setting controls its grass material. +Terrain Smoother owns the only Sky terrain pass, even when its master switch is +off. Removing Terrain Smoother restores standalone grass slab generation. + ## Legacy replacement setting `compat.forceReplaceBuildingBricksSlabs=false` diff --git a/docs/GAMEPLAY.md b/docs/GAMEPLAY.md index 7814fd3..f831d4e 100644 --- a/docs/GAMEPLAY.md +++ b/docs/GAMEPLAY.md @@ -40,6 +40,7 @@ enabled is removed without dropping an item. - Three dirt blocks in a horizontal row make six dirt slabs. - Three grass blocks in a horizontal row make six grass slabs. +- Two matching dirt or grass slabs side by side make the matching full block. - A dirt block and a recognised seed make a grass block. - A dirt slab and a recognised seed make a grass slab. - A grass block or grass slab together with a compatible shovel makes one diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md new file mode 100644 index 0000000..e3583b2 --- /dev/null +++ b/docs/INTEGRATION.md @@ -0,0 +1,25 @@ +# Optional content integration + +Grass Slabs works on its own. Content and terrain mods can use version 1 of +`zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi` without changing its saved IDs. + +A terrain owner calls `claimSmoothing(modid, enabled)` before Grass Slabs loads +its configuration. Ownership suppresses the separate Grass Slabs generation +pass, even when the owner's master switch is off. `grassGenerationAllowed()` +respects the player's grass setting and the legacy-generator safety checks. + +During initialization, register each ordinary dirt/grass orientation with +`registerGrassForm`. Conflicting mappings and late registrations are rejected. +Do not register coarse dirt or podzol. `tickDirt`, `tickGrass` and `repairSupport` +share the grass lifecycle while retaining the registered shape and orientation. +Content mods should keep this integration optional. + +`registerGrassVariant` allows other grass skins to share a dirt state without +changing its default regrowth. Non-spreading skins can opt out of spreading. +`registerGrassSupport` supplies native full-block grass/dirt pairs for support +repair. `registerSpreadRule` can match grass to its source family; return null +to use the default registered pair. Callbacks must not load chunks or write to +the world. Registration closes at load completion. + +Without an integration mod, these extra mappings and rules remain empty and +the standalone generator continues to handle suitable new Overworld slopes. diff --git a/docs/RELEASE-1.1.0.112021.md b/docs/RELEASE-1.1.0.112021.md new file mode 100644 index 0000000..0b9b9c3 --- /dev/null +++ b/docs/RELEASE-1.1.0.112021.md @@ -0,0 +1,13 @@ +# Sky's Grass Slabs 1.1.0 for Minecraft 1.12.2 + +Adds optional integration with Sky's Building Pieces and Sky's Terrain Smoother. +Grass spreads between their matching dirt and grass pieces. Terrain Smoother +can own terrain generation without running a second grass slab pass. + +The mod still works on its own. Existing blocks, orientation, configuration +defaults and saved migration progress are retained. Legacy recovery also keeps +custom item data and previously recorded chunk markers in mixed worlds. +Two matching dirt or grass slabs can be crafted back into their full block. + +Requires Minecraft 1.12.2, Forge 14.23.5.2859 and Java 8. Back up existing +worlds before changing Minecraft versions or removing another mod. diff --git a/docs/WORLD-UPGRADES.md b/docs/WORLD-UPGRADES.md index 873a644..aa13c11 100644 --- a/docs/WORLD-UPGRADES.md +++ b/docs/WORLD-UPGRADES.md @@ -6,7 +6,7 @@ world has been checked in game. ## Sky's Grass Slabs content -Install `SkysGrassSlabs-1.0.1.112021.jar` before opening a world that used the +Install `SkysGrassSlabs-1.1.0.112021.jar` before opening a world that used the Minecraft 1.10.2 or 1.11.2 release. The permanent dirt slab, grass slab, path slab and turf IDs are unchanged. diff --git a/gradle.properties b/gradle.properties index 646f53f..e4a2d2c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,7 +23,7 @@ curseforge_project_id=1677588 mod_id=skysgrassslabs mod_name=Sky's Grass Slabs mod_license=LGPL-2.1-only -mod_version=1.0.1.112021 +mod_version=1.1.0.112021 mod_group=zone.moddev.mc.skysgrassslabs mod_authors=SkyBlade1978 mod_description=Adds dirt, grass and path slabs, turf, natural grass spreading and smoother Overworld terrain. diff --git a/src/clientIntegrationTest/resources/mcmod.info b/src/clientIntegrationTest/resources/mcmod.info index dc466d6..28703a8 100644 --- a/src/clientIntegrationTest/resources/mcmod.info +++ b/src/clientIntegrationTest/resources/mcmod.info @@ -1 +1 @@ -[{"modid":"skysgrassslabsclienttest","name":"Sky's Grass Slabs Client Probe","description":"Build-only rendered client fixture.","version":"1","mcversion":"1.12.2","authorList":["MMD"],"dependencies":["required-after:skysgrassslabs@[1.0.1.112021]"]}] +[{"modid":"skysgrassslabsclienttest","name":"Sky's Grass Slabs Client Probe","description":"Build-only rendered client fixture.","version":"1","mcversion":"1.12.2","authorList":["MMD"],"dependencies":["required-after:skysgrassslabs@[1.1.0.112021]"]}] diff --git a/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/IntegrationTestMod.java b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/IntegrationTestMod.java index 8d90cf7..6b9eef9 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/IntegrationTestMod.java +++ b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/IntegrationTestMod.java @@ -109,7 +109,10 @@ public void serverStarted(FMLServerStartedEvent event) { String phase = System.getProperty("skysgrassslabs.integrationPhase", "fresh"); try { Properties evidence = load(marker); - if ("upgrade-110-first".equals(phase) || + if ("released-seed".equals(phase) || "released-upgrade".equals(phase)) { + ReleasedUpgradeChecks.run(world, "released-seed".equals(phase)); + evidence.setProperty(phase.replace('-', '_') + "_complete", "true"); + } else if ("upgrade-110-first".equals(phase) || "upgrade-110-reload".equals(phase)) { verifyForwardUpgradeFixture(world, "1.10.2", "1.0.0.110021", "2030960E217C3F61AE4919C91058696B02F9FAE570BE1CD7B698696EA7BEB861"); diff --git a/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/ReleasedUpgradeChecks.java b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/ReleasedUpgradeChecks.java new file mode 100644 index 0000000..966d10f --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/ReleasedUpgradeChecks.java @@ -0,0 +1,105 @@ +package zone.moddev.mc.skysgrassslabs.integrationtest; + +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.init.Blocks; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.tileentity.TileEntityChest; +import net.minecraft.util.ClassInheritanceMultiMap; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; +import zone.moddev.mc.skysgrassslabs.init.ModBlocks; +import zone.moddev.mc.skysgrassslabs.world.ModWorldState; + +/** Seeds a released-version save and verifies it with the next version. */ +final class ReleasedUpgradeChecks { + private ReleasedUpgradeChecks() { } + + static void run(WorldServer world, boolean seed) { + world.getGameRules().setOrCreateGameRule("randomTickSpeed", "0"); + Block[] blocks = {ModBlocks.DIRT_SLAB, ModBlocks.GRASS_SLAB, + ModBlocks.PATH_SLAB, ModBlocks.TURF}; + int states = 0; + for (Block block : blocks) { + for (int metadata = 0; metadata < (block == ModBlocks.TURF ? 1 : 2); ++metadata) { + BlockPos pos = new BlockPos(160 + states++, 100, 160); + IBlockState expected = block.getStateFromMeta(metadata); + if (seed) { + world.setBlockState(pos.down(), Blocks.DIRT.getDefaultState(), 2); + world.setBlockToAir(pos.up()); + world.setBlockState(pos, expected, 2); + } else { + require(world.getBlockState(pos).equals(expected), + "Released block identity and orientation: " + block.getRegistryName() + + " / " + metadata); + } + } + } + + BlockPos chestPos = new BlockPos(160, 101, 164); + if (seed) world.setBlockState(chestPos, Blocks.CHEST.getDefaultState(), 2); + TileEntityChest chest = (TileEntityChest) world.getTileEntity(chestPos); + require(chest != null, "Released container survived"); + for (int slot = 0; slot < blocks.length; ++slot) { + ItemStack expected = new ItemStack(blocks[slot], 11 + slot, + blocks[slot] == ModBlocks.TURF ? 0 : 1); + NBTTagCompound data = new NBTTagCompound(); + data.setString("upgrade_note", "Saved custom item data"); + data.setInteger("fixture_slot", slot); + expected.setTagCompound(data); + if (seed) chest.setInventorySlotContents(slot, expected); + else require(ItemStack.areItemStacksEqual(chest.getStackInSlot(slot), expected), + "Released container item identity, count, metadata and NBT: " + slot); + } + + if (seed) { + ItemStack stack = new ItemStack(ModBlocks.GRASS_SLAB, 7, 1); + NBTTagCompound data = new NBTTagCompound(); + data.setString("upgrade_entity", "Retained"); + stack.setTagCompound(data); + require(world.spawnEntity(new EntityItem(world, 164.5, 101, 164.5, stack)), + "Released dropped item seeded"); + ModWorldState state = ModWorldState.get(world); + state.recordChunk(); + state.recordGrassBlocks(13, 0); + state.recordDirtBlocks(17, 1); + state.recordGrassItems(19); + state.recordDirtItems(23); + } else { + boolean found = false; + for (ClassInheritanceMultiMap section : + world.getChunk(chestPos).getEntityLists()) { + for (Entity entity : section) { + if (!(entity instanceof EntityItem)) continue; + ItemStack stack = ((EntityItem) entity).getItem(); + if (!stack.hasTagCompound() + || !stack.getTagCompound().hasKey("upgrade_entity")) continue; + require(stack.getItem() == Item.getItemFromBlock(ModBlocks.GRASS_SLAB) + && stack.getCount() == 7 && stack.getMetadata() == 1 + && "Retained".equals(stack.getTagCompound() + .getString("upgrade_entity")), + "Released dropped item identity, count, metadata and NBT"); + found = true; + } + } + require(found, "Released dropped item survived"); + ModWorldState state = ModWorldState.get(world); + require(ModWorldState.SCHEMA_VERSION == 1 && state.migratedChunks() == 1 + && state.migratedGrassBlocks() == 13 + && state.migratedGrassBlocksTop() == 13 + && state.migratedDirtBlocks() == 17 + && state.migratedDirtBlocksBottom() == 17 + && state.migratedGrassItems() == 19 && state.migratedDirtItems() == 23, + "Released schema-1 counters survived unchanged"); + } + System.out.println("GRASS_SLABS_RELEASED_UPGRADE_PASS seed=" + seed + " states=" + states); + } + + private static void require(boolean value, String message) { + if (!value) throw new AssertionError(message); + } +} diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java b/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java index ab0cbc9..78380a9 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java @@ -5,6 +5,7 @@ import net.minecraftforge.fml.common.SidedProxy; import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLLoadCompleteEvent; import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import org.apache.logging.log4j.Logger; @@ -14,13 +15,15 @@ import zone.moddev.mc.skysgrassslabs.event.CommonEvents; import zone.moddev.mc.skysgrassslabs.proxy.CommonProxy; import zone.moddev.mc.skysgrassslabs.world.GrassSlabSmoothingHandler; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; +import zone.moddev.mc.skysgrassslabs.init.ModBlocks; @Mod(modid = SkysGrassSlabs.MOD_ID, name = SkysGrassSlabs.NAME, version = SkysGrassSlabs.VERSION, dependencies = "before:buildingbricks") public final class SkysGrassSlabs { public static final String MOD_ID = "skysgrassslabs"; public static final String NAME = "Sky's Grass Slabs"; - public static final String VERSION = "1.0.1.112021"; + public static final String VERSION = "1.1.0.112021"; @SidedProxy( clientSide = "zone.moddev.mc.skysgrassslabs.proxy.ClientProxy", @@ -39,7 +42,7 @@ public void preInit(FMLPreInitializationEvent event) { LegacyMigrationHandler migration = new LegacyMigrationHandler(); MinecraftForge.EVENT_BUS.register(migration); FMLCommonHandler.instance().bus().register(migration); - if (SkysGrassSlabsConfig.isSmoothingActive()) { + if (!GrassSlabsApi.hasSmoothingOwner() && SkysGrassSlabsConfig.isSmoothingActive()) { MinecraftForge.EVENT_BUS.register(new GrassSlabSmoothingHandler()); } proxy.preInit(); @@ -47,6 +50,10 @@ public void preInit(FMLPreInitializationEvent event) { @Mod.EventHandler public void init(FMLInitializationEvent event) { + for (int metadata = 0; metadata < 2; ++metadata) { + GrassSlabsApi.registerGrassForm(ModBlocks.DIRT_SLAB.getStateFromMeta(metadata), + ModBlocks.GRASS_SLAB.getStateFromMeta(metadata)); + } proxy.init(); } @@ -55,6 +62,11 @@ public void postInit(FMLPostInitializationEvent event) { BuildingBricksCompat.resolveBlocks(); } + @Mod.EventHandler + public void loadComplete(FMLLoadCompleteEvent event) { + GrassSlabsApi.freeze(); + } + public SkysGrassSlabs() { } } 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..0498b17 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/api/GrassSlabsApi.java @@ -0,0 +1,107 @@ +package zone.moddev.mc.skysgrassslabs.api; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import zone.moddev.mc.skysgrassslabs.block.GrassSpread; +import zone.moddev.mc.skysgrassslabs.config.SkysGrassSlabsConfig; + +/** Optional content integration. Registration closes after mod initialization. */ +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 java.util.Set NON_SPREADING = new java.util.HashSet<>(); + private static final Map SUPPORTS = new LinkedHashMap<>(); + private static final Map> SPREAD_RULES = new LinkedHashMap<>(); + private static String smoothingOwner; + private static boolean ownerEnabled, configurationLoaded, frozen; + + private GrassSlabsApi() { } + + /** Claim during startup. The owner suppresses our pass even when its own generation is off. */ + public static void claimSmoothing(String owner, boolean enabled) { + if (configurationLoaded || owner == null || owner.isEmpty() || smoothingOwner != null) + throw new IllegalStateException("Smoothing ownership must be claimed once before configuration loads"); + smoothingOwner = owner; + ownerEnabled = enabled; + } + + public static boolean hasSmoothingOwner() { return smoothingOwner != null; } + public static boolean ownerAllowsGeneration() { return smoothingOwner == null || ownerEnabled; } + public static boolean grassGenerationAllowed() { return SkysGrassSlabsConfig.isSmoothingActive(); } + + /** Register each ordinary dirt/grass orientation, never coarse dirt or podzol. */ + public static void registerGrassForm(IBlockState dirt, IBlockState grass) { + if (frozen) throw new IllegalStateException("Grass form registration is closed"); + if (dirt == null || grass == null || dirt.getBlock() == grass.getBlock() || + dirt.getBlock().hasTileEntity(dirt) || grass.getBlock().hasTileEntity(grass)) + throw new IllegalArgumentException("Grass forms need distinct non-tile dirt and grass states"); + 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); + } + + public static boolean isGrassForm(IBlockState state) { return GRASS.containsKey(canonical(state)); } + /** Additional grass skins can share a dirt form without changing its default regrowth. */ + public static void registerGrassVariant(IBlockState dirt, IBlockState grass, boolean spreads) { + if(frozen)throw new IllegalStateException("Grass form registration is closed"); + if(dirt==null||grass==null||dirt.equals(grass)||dirt.getBlock().hasTileEntity(dirt)||grass.getBlock().hasTileEntity(grass)) + throw new IllegalArgumentException("Grass variants need distinct non-tile states"); + 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 isSpreadingGrassForm(IBlockState state) { return isGrassForm(state)&&!NON_SPREADING.contains(canonical(state)); } + public static IBlockState grassFor(IBlockState dirt) { return DIRT.get(canonical(dirt)); } + /** Choose grass to match the source; return null to use the default dirt/grass pair. */ + public static IBlockState grassFor(IBlockState dirt,IBlockState source) { + for(java.util.function.BiFunction rule:SPREAD_RULES.values()) { + IBlockState result=rule.apply(canonical(source),canonical(dirt));if(result!=null)return result; + } + return grassFor(dirt); + } + public static void registerSpreadRule(String owner,java.util.function.BiFunction rule) { + if(frozen)throw new IllegalStateException("Grass form registration is closed"); + 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); + } + public static void registerGrassSupport(IBlockState grass,IBlockState dirt) { + if(frozen)throw new IllegalStateException("Grass form registration is closed"); + if(grass==null||dirt==null||grass.equals(dirt)||grass.getBlock().hasTileEntity(grass)||dirt.getBlock().hasTileEntity(dirt)||SUPPORTS.containsKey(canonical(grass))) + throw new IllegalArgumentException("Invalid or conflicting grass support"); + SUPPORTS.put(canonical(grass),canonical(dirt)); + } + public static IBlockState dirtFor(IBlockState grass) { return GRASS.get(canonical(grass)); } + public static void configurationLoaded() { configurationLoaded = true; } + public static void freeze() { frozen = true; } + + public static void tickDirt(World world, BlockPos pos, IBlockState state, Random random) { + GrassSpread.tickDirtSlab(world, pos, state, random); + } + + public static void tickGrass(World world, BlockPos pos, IBlockState state, Random random) { + if (world.isRemote) return; + repairSupport(world, pos); + if (!GrassSpread.canRemainGrass(world, pos)) { + IBlockState dirt = dirtFor(state); + if (dirt != null) world.setBlockState(pos, dirt, 3); + } else if(isSpreadingGrassForm(state)) GrassSpread.spreadFrom(world, pos, random, pos.down()); + } + + public static void repairSupport(World world, BlockPos pos) { + if(world.isRemote||!world.isBlockLoaded(pos.down()))return; + IBlockState lower=world.getBlockState(pos.down()); + IBlockState dirt=lower.getBlock()==Blocks.GRASS?Blocks.DIRT.getDefaultState():SUPPORTS.get(canonical(lower)); + if(dirt!=null)world.setBlockState(pos.down(),dirt,2); + } + + private static IBlockState canonical(IBlockState state) { + return state.getBlock().getStateFromMeta(state.getBlock().getMetaFromState(state)); + } +} 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 769b622..b19f530 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSlabBlock.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSlabBlock.java @@ -166,8 +166,6 @@ public BlockRenderLayer getRenderLayer() { } private static void dirtifyGrassSupport(World world, BlockPos pos) { - if (!world.isRemote && world.getBlockState(pos.down()).getBlock() == Blocks.GRASS) { - world.setBlockState(pos.down(), Blocks.DIRT.getDefaultState(), 2); - } + zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi.repairSupport(world, 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 cb59143..eb6e0a3 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSpread.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSpread.java @@ -8,6 +8,7 @@ import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import zone.moddev.mc.skysgrassslabs.init.ModBlocks; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; public final class GrassSpread { private static final int SPREAD_ATTEMPTS = 4; @@ -37,7 +38,7 @@ public static void spreadFrom(World world, BlockPos source, Random random, Block if (target.getY() < 0 || target.getY() >= 256 || !world.isBlockLoaded(target)) { return; } - growTarget(world, target); + growTarget(world, target, world.getBlockState(source)); } } @@ -52,26 +53,28 @@ public static void tickDirtSlab(World world, BlockPos target, IBlockState state, return; } if (isViableSource(world, source)) { - world.setBlockState(target, ModBlocks.GRASS_SLAB.getDefaultState() - .withProperty(BlockSlab.HALF, state.getValue(BlockSlab.HALF)), 3); + IBlockState grass = GrassSlabsApi.grassFor(state,world.getBlockState(source)); + if (grass != null) world.setBlockState(target, grass, 3); return; } } } public static boolean growTarget(World world, BlockPos target) { + return growTarget(world,target,Blocks.GRASS.getDefaultState()); + } + private static boolean growTarget(World world, BlockPos target,IBlockState source) { if (!targetIsViable(world, target)) { return false; } IBlockState state = world.getBlockState(target); if (state.getBlock() == Blocks.DIRT && state.getValue(BlockDirt.VARIANT) == BlockDirt.DirtType.DIRT) { - return world.setBlockState(target, Blocks.GRASS.getDefaultState(), 3); - } - if (state.getBlock() == ModBlocks.DIRT_SLAB) { - return world.setBlockState(target, ModBlocks.GRASS_SLAB.getDefaultState() - .withProperty(BlockSlab.HALF, state.getValue(BlockSlab.HALF)), 3); + IBlockState mapped=GrassSlabsApi.grassFor(state,source); + return world.setBlockState(target,mapped==null?Blocks.GRASS.getDefaultState():mapped,3); } + IBlockState grass = GrassSlabsApi.grassFor(state,source); + if (grass != null) return world.setBlockState(target, grass, 3); return false; } @@ -80,7 +83,7 @@ public static boolean isViableSource(World world, BlockPos pos) { if (!canRemainGrass(world, pos) || !hasSpreadLight(world, pos)) { return false; } - if (state.getBlock() == Blocks.GRASS || state.getBlock() == ModBlocks.GRASS_SLAB) { + if (state.getBlock() == Blocks.GRASS || GrassSlabsApi.isSpreadingGrassForm(state)) { return true; } return state.getBlock() == ModBlocks.TURF && world.getBlockState(pos.down()).getBlock() == Blocks.DIRT; @@ -88,7 +91,7 @@ public static boolean isViableSource(World world, BlockPos pos) { private static boolean targetIsViable(World world, BlockPos target) { IBlockState state = world.getBlockState(target); - boolean dirt = state.getBlock() == ModBlocks.DIRT_SLAB || + boolean dirt = GrassSlabsApi.grassFor(state) != null || (state.getBlock() == Blocks.DIRT && state.getValue(BlockDirt.VARIANT) == BlockDirt.DirtType.DIRT); if (!dirt) { @@ -96,7 +99,7 @@ private static boolean targetIsViable(World world, BlockPos target) { } BlockPos above = target.up(); IBlockState cover = world.getBlockState(above); - if (cover.getBlock() == ModBlocks.TURF || cover.getBlock() == ModBlocks.GRASS_SLAB) { + if (cover.getBlock() == ModBlocks.TURF || GrassSlabsApi.isGrassForm(cover)) { return false; } return world.getLightFromNeighbors(above) >= 4 && diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/compat/BuildingBricksCompat.java b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/BuildingBricksCompat.java index 90b3ff4..0124882 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/compat/BuildingBricksCompat.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/BuildingBricksCompat.java @@ -41,7 +41,7 @@ public static void preInit(File configDirectory) { SkysGrassSlabsConfig.forceReplaceBuildingBricksSlabs() ? "enabled" : "disabled"); if (!BuildingBricksPolicy.shouldArbitrateWorldgen(installed, - SkysGrassSlabsConfig.generateGrassSlabs())) return; + SkysGrassSlabsConfig.isSmoothingActive())) return; File configFile = new File(configDirectory, "BuildingBricks/general.cfg"); try { @@ -158,6 +158,7 @@ public int getMetadata(int damage) { return damage & 1; } }; + item.setHasSubtypes(true); return item.setRegistryName(block.getRegistryName()); } diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyMigrationHandler.java b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyMigrationHandler.java index c62593d..2c4ed77 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyMigrationHandler.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyMigrationHandler.java @@ -25,6 +25,10 @@ import net.minecraft.tileentity.TileEntity; import net.minecraft.util.ClassInheritanceMultiMap; import net.minecraft.util.ResourceLocation; +import net.minecraft.util.EnumFacing; +import net.minecraftforge.items.CapabilityItemHandler; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.IItemHandlerModifiable; import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.NibbleArray; @@ -32,6 +36,7 @@ import net.minecraftforge.event.world.BlockEvent; import net.minecraftforge.event.world.ChunkDataEvent; import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.event.entity.EntityJoinWorldEvent; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.PlayerEvent; @@ -43,8 +48,9 @@ public final class LegacyMigrationHandler { private static final String CHUNK_MARKER = "skysgrassslabs"; private static final String CHUNK_MIGRATION_VERSION = "buildingbricks_migration_version"; - private final Map chunkMarkersToSave = - new HashMap(); + private final Map chunkMarkersToSave = + new HashMap(); + private final java.util.Queue pendingHandlers = new java.util.concurrent.ConcurrentLinkedQueue(); @SubscribeEvent public void loadChunk(ChunkDataEvent.Load event) { @@ -54,14 +60,17 @@ public void loadChunk(ChunkDataEvent.Load event) { int markerVersion = marker.getInteger(CHUNK_MIGRATION_VERSION); Chunk chunk = event.getChunk(); ChunkKey chunkKey = ChunkKey.of(world, chunk); + if (shouldPreserveChunkMarker(markerVersion)) { + chunkMarkersToSave.put(chunkKey, marker.copy()); + } if (BuildingBricksCompat.hasLegacyAliases()) { boolean changed = migrateBlocks(chunk, event.getData(), null); changed |= migrateChunkInventories(chunk, null); + pendingHandlers.add(chunk); if (changed) chunk.setModified(true); return; } if (shouldPreserveChunkMarker(markerVersion)) { - chunkMarkersToSave.put(chunkKey, markerVersion); return; } if (!shouldMigrateChunk(BuildingBricksCompat.shouldReplaceSlabs(), markerVersion)) return; @@ -74,52 +83,60 @@ public void loadChunk(ChunkDataEvent.Load event) { ModWorldState state = ModWorldState.get(world); migrateBlocks(chunk, event.getData(), state); migrateChunkInventories(chunk, state); + pendingHandlers.add(chunk); state.recordChunk(); chunk.setModified(true); - chunkMarkersToSave.put(chunkKey, ModWorldState.MIGRATION_VERSION); + NBTTagCompound migratedMarker=marker.copy(); + migratedMarker.setInteger(CHUNK_MIGRATION_VERSION,ModWorldState.MIGRATION_VERSION); + chunkMarkersToSave.put(chunkKey,migratedMarker); } @SubscribeEvent public void saveChunk(ChunkDataEvent.Save event) { - Integer markerVersion = chunkMarkersToSave.get( + NBTTagCompound savedMarker = chunkMarkersToSave.get( ChunkKey.of(event.getWorld(), event.getChunk())); - if (markerVersion == null) return; - NBTTagCompound marker = event.getData().hasKey(CHUNK_MARKER, 10) - ? event.getData().getCompoundTag(CHUNK_MARKER) : new NBTTagCompound(); - marker.setInteger(CHUNK_MIGRATION_VERSION, markerVersion.intValue()); + if (savedMarker == null) return; + NBTTagCompound marker = savedMarker.copy(); + if(event.getData().hasKey(CHUNK_MARKER,10))marker.merge(event.getData().getCompoundTag(CHUNK_MARKER)); event.getData().setTag(CHUNK_MARKER, marker); } @SubscribeEvent public void convertPlacedBlock(BlockEvent.PlaceEvent event) { World world = event.getWorld(); - if (world.isRemote || (!BuildingBricksCompat.shouldReplaceSlabs() && - !BuildingBricksCompat.hasLegacyAliases())) { + if (world.isRemote || (!BuildingBricksCompat.shouldReplaceSlabs() && !BuildingBricksCompat.hasLegacyAliases())) { return; } IBlockState placed = event.getPlacedBlock(); IBlockState replacement = replacementFor(placed); if (replacement != null && world.setBlockState(event.getPos(), replacement, 3)) { - ModWorldState state = ModWorldState.get(world); + ModWorldState state = BuildingBricksCompat.hasLegacyAliases() ? null : ModWorldState.get(world); int metadata = placed.getBlock().getMetaFromState(placed) & 1; - if (placed.getBlock() == BuildingBricksCompat.grassSlab()) { + if (state != null && placed.getBlock() == BuildingBricksCompat.grassSlab()) { state.recordGrassBlocks(1, metadata); } - if (placed.getBlock() == BuildingBricksCompat.dirtSlab()) { + if (state != null && placed.getBlock() == BuildingBricksCompat.dirtSlab()) { state.recordDirtBlocks(1, metadata); } } } + @SubscribeEvent + public void joinEntity(EntityJoinWorldEvent event) { + if (event.getWorld().isRemote || (!BuildingBricksCompat.hasLegacyAliases() && + !BuildingBricksCompat.shouldReplaceSlabs()) || event.getEntity() instanceof EntityPlayer) return; + NBTTagCompound serialized = event.getEntity().serializeNBT(); + ModWorldState state = BuildingBricksCompat.hasLegacyAliases() ? null : ModWorldState.get(event.getWorld()); + if (migrateStacksInNbt(serialized, state)) event.getEntity().deserializeNBT(serialized); + } + @SubscribeEvent public void playerLogin(PlayerEvent.PlayerLoggedInEvent event) { - if (!(event.player instanceof EntityPlayer) || (!BuildingBricksCompat.shouldReplaceSlabs() && - !BuildingBricksCompat.hasLegacyAliases())) { + if (!(event.player instanceof EntityPlayer) || (!BuildingBricksCompat.shouldReplaceSlabs() && !BuildingBricksCompat.hasLegacyAliases())) { return; } EntityPlayer player = event.player; - ModWorldState state = BuildingBricksCompat.hasLegacyAliases() ? null - : ModWorldState.get(player.getEntityWorld()); + ModWorldState state = BuildingBricksCompat.hasLegacyAliases() ? null : ModWorldState.get(player.getEntityWorld()); migrateInventory(player.inventory, state); migrateInventory(player.getInventoryEnderChest(), state); } @@ -135,22 +152,22 @@ public void saveWorld(WorldEvent.Save event) { } public static void remapMissingBlocks(RegistryEvent.MissingMappings event) { - if (BuildingBricksCompat.hasLegacyAliases()) return; + // Separate historical holders keep the saved numeric slots distinct. + // Do not remap several old IDs onto an already registered Sky block. + if (!BuildingBricksCompat.hasLegacyAliases()) return; for (RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { - LegacySlabKind kind = legacySlabKind(mapping.key); - if (kind == null) continue; - mapping.remap(kind == LegacySlabKind.GRASS - ? ModBlocks.GRASS_SLAB : ModBlocks.DIRT_SLAB); + Block holder = net.minecraftforge.fml.common.registry.ForgeRegistries.BLOCKS.getValue(mapping.key); + if (legacySlabKind(mapping.key) != null && holder instanceof LegacySlabAliasBlock) + mapping.remap(holder); } } public static void remapMissingItems(RegistryEvent.MissingMappings event) { - if (BuildingBricksCompat.hasLegacyAliases()) return; + if (!BuildingBricksCompat.hasLegacyAliases()) return; for (RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { - LegacySlabKind kind = legacySlabKind(mapping.key); - if (kind == null) continue; - mapping.remap(Item.getItemFromBlock(kind == LegacySlabKind.GRASS - ? ModBlocks.GRASS_SLAB : ModBlocks.DIRT_SLAB)); + Block holder = net.minecraftforge.fml.common.registry.ForgeRegistries.BLOCKS.getValue(mapping.key); + if (legacySlabKind(mapping.key) != null && holder instanceof LegacySlabAliasBlock) + mapping.remap(Item.getItemFromBlock(holder)); } } @@ -202,8 +219,7 @@ private static boolean migrateBlocks(Chunk chunk, NBTTagCompound chunkData, if (numericId == grassId || numericId == historicalGrassId || numericId == dirtId) { int slabMetadata = metadata.getFromIndex(index) & 1; section.set(index & 15, index >> 8 & 15, index >> 4 & 15, - skyStateFor(numericId == grassId || numericId == historicalGrassId, - slabMetadata)); + skyStateFor(numericId == grassId || numericId == historicalGrassId, slabMetadata)); if (numericId == grassId || numericId == historicalGrassId) { if (slabMetadata == 0) ++grassTop; else ++grassBottom; @@ -240,8 +256,7 @@ private static IBlockState replacementFor(IBlockState oldState) { return null; } int metadata = oldBlock.getMetaFromState(oldState) & 1; - return skyStateFor(oldBlock == BuildingBricksCompat.grassSlab() || - oldBlock == BuildingBricksCompat.historicalGrassSlab(), metadata); + return skyStateFor(oldBlock == BuildingBricksCompat.grassSlab() || oldBlock == BuildingBricksCompat.historicalGrassSlab(), metadata); } static IBlockState skyStateFor(boolean grass, int metadata) { @@ -270,6 +285,31 @@ private static boolean migrateChunkInventories(Chunk chunk, ModWorldState state) return changed; } + @SubscribeEvent + public void migrateLoadedHandlers(net.minecraftforge.fml.common.gameevent.TickEvent.ServerTickEvent event) { + if(event.phase!=net.minecraftforge.fml.common.gameevent.TickEvent.Phase.END)return; + for(int count=0;count<8;count++) { + Chunk chunk=pendingHandlers.poll();if(chunk==null)break; + World world=chunk.getWorld(); + if(!world.isBlockLoaded(new net.minecraft.util.math.BlockPos(chunk.x<<4,0,chunk.z<<4),false))continue; + ModWorldState state=BuildingBricksCompat.hasLegacyAliases()?null:ModWorldState.get(world); + for(TileEntity tile:chunk.getTileEntityMap().values()) { + // A loot inventory accessor generates loot and can re-enter loading. + if(tile.writeToNBT(new NBTTagCompound()).hasKey("LootTable",8))continue; + java.util.Set handled=Collections.newSetFromMap(new java.util.IdentityHashMap()); + for(EnumFacing face:new EnumFacing[]{null,EnumFacing.DOWN,EnumFacing.UP,EnumFacing.NORTH,EnumFacing.SOUTH,EnumFacing.WEST,EnumFacing.EAST}) { + if(!tile.hasCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY,face))continue; + IItemHandler handler=tile.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY,face); + if(!(handler instanceof IItemHandlerModifiable)||!handled.add(handler))continue; + for(int slot=0;slot registry) { "DDD", 'D', Blocks.DIRT), "dirt_slab"), named(new ShapedOreRecipe(null, new ItemStack(ModBlocks.GRASS_SLAB, 6), "GGG", 'G', Blocks.GRASS), "grass_slab"), + named(new ShapedOreRecipe(null, new ItemStack(Blocks.DIRT), "SS", 'S', + new ItemStack(ModBlocks.DIRT_SLAB, 1, OreDictionary.WILDCARD_VALUE)), + "dirt_from_slabs"), + named(new ShapedOreRecipe(null, new ItemStack(Blocks.GRASS), "SS", 'S', + new ItemStack(ModBlocks.GRASS_SLAB, 1, OreDictionary.WILDCARD_VALUE)), + "grass_from_slabs"), named(new ShapelessOreRecipe(null, new ItemStack(Blocks.GRASS), new ItemStack(Blocks.DIRT), SEED_ORE), "grass_block_from_seeds"), named(new ShapelessOreRecipe(null, new ItemStack(ModBlocks.GRASS_SLAB), diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java index d2ec356..1907fab 100644 --- a/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java @@ -20,7 +20,7 @@ void releaseIdentityAndToolchainArePinned() throws Exception { assertTrue(properties.contains("minecraft_version=1.12.2")); assertTrue(properties.contains("forge_version=14.23.5.2859")); assertTrue(properties.contains("mapping_version=39-1.12")); - assertTrue(properties.contains("mod_version=1.0.1.112021")); + assertTrue(properties.contains("mod_version=1.1.0.112021")); assertTrue(properties.contains("curseforge_project_id=1677588")); assertTrue(properties.contains("java_toolchain_version=8.0.502+7")); } @@ -121,7 +121,7 @@ void releaseTagWorkflowPointsAtTheManualDispatcherInputs() throws Exception { assertFalse(workflow.contains("release_ref:")); assertFalse(workflow.contains("curseforge_channel:")); assertFalse(workflow.contains("confirm_version:")); - assertTrue(new File("docs/RELEASE-1.0.1.112021.md").isFile()); + assertTrue(new File("docs/RELEASE-1.1.0.112021.md").isFile()); } @Test diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/block/GrassFormsContractTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/block/GrassFormsContractTest.java new file mode 100644 index 0000000..e26a6e6 --- /dev/null +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/block/GrassFormsContractTest.java @@ -0,0 +1,44 @@ +package zone.moddev.mc.skysgrassslabs.block; + +import net.minecraft.block.BlockGrass; +import net.minecraft.block.BlockSlab; +import net.minecraft.block.state.IBlockState; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import zone.moddev.mc.skysgrassslabs.MinecraftTestBootstrap; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; +import zone.moddev.mc.skysgrassslabs.init.ModBlocks; +import static org.junit.jupiter.api.Assertions.*; + +class GrassFormsContractTest { + @BeforeAll static void register() { + MinecraftTestBootstrap.registerVanilla(); + for (int metadata = 0; metadata < 2; metadata++) { + GrassSlabsApi.registerGrassForm(ModBlocks.DIRT_SLAB.getStateFromMeta(metadata), + ModBlocks.GRASS_SLAB.getStateFromMeta(metadata)); + } + } + + @Test void bothOrientationsRoundTripAndSnowDoesNotChangeIdentity() { + for (int metadata = 0; metadata < 2; metadata++) { + IBlockState dirt = ModBlocks.DIRT_SLAB.getStateFromMeta(metadata); + IBlockState grass = ModBlocks.GRASS_SLAB.getStateFromMeta(metadata); + assertEquals(grass, GrassSlabsApi.grassFor(dirt)); + assertEquals(dirt, GrassSlabsApi.dirtFor(grass)); + assertEquals(dirt, GrassSlabsApi.dirtFor(grass.withProperty(BlockGrass.SNOWY, true))); + assertEquals(grass.getValue(BlockSlab.HALF), + GrassSlabsApi.grassFor(dirt).getValue(BlockSlab.HALF)); + assertTrue(GrassSlabsApi.isGrassForm(grass)); + assertFalse(GrassSlabsApi.isGrassForm(dirt)); + } + } + + @Test void duplicateOrInvalidMappingsAreRejected() { + assertThrows(IllegalArgumentException.class, () -> GrassSlabsApi.registerGrassForm( + ModBlocks.DIRT_SLAB.getStateFromMeta(0), ModBlocks.GRASS_SLAB.getStateFromMeta(0))); + assertThrows(IllegalArgumentException.class, () -> GrassSlabsApi.registerGrassForm( + null, ModBlocks.GRASS_SLAB.getDefaultState())); + assertThrows(IllegalArgumentException.class, () -> GrassSlabsApi.registerGrassForm( + ModBlocks.DIRT_SLAB.getDefaultState(), ModBlocks.DIRT_SLAB.getDefaultState())); + } +}