diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d861a3b..dba2530 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,9 @@ jobs: distribution: microsoft java-version: '17' + - name: Pin production Java + run: echo "SGS_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 @@ -46,7 +49,7 @@ jobs: - name: Build, test, and audit release artifacts run: >- - ./gradlew clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums + ./gradlew clean check integrationTest build javadoc verifyReleaseArtifacts writeReleaseChecksums verifyEclipseProductionClasspath --no-daemon --stacktrace - name: Upload audited release candidate @@ -56,9 +59,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/SkysGrassSlabs-1.0.0.110021.jar - build/libs/SkysGrassSlabs-1.0.0.110021-sources.jar - build/libs/SkysGrassSlabs-1.0.0.110021-javadoc.jar + build/libs/SkysGrassSlabs-1.1.0.110021.jar + build/libs/SkysGrassSlabs-1.1.0.110021-sources.jar + build/libs/SkysGrassSlabs-1.1.0.110021-javadoc.jar build/release/SHA256SUMS CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c401b8..9dfb00b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.1.0.110021 - Minecraft 1.10.2 + +- Added optional shared grass rules for Building Pieces and safe generation + ownership for Terrain Smoother. Standalone grass smoothing is retained. +- Fixed legacy slab recovery in worlds containing both old and Sky slabs. +- Preserved slab orientation, saved item data and earlier migration history. +- Added slab recombination recipes and optional native soil rules for terrain + add-ons. + ## 1.0.1.110021 - Minecraft 1.10.2 - Corrected the side texture alignment on top and bottom path slabs so it diff --git a/README.md b/README.md index fd97c1b..0bc32ac 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 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.10.2. Version `1.0.1.110021` requires Forge +This branch is for Minecraft 1.10.2. Version `1.1.0.110021` requires Forge 12.18.3.2511 or a compatible Forge 12.18.3 build and Java 8. ## Installation @@ -30,7 +30,7 @@ or updating world changing mods. - [Gameplay and recipes](docs/GAMEPLAY.md) - [Configuration](docs/CONFIGURATION.md) - [Existing worlds and legacy slabs](docs/WORLD-UPGRADES.md) -- [Release notes](docs/RELEASE-1.0.1.110021.md) +- [Release notes](docs/RELEASE-1.1.0.110021.md) ## License diff --git a/build.gradle b/build.gradle index 1fc0eba..5c04908 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') @@ -160,6 +172,7 @@ def configureFromForgeRun = { JavaExec process, String runTaskName -> run.setClasspath(files(forge10DevelopmentOutput, run.classpath.filter { !splitMainOutputs.contains(it) })) + if (pinnedJava8.isPresent()) run.executable = javaToolchains.launcherFor(exactJava8Spec).get().executablePath.asFile.path run.exec() } finally { runOptions.workingDir.set(originalConfiguredWorkingDir) @@ -275,6 +288,7 @@ def createIntegrationProcess = { String phase, Object dependency -> dependsOn dependency workingDir integrationRunDirectory systemProperty 'skysgrassslabs.integrationPhase', phase.toLowerCase(Locale.ROOT) + systemProperty 'log4j.configurationFile', file('src/integrationTest/resources/log4j2.xml').toURI().toString() configureFromForgeRun(delegate, 'runServer') doLast { File marker = new File(integrationRunDirectory.get().asFile, diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 0754b3b..13c4886 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -13,6 +13,11 @@ 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 and has its own master switch. +Removing Terrain Smoother restores the standalone grass slab generator; no +configuration file needs to be changed. + When the supported legacy slab mod is also installed, enabling Sky's world smoothing turns off its overlapping grass slab generator. A backup of the legacy configuration is made before it is changed. This affects generation diff --git a/docs/GAMEPLAY.md b/docs/GAMEPLAY.md index 8bd1768..dabee25 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..aa5b619 --- /dev/null +++ b/docs/INTEGRATION.md @@ -0,0 +1,27 @@ +# Optional content integration + +Grass Slabs remains standalone. Terrain Smoother 0.1 uses Grass Slabs 1.1's +version 1 API in `zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi`. + +A terrain owner calls `claimSmoothing(modid, enabled)` before Grass Slabs +pre-initialization. Installed ownership suppresses the separate grass pass, +including when the owner is disabled. `grassGenerationAllowed()` incorporates +the player's grass setting and the legacy-generator arbitration result. + +Content mods register ordinary dirt/grass state pairs with `registerGrassForm` +during initialization, one pair for each orientation. Registration rejects +conflicts and closes at load completion. Never register coarse dirt or podzol. +`tickGrass`, `tickDirt` and `repairSupport` retain matching state orientation, +grass lighting rules and covered-target rejection. Integration must be optional +when the other content mod is not installed. + +`registerGrassVariant(dirt, grass, spreads)` supports additional grass skins +sharing a dirt state without replacing its default regrowth. Set `spreads` to +false for native surfaces that do not spread. `registerGrassSupport` supplies +native full grass/dirt pairs for support repair. + +An add-on can register a source-aware `registerSpreadRule(modid, rule)` to retain +its native grass family. The callback receives the source grass and target dirt +states and returns a registered matching grass state, or null to use the default +pair. It must not load chunks or write to the world. All registration closes at +load completion. Without an add-on these rules leave vanilla behaviour unchanged. diff --git a/docs/RELEASE-1.0.2.110021.md b/docs/RELEASE-1.0.2.110021.md new file mode 100644 index 0000000..b8b6e98 --- /dev/null +++ b/docs/RELEASE-1.0.2.110021.md @@ -0,0 +1,11 @@ +# Sky's Grass Slabs 1.0.2 for Minecraft 1.10.2 + +Improved recovery of legacy grass and dirt slabs in worlds that already contain +Sky slabs. Both slab orientations and saved item data are preserved, including +content in chunks visited by an earlier migration. + +Gameplay, recipes, terrain generation and configuration defaults are unchanged. +Back up your world before removing any mod. Other legacy building pieces need +their own supported replacements. + +Requires Minecraft 1.10.2, Forge 12.18.3.2511 and Java 8. diff --git a/docs/RELEASE-1.1.0.110021.md b/docs/RELEASE-1.1.0.110021.md new file mode 100644 index 0000000..79c9800 --- /dev/null +++ b/docs/RELEASE-1.1.0.110021.md @@ -0,0 +1,11 @@ +# Sky's Grass Slabs 1.1.0 for Minecraft 1.10.2 + +Adds optional integration with Sky's Building Pieces and Sky's Terrain Smoother. +Grass can spread between their matching dirt and grass pieces, and the smoother +can manage terrain generation without running a second grass slab pass. + +The mod still works on its own. Existing blocks, recipes, configuration defaults +and saved migration progress are retained. This release also includes improved +recovery of legacy slabs in mixed worlds. Back up existing worlds before updating. + +Requires Minecraft 1.10.2, Forge 12.18.3.2511 and Java 8. diff --git a/docs/WORLD-UPGRADES.md b/docs/WORLD-UPGRADES.md index 0f406e4..5a58f94 100644 --- a/docs/WORLD-UPGRADES.md +++ b/docs/WORLD-UPGRADES.md @@ -49,6 +49,12 @@ historical grass slab IDs are recovered automatically as Sky slabs. Forge may still warn about unsupported BuildingBricks content and may remove that content after offering its normal backup and missing block confirmation. +Recovery also works in mixed worlds containing both legacy and permanent Sky +IDs, including chunks already marked by a previous migration. Hidden recovery +holders keep those identities separate while loading; they are not creative +items or normal recipe ingredients. Existing migration counters and reports +remain historical and are not reset during absent-mod recovery. + Do not continue past that warning unless the listed unsupported blocks are no longer needed. Keeping BuildingBricks installed is the safest choice for a world that still uses its other shapes. diff --git a/gradle.properties b/gradle.properties index 96c7a36..cff9d61 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.110021 +mod_version=1.1.0.110021 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/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/AliasRecoveryChecks.java b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/AliasRecoveryChecks.java new file mode 100644 index 0000000..f6035c1 --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/AliasRecoveryChecks.java @@ -0,0 +1,94 @@ +package zone.moddev.mc.skysgrassslabs.integrationtest; + +import net.minecraft.block.Block; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.entity.item.EntityItemFrame; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.init.Blocks; +import net.minecraft.nbt.*; +import net.minecraft.tileentity.TileEntityChest; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.Chunk; +import net.minecraftforge.event.world.ChunkDataEvent; +import net.minecraftforge.event.entity.EntityJoinWorldEvent; +import net.minecraftforge.common.util.FakePlayer; +import net.minecraftforge.common.util.FakePlayerFactory; +import net.minecraftforge.fml.common.gameevent.PlayerEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraft.util.EnumFacing; +import com.mojang.authlib.GameProfile; +import java.util.UUID; +import zone.moddev.mc.skysgrassslabs.compat.*; +import zone.moddev.mc.skysgrassslabs.init.ModBlocks; +import zone.moddev.mc.skysgrassslabs.world.ModWorldState; + +/** Mixed permanent/historical identities, including an already migrated chunk. */ +public final class AliasRecoveryChecks { + private static BlockPos pos(int x){return new BlockPos(4096+x,80,4096);} + public static void fresh(WorldServer world) { + Block[] aliases=BuildingBricksCompat.legacyAliases();require(aliases.length==3,"three distinct holders"); + NBTTagCompound before=state(world); + for(int i=0;i<3;i++)for(int orientation=0;orientation<2;orientation++) { + Block alias=aliases[i];require(alias!=ModBlocks.GRASS_SLAB&&alias!=ModBlocks.DIRT_SLAB,"holder identity must not collide with permanent IDs"); + world.setBlockState(pos(i*2+orientation),alias.getStateFromMeta(orientation),2); + } + world.setBlockState(pos(6),sky(true).getStateFromMeta(0),2); + world.setBlockState(pos(7),sky(false).getStateFromMeta(1),2); + world.setBlockState(pos(8),Blocks.CHEST.getDefaultState(),2); + TileEntityChest chest=(TileEntityChest)world.getTileEntity(pos(8)); + for(int i=0;i<3;i++)chest.setInventorySlotContents(i,stack(aliases[i],i+7)); + // A real entity join exercises the early live-item path without a counter reset. + EntityItem dropped=new EntityItem(world,4096,83,4096,stack(aliases[2],11)); + new LegacyMigrationHandler().joinEntity(new EntityJoinWorldEvent(dropped,world)); + require(dropped.getEntityItem().getItem()==Item.getItemFromBlock(ModBlocks.GRASS_SLAB)&&dropped.getEntityItem().stackSize==11&&dropped.getEntityItem().getTagCompound().getString("custom").equals("preserved"),"dropped stack recovery"); + LegacyMigrationHandler events=new LegacyMigrationHandler(); + EntityItemFrame frame=new EntityItemFrame(world,pos(9),EnumFacing.NORTH); + frame.setDisplayedItem(stack(aliases[1],1)); + events.joinEntity(new EntityJoinWorldEvent(frame,world)); + checkStack(frame.getDisplayedItem(),ModBlocks.DIRT_SLAB,1,"frame"); + FakePlayer player=FakePlayerFactory.get(world,new GameProfile( + UUID.fromString("11111111-1111-4111-8111-111111111111"),"SlabRecoveryTest")); + player.inventory.setInventorySlotContents(0,stack(aliases[0],13)); + player.getInventoryEnderChest().setInventorySlotContents(0,stack(aliases[1],17)); + events.playerLogin(new PlayerEvent.PlayerLoggedInEvent(player)); + checkStack(player.inventory.getStackInSlot(0),ModBlocks.GRASS_SLAB,13,"player"); + checkStack(player.getInventoryEnderChest().getStackInSlot(0),ModBlocks.DIRT_SLAB,17,"ender chest"); + // Queue a loaded chest, then put a historical stack through its live handler. + world.setBlockState(pos(10),Blocks.CHEST.getDefaultState(),2); + events.loadChunk(new ChunkDataEvent.Load(world.getChunkFromBlockCoords(pos(10)),new NBTTagCompound())); + TileEntityChest handlerChest=(TileEntityChest)world.getTileEntity(pos(10)); + handlerChest.setInventorySlotContents(0,stack(aliases[2],19)); + events.migrateLoadedHandlers(new TickEvent.ServerTickEvent(TickEvent.Phase.END)); + checkStack(handlerChest.getStackInSlot(0),ModBlocks.GRASS_SLAB,19,"live item handler"); + events.playerLogin(new PlayerEvent.PlayerLoggedInEvent(player)); + checkStack(player.inventory.getStackInSlot(0),ModBlocks.GRASS_SLAB,13,"second login"); + require(before.equals(state(world)),"absent recovery preserves historical counters"); + System.out.println("SKY_ALIAS_FIXTURE_SEEDED historical_and_permanent_ids=true player_ender_frame_handler=true"); + } + public static void reload(WorldServer world) { + NBTTagCompound before=state(world); + for(int i=0;i<3;i++)for(int orientation=0;orientation<2;orientation++) { + Block expected=i==1?ModBlocks.DIRT_SLAB:ModBlocks.GRASS_SLAB; + require(world.getBlockState(pos(i*2+orientation)).equals(expected.getStateFromMeta(orientation)),"alias block orientation "+i+" / "+orientation); + } + require(world.getBlockState(pos(6)).equals(sky(true).getStateFromMeta(0))&&world.getBlockState(pos(7)).equals(sky(false).getStateFromMeta(1)),"permanent mixed content unchanged"); + TileEntityChest chest=(TileEntityChest)world.getTileEntity(pos(8)); + for(int i=0;i<3;i++){ItemStack s=chest.getStackInSlot(i);require(s!=null&&s.stackSize==i+7&&s.getItem()==Item.getItemFromBlock(i==1?ModBlocks.DIRT_SLAB:ModBlocks.GRASS_SLAB)&&s.getTagCompound().getString("custom").equals("preserved"),"chest count and NBT");} + // Revisit a marker that predates the remaining historical content. + Chunk chunk=world.getChunkFromBlockCoords(pos(0));Block alias=BuildingBricksCompat.grassSlab();world.setBlockState(pos(0),alias.getStateFromMeta(0),2); + NBTTagCompound root=new NBTTagCompound(),level=new NBTTagCompound(),section=new NBTTagCompound(),marker=new NBTTagCompound();root.setTag("Level",level);marker.setInteger("buildingbricks_migration_version",1);marker.setString("future_field","preserved");root.setTag("skysgrassslabs",marker); + byte[] low=new byte[4096],high=new byte[2048],data=new byte[2048];int id=Block.getIdFromBlock(alias);low[0]=(byte)id;high[0]=(byte)(id>>8);section.setByte("Y",(byte)5);section.setByteArray("Blocks",low);section.setByteArray("Add",high);section.setByteArray("Data",data);NBTTagList sections=new NBTTagList();sections.appendTag(section);level.setTag("Sections",sections); + LegacyMigrationHandler events=new LegacyMigrationHandler();events.loadChunk(new ChunkDataEvent.Load(chunk,root)); + require(world.getBlockState(pos(0)).equals(sky(true).getStateFromMeta(0)),"old marker cannot skip remaining recovery"); + NBTTagCompound saved=new NBTTagCompound();events.saveChunk(new ChunkDataEvent.Save(chunk,saved));require(saved.getCompoundTag("skysgrassslabs").equals(marker),"complete old marker preserved"); + require(before.equals(state(world)),"reload preserves counters"); + System.out.println("SKY_ALIAS_RECOVERY_PASS orientations=2 aliases=3 counts_and_nbt=true old_marker=true"); + } + private static ItemStack stack(Block alias,int count){ItemStack s=new ItemStack(alias,count,0);NBTTagCompound tag=new NBTTagCompound();tag.setString("custom","preserved");s.setTagCompound(tag);return s;} + private static void checkStack(ItemStack stack,Block block,int count,String context){require(stack!=null&&stack.getItem()==Item.getItemFromBlock(block)&&stack.stackSize==count&&stack.getTagCompound().getString("custom").equals("preserved"),context+" identity/count/NBT");} + private static NBTTagCompound state(WorldServer world){return ((net.minecraft.world.WorldSavedData)ModWorldState.get(world)).writeToNBT(new NBTTagCompound());} + private static Block sky(boolean grass){return grass?ModBlocks.GRASS_SLAB:ModBlocks.DIRT_SLAB;} + private static void require(boolean value,String message){if(!value)throw new AssertionError(message);} +} 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 9d3bd4f..5261d73 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/IntegrationTestMod.java +++ b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/IntegrationTestMod.java @@ -101,9 +101,13 @@ public void serverStarted(FMLServerStartedEvent event) { String phase = System.getProperty("skysgrassslabs.integrationPhase", "fresh"); try { Properties evidence = load(marker); - if ("fresh".equals(phase)) { + if ("upgrade-seed".equals(phase) || "upgrade-reload".equals(phase)) { + ReleasedUpgradeChecks.run(world,"upgrade-seed".equals(phase)); + evidence.setProperty(phase+"_complete","true"); + } else if ("fresh".equals(phase)) { int gameplayChecks = verifyGameplay(server, world); int worldgenChecks = verifyWorldgen(world); + if(BuildingBricksCompat.hasLegacyAliases())AliasRecoveryChecks.fresh(world); evidence.setProperty("gameplay_checks", Integer.toString(gameplayChecks)); evidence.setProperty("worldgen_checks", Integer.toString(worldgenChecks)); if (Loader.isModLoaded(BuildingBricksCompat.MOD_ID)) { @@ -122,6 +126,7 @@ public void serverStarted(FMLServerStartedEvent event) { require(ModWorldState.SCHEMA_VERSION == 1, "World-state schema changed during reload"); require(ModWorldState.get(world) != null, "World state did not reload"); + if(BuildingBricksCompat.hasLegacyAliases())AliasRecoveryChecks.reload(world); if ("true".equals(evidence.getProperty("compat_fixture_seeded"))) { require(!SkysGrassSlabsConfig.forceReplaceBuildingBricksSlabs(), "Default coexistence reload unexpectedly enabled replacement"); 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..e42b2f2 --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/ReleasedUpgradeChecks.java @@ -0,0 +1,60 @@ +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.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.tileentity.TileEntityChest; +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; + +/** Runs against the released jar first, then checks the same save with its successor. */ +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 number=0; + for(Block block:blocks)for(int metadata=0;metadata<(block==ModBlocks.TURF?1:2);metadata++) { + BlockPos pos=new BlockPos(160+number++,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 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 section:world.getChunkFromBlockCoords(chestPos).getEntityLists())for(Entity entity:section) + if(entity instanceof EntityItem) { + ItemStack stack=((EntityItem)entity).getEntityItem(); + if(stack.getTagCompound()!=null&&stack.getTagCompound().hasKey("upgrade_entity")) { + require(stack.getItem()==net.minecraft.item.Item.getItemFromBlock(ModBlocks.GRASS_SLAB)&&stack.stackSize==7&&stack.getMetadata()==1&&stack.getTagCompound().getString("upgrade_entity").equals("Retained"),"Released dropped item and NBT");found=true; + } + } + require(found,"Released entity survived"); + ModWorldState state=ModWorldState.get(world); + require(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="+number); + } + private static void require(boolean value,String message){if(!value)throw new AssertionError(message);} +} diff --git a/src/integrationTest/resources/log4j2.xml b/src/integrationTest/resources/log4j2.xml new file mode 100644 index 0000000..d15f663 --- /dev/null +++ b/src/integrationTest/resources/log4j2.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java b/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java index 2063a35..da52173 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.FMLMissingMappingsEvent; import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; @@ -17,13 +18,14 @@ import zone.moddev.mc.skysgrassslabs.init.ModRecipes; import zone.moddev.mc.skysgrassslabs.proxy.CommonProxy; import zone.moddev.mc.skysgrassslabs.world.GrassSlabSmoothingHandler; +import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; @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.110021"; + public static final String VERSION = "1.1.0.110021"; @SidedProxy( clientSide = "zone.moddev.mc.skysgrassslabs.proxy.ClientProxy", @@ -37,13 +39,16 @@ public void preInit(FMLPreInitializationEvent event) { logger = event.getModLog(); SkysGrassSlabsConfig.load(event.getSuggestedConfigurationFile()); ModBlocks.register(); + for (int meta = 0; meta < 2; ++meta) + GrassSlabsApi.registerGrassForm(ModBlocks.DIRT_SLAB.getStateFromMeta(meta), + ModBlocks.GRASS_SLAB.getStateFromMeta(meta)); BuildingBricksCompat.preInit(event.getModConfigurationDirectory()); MinecraftForge.EVENT_BUS.register(new CommonEvents()); 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(); @@ -60,6 +65,9 @@ public void postInit(FMLPostInitializationEvent event) { BuildingBricksCompat.resolveBlocks(); } + @Mod.EventHandler + public void loadComplete(FMLLoadCompleteEvent event) { GrassSlabsApi.freeze(); } + @Mod.EventHandler public void missingMappings(FMLMissingMappingsEvent event) { LegacyMigrationHandler.remapMissingMappings(event); 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 d49e48c..7fef8ee 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSlabBlock.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSlabBlock.java @@ -165,8 +165,6 @@ public BlockRenderLayer getBlockLayer() { } 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 2785a7f..8663768 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/compat/BuildingBricksCompat.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/BuildingBricksCompat.java @@ -8,6 +8,7 @@ import java.util.Map; import net.minecraft.block.Block; import net.minecraft.item.Item; +import net.minecraft.item.ItemBlock; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; import net.minecraftforge.fml.common.Loader; @@ -26,6 +27,8 @@ public final class BuildingBricksCompat { private static Block grassSlab; private static Block dirtSlab; + private static Block historicalGrassSlab; + private static boolean legacyAliasesRegistered; private static boolean bridgeRecipesRegistered; private static final Map buildingBricksBlocks = Collections.synchronizedMap(new IdentityHashMap()); @@ -33,13 +36,14 @@ public final class BuildingBricksCompat { public static void preInit(File configDirectory) { boolean installed = Loader.isModLoaded(MOD_ID); if (!installed) { + registerLegacyAliases(); return; } SkysGrassSlabs.logger.info("BuildingBricks slab replacement is {}", SkysGrassSlabsConfig.forceReplaceBuildingBricksSlabs() ? "enabled" : "disabled"); if (!BuildingBricksPolicy.shouldArbitrateWorldgen(installed, - SkysGrassSlabsConfig.generateGrassSlabs())) return; + SkysGrassSlabsConfig.isSmoothingActive())) return; File configFile = new File(configDirectory, "BuildingBricks/general.cfg"); try { @@ -62,6 +66,8 @@ public static void resolveBlocks() { ? Block.REGISTRY.getObject(GRASS_SLAB_ID) : null; dirtSlab = Block.REGISTRY.containsKey(DIRT_SLAB_ID) ? Block.REGISTRY.getObject(DIRT_SLAB_ID) : null; + historicalGrassSlab = Block.REGISTRY.containsKey(HISTORICAL_GRASS_SLAB_ID) + ? Block.REGISTRY.getObject(HISTORICAL_GRASS_SLAB_ID) : null; buildingBricksBlocks.clear(); for (ResourceLocation id : Block.REGISTRY.getKeys()) { if (MOD_ID.equals(id.getResourceDomain())) { @@ -84,6 +90,40 @@ public static boolean isInstalled() { return Loader.isModLoaded(MOD_ID); } + public static Block historicalGrassSlab() { + if (historicalGrassSlab == null) resolveBlocks(); + return historicalGrassSlab; + } + + public static boolean hasLegacyAliases() { + return legacyAliasesRegistered; + } + + public static Block[] legacyAliases() { + return legacyAliasesRegistered ? new Block[] {grassSlab(), dirtSlab(), historicalGrassSlab()} + : new Block[0]; + } + + private static void registerLegacyAliases() { + registerLegacyAlias(GRASS_SLAB_ID, true); + registerLegacyAlias(DIRT_SLAB_ID, false); + registerLegacyAlias(HISTORICAL_GRASS_SLAB_ID, true); + legacyAliasesRegistered = true; + resolveBlocks(); + } + + private static void registerLegacyAlias(ResourceLocation id, boolean grass) { + LegacySlabAliasBlock block = new LegacySlabAliasBlock(grass); + block.setRegistryName(id).setUnlocalizedName(SkysGrassSlabs.MOD_ID + ".legacy_" + id.getResourcePath()); + ItemBlock item = new ItemBlock(block) { + @Override public int getMetadata(int damage) { return damage & 1; } + }; + item.setHasSubtypes(true); + item.setRegistryName(id); + GameRegistry.register(block); + GameRegistry.register(item); + } + static boolean shouldReplaceSlabs() { return BuildingBricksPolicy.shouldReplaceSlabs(isInstalled(), SkysGrassSlabsConfig.forceReplaceBuildingBricksSlabs()); @@ -104,8 +144,9 @@ public static Map buildingBricksBlockIdsByNumericId() { } public static boolean isGrassSlabItem(ItemStack stack) { - return stack != null && grassSlab() != null && - stack.getItem() == Item.getItemFromBlock(grassSlab()); + return stack != null && ((grassSlab() != null && + stack.getItem() == Item.getItemFromBlock(grassSlab())) || + (historicalGrassSlab() != null && stack.getItem() == Item.getItemFromBlock(historicalGrassSlab()))); } static boolean isDirtSlabItem(ItemStack stack) { 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 f2bb366..c5c5b3a 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.fml.common.event.FMLMissingMappingsEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.PlayerEvent; @@ -44,8 +49,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) { @@ -56,7 +62,16 @@ public void loadChunk(ChunkDataEvent.Load event) { Chunk chunk = event.getChunk(); ChunkKey chunkKey = ChunkKey.of(world, chunk); if (shouldPreserveChunkMarker(markerVersion)) { - chunkMarkersToSave.put(chunkKey, 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)) { return; } if (!shouldMigrateChunk(BuildingBricksCompat.shouldReplaceSlabs(), markerVersion)) return; @@ -69,49 +84,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()) { + 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()) { + if (!(event.player instanceof EntityPlayer) || (!BuildingBricksCompat.shouldReplaceSlabs() && !BuildingBricksCompat.hasLegacyAliases())) { return; } EntityPlayer player = event.player; - ModWorldState state = ModWorldState.get(player.getEntityWorld()); + ModWorldState state = BuildingBricksCompat.hasLegacyAliases() ? null : ModWorldState.get(player.getEntityWorld()); migrateInventory(player.inventory, state); migrateInventory(player.getInventoryEnderChest(), state); } @@ -127,16 +153,11 @@ public void saveWorld(WorldEvent.Save event) { } public static void remapMissingMappings(FMLMissingMappingsEvent event) { - for (FMLMissingMappingsEvent.MissingMapping mapping : event.getAll()) { - LegacySlabKind kind = legacySlabKind(mapping.resourceLocation); - if (kind == null) continue; - boolean grass = kind == LegacySlabKind.GRASS; - if (mapping.type == GameRegistry.Type.BLOCK) { - mapping.remap(grass ? ModBlocks.GRASS_SLAB : ModBlocks.DIRT_SLAB); - } else if (mapping.type == GameRegistry.Type.ITEM) { - mapping.remap(Item.getItemFromBlock(grass ? ModBlocks.GRASS_SLAB : ModBlocks.DIRT_SLAB)); - } - } + // Absent-mod holders retain each old registry identity separately. + // Remapping several saved IDs onto an already registered Sky object + // corrupts Forge 1.10's numeric registry reconciliation in mixed saves. + // Installed BuildingBricks owns its own IDs; unknown entries stay visible + // in Forge's normal warning rather than being silently substituted. } static LegacySlabKind legacySlabKind(ResourceLocation id) { @@ -155,7 +176,7 @@ static boolean shouldPreserveChunkMarker(int markerVersion) { return markerVersion >= ModWorldState.MIGRATION_VERSION; } - private static void migrateBlocks(Chunk chunk, NBTTagCompound chunkData, + private static boolean migrateBlocks(Chunk chunk, NBTTagCompound chunkData, ModWorldState worldState) { long grassTop = 0; long grassBottom = 0; @@ -168,6 +189,8 @@ private static void migrateBlocks(Chunk chunk, NBTTagCompound chunkData, Map buildingBricksIds = BuildingBricksCompat.buildingBricksBlockIdsByNumericId(); int grassId = Block.getIdFromBlock(BuildingBricksCompat.grassSlab()); + int historicalGrassId = BuildingBricksCompat.historicalGrassSlab() == null ? -1 + : Block.getIdFromBlock(BuildingBricksCompat.historicalGrassSlab()); int dirtId = Block.getIdFromBlock(BuildingBricksCompat.dirtSlab()); for (int sectionIndex = 0; sectionIndex < serializedSections.tagCount(); ++sectionIndex) { NBTTagCompound serializedSection = serializedSections.getCompoundTagAt(sectionIndex); @@ -182,11 +205,11 @@ private static void migrateBlocks(Chunk chunk, NBTTagCompound chunkData, for (int index = 0; index < blocks.length; ++index) { int numericId = (blocks[index] & 255) | (add == null ? 0 : add.getFromIndex(index) << 8); - if (numericId == grassId || numericId == dirtId) { + 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, slabMetadata)); - if (numericId == grassId) { + skyStateFor(numericId == grassId || numericId == historicalGrassId, slabMetadata)); + if (numericId == grassId || numericId == historicalGrassId) { if (slabMetadata == 0) ++grassTop; else ++grassBottom; } else { @@ -202,23 +225,27 @@ private static void migrateBlocks(Chunk chunk, NBTTagCompound chunkData, } } } - worldState.recordGrassBlocks(grassTop, 0); - worldState.recordGrassBlocks(grassBottom, 1); - worldState.recordDirtBlocks(dirtTop, 0); - worldState.recordDirtBlocks(dirtBottom, 1); - for (Map.Entry entry : unsupported.entrySet()) { - worldState.recordUnsupported("block:" + entry.getKey(), entry.getValue()); + if (worldState != null) { + worldState.recordGrassBlocks(grassTop, 0); + worldState.recordGrassBlocks(grassBottom, 1); + worldState.recordDirtBlocks(dirtTop, 0); + worldState.recordDirtBlocks(dirtBottom, 1); + for (Map.Entry entry : unsupported.entrySet()) { + worldState.recordUnsupported("block:" + entry.getKey(), entry.getValue()); + } } + return grassTop + grassBottom + dirtTop + dirtBottom > 0; } private static IBlockState replacementFor(IBlockState oldState) { Block oldBlock = oldState.getBlock(); if (oldBlock != BuildingBricksCompat.grassSlab() && + oldBlock != BuildingBricksCompat.historicalGrassSlab() && oldBlock != BuildingBricksCompat.dirtSlab()) { return null; } int metadata = oldBlock.getMetaFromState(oldState) & 1; - return skyStateFor(oldBlock == BuildingBricksCompat.grassSlab(), metadata); + return skyStateFor(oldBlock == BuildingBricksCompat.grassSlab() || oldBlock == BuildingBricksCompat.historicalGrassSlab(), metadata); } static IBlockState skyStateFor(boolean grass, int metadata) { @@ -226,11 +253,13 @@ static IBlockState skyStateFor(boolean grass, int metadata) { .getStateFromMeta(metadata & 1); } - private static void migrateChunkInventories(Chunk chunk, ModWorldState state) { + private static boolean migrateChunkInventories(Chunk chunk, ModWorldState state) { + boolean changed = false; for (TileEntity tileEntity : chunk.getTileEntityMap().values()) { NBTTagCompound serialized = tileEntity.writeToNBT(new NBTTagCompound()); if (migrateStacksInNbt(serialized, state)) { tileEntity.readFromNBT(serialized); + changed = true; } } for (ClassInheritanceMultiMap list : chunk.getEntityLists()) { @@ -238,6 +267,33 @@ private static void migrateChunkInventories(Chunk chunk, ModWorldState state) { NBTTagCompound serialized = entity.serializeNBT(); if (migrateStacksInNbt(serialized, state)) { entity.deserializeNBT(serialized); + changed = true; + } + } + } + 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.xPosition<<4,0,chunk.zPosition<<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;slotGrassSlabsApi.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())); + } +} diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/compat/MigrationMappingTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/compat/MigrationMappingTest.java index 90d5c08..621186c 100644 --- a/src/test/java/zone/moddev/mc/skysgrassslabs/compat/MigrationMappingTest.java +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/compat/MigrationMappingTest.java @@ -2,6 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.*; +import net.minecraft.nbt.*; import net.minecraft.block.BlockSlab; import net.minecraft.block.state.IBlockState; @@ -37,4 +39,26 @@ void onlyTheOrientationBitIsAcceptedFromHistoricalMetadata() { assertEquals(BlockSlab.EnumBlockHalf.BOTTOM, LegacyMigrationHandler.skyStateFor(false, 3).getValue(BlockSlab.HALF)); } + + @Test void holdersRetainBothOrientationsWithoutPlayableEntries() { + for(boolean grass:new boolean[]{false,true}) { + LegacySlabAliasBlock holder=new LegacySlabAliasBlock(grass); + assertNull(holder.getCreativeTabToDisplayOn());assertFalse(holder.hasTileEntity(holder.getDefaultState())); + for(int metadata=0;metadata<16;metadata++)assertEquals(metadata&1,holder.getMetaFromState(holder.getStateFromMeta(metadata))); + } + } + + @Test void historicalStacksKeepCountsAndUnrelatedNestedData() { + if(ModBlocks.GRASS_SLAB.getRegistryName()==null)ModBlocks.GRASS_SLAB.setRegistryName("skysgrassslabs","grass_slab"); + if(ModBlocks.DIRT_SLAB.getRegistryName()==null)ModBlocks.DIRT_SLAB.setRegistryName("skysgrassslabs","dirt_slab"); + for(String id:new String[]{"buildingbricks:grass_slab","buildingbricks:dirt_slab","buildingbrickscompatvanilla:grass_slab"}) { + NBTTagCompound stack=new NBTTagCompound(),tag=new NBTTagCompound();stack.setString("id",id);stack.setByte("Count",(byte)47);stack.setShort("Damage",(short)13); + tag.setString("custom_name","Original");tag.setInteger("RepairCost",9);tag.setString("material","minecraft:dirt");stack.setTag("tag",tag); + NBTTagCompound caps=new NBTTagCompound();caps.setString("other_capability","preserved");stack.setTag("ForgeCaps",caps); + assertTrue(LegacyMigrationHandler.migrateStacksInNbt(stack,null)); + assertEquals(id.contains("dirt_slab")?"skysgrassslabs:dirt_slab":"skysgrassslabs:grass_slab",stack.getString("id")); + assertEquals(47,stack.getByte("Count"));assertEquals(tag,stack.getCompoundTag("tag"));assertEquals(caps,stack.getCompoundTag("ForgeCaps")); + NBTTagCompound converted=stack.copy();assertFalse(LegacyMigrationHandler.migrateStacksInNbt(stack,null));assertEquals(converted,stack); + } + } }