From 3488ed58403722ac3ff203cff7680dfc16e0fd3f Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 9 Nov 2026 13:48:20 +0000 Subject: [PATCH 1/2] Keep companion mod data during legacy world upgrades --- CHANGELOG.txt | 4 + README.md | 11 +- docs/VERSIONS.md | 4 +- gradle.properties | 2 +- gradle/ide/eclipse.gradle | 2 +- gradle/prepare-forge-client-runtime.py | 2 +- gradle/release/artifacts.gradle | 4 +- gradle/verification/packaged-runtime.gradle | 2 +- .../Legacy112MigrationExtension.java | 40 +++++++ .../migration/Legacy112WorldMigrator.java | 102 ++++++++++++---- .../packaged-resources/META-INF/mods.toml | 2 +- .../basemetals/smoke/PackagedServerSmoke.java | 2 +- .../packaged-resources/META-INF/mods.toml | 2 +- .../Legacy112MigrationExtensionTest.java | 109 ++++++++++++++++++ 14 files changed, 254 insertions(+), 34 deletions(-) create mode 100644 src/main/java/zone/moddev/mc/basemetals/api/migration/Legacy112MigrationExtension.java create mode 100644 src/test/java/zone/moddev/mc/basemetals/migration/Legacy112MigrationExtensionTest.java diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1e65c01ad..118900861 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,7 @@ +Base Metals 3.1.2.118021 + +- Let companion mods preserve their saved block states, items and entities before Base Metals flattens a Minecraft 1.10 or 1.12 world. + Base Metals 3.1.1.118021 - Fix startup failures when Base Metals is added to an existing modern world, including worlds with external chunks or empty region files. diff --git a/README.md b/README.md index 319abf50b..473299dac 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ shields, bows, crossbows, arrows, and bolts. The mod also includes Crack Hammers, scythes, metal anvils, the human detector, villager trades, advancements, and structure loot. -This is the Minecraft 1.18.2 Forge version, `3.1.1.118021`. It brings over the +This is the Minecraft 1.18.2 Forge version, `3.1.2.118021`. It brings over the fixes and features from the earlier ports, including raw metals, ore texture overlays and High/Low Fantasy modes. Existing Base Metals items and blocks keep their registry IDs. OreSpawn handles ore generation; MMDLib and Additional Loot @@ -26,7 +26,7 @@ Tables are no longer needed. - OreSpawn `[4.1.0.118021,5.0.0)` on both client and server This version uses OreSpawn `4.1.0.118021`. Put -`BaseMetals-3.1.1.118021.jar` and a compatible OreSpawn JAR in the `mods` +`BaseMetals-3.1.2.118021.jar` and a compatible OreSpawn JAR in the `mods` directory. MMDLib and Additional Loot Tables must not be carried forward from an older installation. Mineralogy is optional. @@ -130,6 +130,11 @@ is unavailable. Always perform the first upgrade on a copy of the world and keep the untouched original. Read [Migration](docs/MIGRATION.md) before starting an upgraded world. +Companion mods can register a `Legacy112MigrationExtension` during common +setup. It lets them prepare their own saved data and restore block metadata +before Base Metals' legacy conversion would otherwise discard it. Existing +Base Metals saves do not need the companion mod to use this upgrade path. + Adding Base Metals to a modern world does not trigger the old numeric-ID conversion just because an older backup is present. Existing player data stays in place; genuine legacy upgrades still make backups before changing saved data. @@ -183,7 +188,7 @@ artifacts. GameTests, smoke probes, migration fixtures, and reference sources are isolated from the published JAR. Release artifacts use Maven coordinate -`zone.moddev.mc.basemetals:BaseMetals:3.1.1.118021`. +`zone.moddev.mc.basemetals:BaseMetals:3.1.2.118021`. Report defects and compatibility problems through the [Base Metals issue tracker](https://github.com/MinecraftModDevelopmentMods/BaseMetals/issues). diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index a92b70a79..bd713c0d5 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -1,6 +1,6 @@ # Supported versions -Base Metals `3.1.1.118021` targets Minecraft 1.18.2, Forge 40.3.0 and Java 17. +Base Metals `3.1.2.118021` targets Minecraft 1.18.2, Forge 40.3.0 and Java 17. OreSpawn `4.1.0.118021` is the exact build and test dependency. Clients and servers need OreSpawn `[4.1.0.118021,5.0.0)`. @@ -10,7 +10,7 @@ Its SHA-256 is `BD8D84A96DAFB7668E95D61B9D72C62AEF3440675473E41244C182C64653356F`. Release artifacts use Maven coordinate -`zone.moddev.mc.basemetals:BaseMetals:3.1.1.118021`. +`zone.moddev.mc.basemetals:BaseMetals:3.1.2.118021`. The provider remains schema 3, revision 1. OreSpawn 4.1 is needed for the configuration-screen API; the provider's eleven rule IDs and 1.18 distributions diff --git a/gradle.properties b/gradle.properties index be9fd14ae..c7991c665 100644 --- a/gradle.properties +++ b/gradle.properties @@ -67,7 +67,7 @@ mod_name=Base Metals # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=LGPL-2.1 # The mod version. See https://semver.org/ -mod_version=3.1.1.118021 +mod_version=3.1.2.118021 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/gradle/ide/eclipse.gradle b/gradle/ide/eclipse.gradle index 634322121..814d97d02 100644 --- a/gradle/ide/eclipse.gradle +++ b/gradle/ide/eclipse.gradle @@ -304,7 +304,7 @@ def verifyEclipseProductionClasspath = tasks.register('verifyEclipseProductionCl } } String metadata = new File(projectDir, 'bin/main/META-INF/mods.toml').getText('UTF-8') - if (metadata.contains('${') || !metadata.contains('version="3.1.1.118021"')) { + if (metadata.contains('${') || !metadata.contains('version="3.1.2.118021"')) { throw new GradleException('Eclipse output contains unresolved or invalid Base Metals version metadata') } } diff --git a/gradle/prepare-forge-client-runtime.py b/gradle/prepare-forge-client-runtime.py index cda7327f2..d67cfd3ae 100644 --- a/gradle/prepare-forge-client-runtime.py +++ b/gradle/prepare-forge-client-runtime.py @@ -23,7 +23,7 @@ FORGE_INSTALLER_SHA256 = "9434c29790504dcd11ce97cb30dd8891b4b18848982357bc1b5abfa79fb95103" VERSION_MANIFEST_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json" ASSET_OBJECT_URL = "https://resources.download.minecraft.net/{prefix}/{digest}" -USER_AGENT = "BaseMetals-release-validation/3.1.1.118021" +USER_AGENT = "BaseMetals-release-validation/3.1.2.118021" def digest_file(path: Path, algorithm: str) -> str: diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle index ec446df48..a314895c4 100644 --- a/gradle/release/artifacts.gradle +++ b/gradle/release/artifacts.gradle @@ -199,7 +199,7 @@ tasks.register('verifyReleaseConfiguration') { dependsOn tasks.named('verifyMavenizerCompatibilityFixture') dependsOn tasks.named('verifyJava17Toolchain') doLast { - if (configuredReleaseIdentity.modVersion != '3.1.1.118021' + if (configuredReleaseIdentity.modVersion != '3.1.2.118021' || configuredReleaseIdentity.modGroup != expectedMavenGroup || configuredReleaseIdentity.minecraftVersion != '1.18.2' || configuredReleaseIdentity.forgeVersion != '40.3.0' @@ -308,7 +308,7 @@ tasks.register('verifyReleaseArtifacts') { } String modsToml = zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8') if (modsToml.contains('${') - || !modsToml.contains('version="3.1.1.118021"') + || !modsToml.contains('version="3.1.2.118021"') || !modsToml.contains('versionRange="[4.1.0.118021,5.0.0)"')) { throw new GradleException('Release mods.toml contains unresolved or incompatible metadata') } diff --git a/gradle/verification/packaged-runtime.gradle b/gradle/verification/packaged-runtime.gradle index 437c76119..d6d3b9ed1 100644 --- a/gradle/verification/packaged-runtime.gradle +++ b/gradle/verification/packaged-runtime.gradle @@ -106,7 +106,7 @@ def packagedServerStartupSmoke = tasks.register('packagedServerStartupSmoke', Ex if (!marker.isFile()) throw new GradleException('Packaged dedicated-server completion marker is missing') Properties values = new Properties() marker.withInputStream { values.load(it) } - if (values.getProperty('basemetals') != '3.1.1.118021' + if (values.getProperty('basemetals') != '3.1.2.118021' || values.getProperty('orespawn') != '4.1.0.118021' || values.getProperty('forge') != '40.3.0') { throw new GradleException("Packaged dedicated-server versions are wrong: ${values}") diff --git a/src/main/java/zone/moddev/mc/basemetals/api/migration/Legacy112MigrationExtension.java b/src/main/java/zone/moddev/mc/basemetals/api/migration/Legacy112MigrationExtension.java new file mode 100644 index 000000000..cd622f470 --- /dev/null +++ b/src/main/java/zone/moddev/mc/basemetals/api/migration/Legacy112MigrationExtension.java @@ -0,0 +1,40 @@ +package zone.moddev.mc.basemetals.api.migration; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.state.BlockState; +import zone.moddev.mc.basemetals.migration.Legacy112WorldMigrator; + +/** + * Lets a companion mod preserve its data when Base Metals pre-flattens a + * Minecraft 1.10 or 1.12 world. Register during common setup, before worlds load. + */ +public interface Legacy112MigrationExtension { + enum Payload { CHUNK, PLAYER } + + /** + * Updates a legacy chunk or player payload in place, before vanilla's data + * fixer or the migrator's block entity snapshot. Return true on mutation. + */ + default boolean prepareData(CompoundTag tag, int dataVersion, Payload payload) { return false; } + + /** Runs immediately before the intermediate chunk data fix, only for chunks being converted. */ + default void beforeChunkFlattening(CompoundTag chunk) { } + + /** + * Checks or restores extension data after vanilla converts the embedded + * single-player record. The prepared source is a read-only snapshot. + */ + default void afterPlayerFlattening(CompoundTag preparedSource, CompoundTag result) { } + + /** + * Returns the native state for an owned legacy block, or null for an ID + * this extension does not own. Metadata is the original four-bit value. + */ + default BlockState decodeBlockState(ResourceLocation legacyId, int metadata) { return null; } + + /** The ID identifies the registering extension and must be unique. */ + static void register(ResourceLocation id, Legacy112MigrationExtension extension) { + Legacy112WorldMigrator.registerExtension(id, extension); + } +} diff --git a/src/main/java/zone/moddev/mc/basemetals/migration/Legacy112WorldMigrator.java b/src/main/java/zone/moddev/mc/basemetals/migration/Legacy112WorldMigrator.java index b085f0320..146f4b909 100644 --- a/src/main/java/zone/moddev/mc/basemetals/migration/Legacy112WorldMigrator.java +++ b/src/main/java/zone/moddev/mc/basemetals/migration/Legacy112WorldMigrator.java @@ -22,11 +22,15 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentSkipListMap; import java.util.zip.DeflaterOutputStream; import zone.moddev.mc.basemetals.BaseMetals; import zone.moddev.mc.basemetals.MissingMappings; +import zone.moddev.mc.basemetals.api.migration.Legacy112MigrationExtension; +import zone.moddev.mc.basemetals.api.migration.Legacy112MigrationExtension.Payload; import zone.moddev.mc.basemetals.content.BaseMetalAnvilBlock; import zone.moddev.mc.basemetals.content.CompatibilityDoubleSlabBlock; import zone.moddev.mc.basemetals.content.ModContent; @@ -79,12 +83,19 @@ public final class Legacy112WorldMigrator { private static final int SECTOR_BYTES = 4_096; private static final String MARKER = "BASEMETALS_1_12_BLOCK_MIGRATION_COMPLETE.txt"; private static final String BACKUP_DIRECTORY = "basemetals-1.12-region-backup"; + private static final Map EXTENSIONS = new ConcurrentSkipListMap<>(); private static Path preparedWorld; private static CompoundTag preparedPlayer; private static int preparedVersion; private Legacy112WorldMigrator() {} + public static void registerExtension(ResourceLocation id, Legacy112MigrationExtension extension) { + if (EXTENSIONS.putIfAbsent(Objects.requireNonNull(id), Objects.requireNonNull(extension)) != null) { + throw new IllegalArgumentException("Duplicate legacy world migration extension: " + id); + } + } + public static void migrateIfNeeded(Path worldRoot) throws IOException { Path root = worldRoot.toAbsolutePath().normalize(); if (Files.isRegularFile(root.resolve(MARKER))) return; @@ -96,6 +107,7 @@ public static void migrateIfNeeded(Path worldRoot) throws IOException { if (oldBlockIds.values().stream().noneMatch(id -> id.startsWith(BaseMetals.MOD_ID + ":"))) return; List regions = findRegionFiles(root); + List extensions = List.copyOf(EXTENSIONS.values()); int convertedRegions = 0; int convertedChunks = 0; @@ -105,7 +117,7 @@ public static void migrateIfNeeded(Path worldRoot) throws IOException { int convertedItemAliases = 0; int convertedDamageValues = 0; for (Path region : regions) { - RegionConversion result = convertRegion(root, region, oldBlockIds); + RegionConversion result = convertRegion(root, region, oldBlockIds, extensions); if (result.convertedChunks() > 0) { convertedRegions++; convertedChunks += result.convertedChunks(); @@ -116,8 +128,8 @@ public static void migrateIfNeeded(Path worldRoot) throws IOException { convertedDamageValues += result.convertedDamageValues(); } } - ItemMigration looseItems = restoreRewrittenSingleplayerPlayer(root) - .plus(migrateLooseInventories(root)); + ItemMigration looseItems = restoreRewrittenSingleplayerPlayer(root, extensions) + .plus(migrateLooseInventories(root, extensions)); convertedBuckets += looseItems.convertedBuckets(); convertedItemAliases += looseItems.convertedItemAliases(); convertedDamageValues += looseItems.convertedDamageValues(); @@ -133,7 +145,7 @@ public static void migrateIfNeeded(Path worldRoot) throws IOException { String result = "Converted " + convertedChunks + " legacy chunks in " + convertedRegions + " region files through Mojang's legacy flattening fixes. Backups: " + BACKUP_DIRECTORY - + ". Third-party states preserved by registry name at their default state: " + + ". Third-party states restored: " + externalStates + ". Legacy block entities preserved: " + preservedBlockEntities + ". Legacy filled buckets converted: " + convertedBuckets + ". Legacy item aliases forwarded: " + convertedItemAliases @@ -270,7 +282,8 @@ private static void ensureBackupCapacity(Path root, List regions) throws I } } - private static RegionConversion convertRegion(Path root, Path region, Map ids) throws IOException { + private static RegionConversion convertRegion(Path root, Path region, Map ids, + List extensions) throws IOException { byte[] source = Files.readAllBytes(region); if (source.length == 0) return new RegionConversion(0, 0, 0, 0, 0, 0); if (source.length < REGION_HEADER_BYTES) throw new IOException("Invalid region header: " + region); @@ -313,7 +326,7 @@ private static RegionConversion convertRegion(Path root, Path region, Map ids) throws IOException { + private static ChunkConversion convertChunk(CompoundTag chunk, Map ids, + List extensions) throws IOException { int sourceVersion = chunk.getInt("DataVersion"); if (sourceVersion > LAST_SUPPORTED_LEGACY_DATA_VERSION) { return new ChunkConversion(false, 0, 0, 0, 0, 0, chunk); } ItemMigration itemMigration = convertLegacyItems(chunk); + boolean extensionChanged = prepareExtensions(extensions, chunk, sourceVersion, Payload.CHUNK); CompoundTag level = chunk.getCompound("Level"); ListTag sections = level.getList("Sections", Tag.TAG_COMPOUND); List savedBlocks = new ArrayList<>(); @@ -371,16 +386,17 @@ private static ChunkConversion convertChunk(CompoundTag chunk, Map> savedBlockEntities = snapshotLegacyBlockEntities(level, savedBlocks); - if (savedBlocks.isEmpty() && itemMigration.isEmpty()) { + if (savedBlocks.isEmpty() && itemMigration.isEmpty() && !extensionChanged) { return new ChunkConversion(false, 0, 0, 0, 0, 0, chunk); } + for (Legacy112MigrationExtension extension : extensions) extension.beforeChunkFlattening(chunk); + Map> savedBlockEntities = snapshotLegacyBlockEntities(level, savedBlocks); CompoundTag fixed = NbtUtils.update(DataFixers.getDataFixer(), DataFixTypes.CHUNK, chunk, sourceVersion, PALETTED_PRE_PROTOCHUNK_DATA_VERSION); fixed.putInt("DataVersion", PALETTED_PRE_PROTOCHUNK_DATA_VERSION); if (!savedBlocks.isEmpty()) { - restoreSavedBlocks(fixed, savedBlocks); + restoreSavedBlocks(fixed, savedBlocks, extensions); restoreLegacyBlockEntities(fixed, savedBlocks, savedBlockEntities); } int preservedBlockEntities = savedBlockEntities.values().stream().mapToInt(List::size).sum(); @@ -389,14 +405,29 @@ private static ChunkConversion convertChunk(CompoundTag chunk, Map ids) throws IOException { - ChunkConversion conversion = convertChunk(chunk, ids); + return convertChunkForTest(chunk, ids, List.of()); + } + + static CompoundTag convertChunkForTest(CompoundTag chunk, Map ids, + List extensions) throws IOException { + ChunkConversion conversion = convertChunk(chunk, ids, extensions); if (!conversion.converted()) throw new IOException("Validation chunk did not contain a third-party block"); return conversion.chunk(); } - private static ItemMigration migrateLooseInventories(Path root) throws IOException { + private static boolean prepareExtensions(List extensions, CompoundTag tag, + int dataVersion, Payload payload) { + if (dataVersion > LAST_SUPPORTED_LEGACY_DATA_VERSION) return false; + boolean changed = false; + for (Legacy112MigrationExtension extension : extensions) changed |= extension.prepareData(tag, dataVersion, payload); + return changed; + } + + private static ItemMigration migrateLooseInventories(Path root, List extensions) + throws IOException { List files = new ArrayList<>(); - files.add(root.resolve("level.dat")); + Path levelDat = root.resolve("level.dat"); + files.add(levelDat); Path playerData = root.resolve("playerdata"); if (Files.isDirectory(playerData)) { try (var paths = Files.list(playerData)) { @@ -412,7 +443,17 @@ private static ItemMigration migrateLooseInventories(Path root) throws IOExcepti if (!Files.isRegularFile(file)) continue; CompoundTag data = NbtIo.readCompressed(file.toFile()); ItemMigration changed = convertLegacyItems(data); - if (changed.isEmpty()) continue; + boolean extensionChanged = false; + if (file.equals(levelDat)) { + CompoundTag world = data.getCompound("Data"); + if (world.contains("Player", Tag.TAG_COMPOUND)) { + extensionChanged = prepareExtensions(extensions, world.getCompound("Player"), + world.getInt("DataVersion"), Payload.PLAYER); + } + } else { + extensionChanged = prepareExtensions(extensions, data, data.getInt("DataVersion"), Payload.PLAYER); + } + if (changed.isEmpty() && !extensionChanged) continue; Path backup = root.resolve(BACKUP_DIRECTORY).resolve(root.relativize(file)); Files.createDirectories(backup.getParent()); if (!Files.exists(backup)) Files.copy(file, backup, StandardCopyOption.COPY_ATTRIBUTES); @@ -423,7 +464,8 @@ private static ItemMigration migrateLooseInventories(Path root) throws IOExcepti } /** Restore only the player captured from this load's original legacy save. */ - private static ItemMigration restoreRewrittenSingleplayerPlayer(Path root) throws IOException { + private static ItemMigration restoreRewrittenSingleplayerPlayer(Path root, + List extensions) throws IOException { Path currentFile = root.resolve("level.dat"); if (!root.equals(preparedWorld) || preparedPlayer == null || !Files.isRegularFile(currentFile)) { return ItemMigration.NONE; @@ -438,8 +480,16 @@ private static ItemMigration restoreRewrittenSingleplayerPlayer(Path root) throw CompoundTag player = preparedPlayer.copy(); ItemMigration migration = convertLegacyItems(player); + prepareExtensions(extensions, player, preparedVersion, Payload.PLAYER); + CompoundTag before = extensions.isEmpty() ? null : player.copy(); + int version = preparedVersion; + if (!extensions.isEmpty() && version < 704) { + player = NbtUtils.update(DataFixers.getDataFixer(), DataFixTypes.PLAYER, player, version, 704); + version = 704; + } CompoundTag fixedPlayer = NbtUtils.update(DataFixers.getDataFixer(), DataFixTypes.PLAYER, - player, preparedVersion); + player, version); + for (Legacy112MigrationExtension extension : extensions) extension.afterPlayerFlattening(before, fixedPlayer); currentData.put("Player", fixedPlayer); Path backup = root.resolve(BACKUP_DIRECTORY).resolve(root.relativize(currentFile)); @@ -557,7 +607,8 @@ private static SectionSnapshot snapshotSection(CompoundTag section, Map savedBlocks) throws IOException { + private static void restoreSavedBlocks(CompoundTag chunk, List savedBlocks, + List extensions) throws IOException { Map targetSections = new HashMap<>(); ListTag sections = chunk.getCompound("Level").getList("Sections", Tag.TAG_COMPOUND); for (Tag value : sections) { @@ -589,7 +640,7 @@ private static void restoreSavedBlocks(CompoundTag chunk, List save } for (LegacyBlock legacy : entry.getValue()) { - CompoundTag state = restoredState(legacy, byPosition); + CompoundTag state = restoredState(legacy, byPosition, extensions); Integer paletteId = paletteIds.get(state); if (paletteId == null) { paletteId = palette.size(); @@ -683,8 +734,8 @@ private static int bitsForPalette(int paletteSize) { return Math.max(4, 32 - Integer.numberOfLeadingZeros(Math.max(1, paletteSize - 1))); } - private static CompoundTag restoredState(LegacyBlock legacy, Map byPosition) - throws IOException { + private static CompoundTag restoredState(LegacyBlock legacy, Map byPosition, + List extensions) throws IOException { String normalizedName = normalizeLegacyRegistryName(legacy.name()); ResourceLocation oldId = ResourceLocation.tryParse(normalizedName); if (oldId == null) throw new IOException("Invalid legacy block registry name " + legacy.name()); @@ -711,6 +762,17 @@ private static CompoundTag restoredState(LegacyBlock legacy, Map calls = new ArrayList<>(); + Legacy112MigrationExtension extension = new Legacy112MigrationExtension() { + @Override public boolean prepareData(CompoundTag tag, int version, Payload payload) { + assertEquals(1343, version); + assertEquals(Payload.CHUNK, payload); + CompoundTag stack = chest.getList("Items", Tag.TAG_COMPOUND).getCompound(0); + CompoundTag itemData = new CompoundTag(); + itemData.putInt("Damage", stack.getShort("Damage")); + stack.put("tag", itemData); + calls.add("prepare"); + return true; + } + @Override public void beforeChunkFlattening(CompoundTag source) { + assertSame(chunk, source); + assertEquals(3, chest.getList("Items", Tag.TAG_COMPOUND).getCompound(0).getCompound("tag").getInt("Damage")); + calls.add("flatten"); + } + }; + CompoundTag fixed = Legacy112WorldMigrator.convertChunkForTest(chunk, + Map.of(0, "minecraft:air", 54, "minecraft:chest"), List.of(extension)); + CompoundTag stack = fixed.getCompound("Level").getList("TileEntities", Tag.TAG_COMPOUND) + .getCompound(0).getList("Items", Tag.TAG_COMPOUND).getCompound(0); + assertEquals(List.of("prepare", "flatten"), calls); + assertEquals("example:wrench", stack.getString("id")); + assertEquals(1, stack.getByte("Count")); + assertEquals(4, stack.getByte("Slot")); + assertEquals(3, stack.getCompound("tag").getInt("Damage")); + } + + @Test + void unclaimedThirdPartyBlocksStillKeepTheirNameAndTheDecoderReceivesRawMetadata() throws Exception { + List metadata = new ArrayList<>(); + Legacy112MigrationExtension extension = new Legacy112MigrationExtension() { + @Override public BlockState decodeBlockState(ResourceLocation id, int data) { + assertEquals(new ResourceLocation("example", "legacy_block"), id); + metadata.add(data); + return null; + } + }; + CompoundTag fixed = Legacy112WorldMigrator.convertChunkForTest(chunk(200, 13), + Map.of(0, "minecraft:air", 200, "example:legacy_block"), List.of(extension)); + assertEquals(List.of(13), metadata); + assertEquals("example:legacy_block", Legacy112WorldMigrator.stateAtForTest(fixed, 0, 0).getString("Name")); + } + + private static CompoundTag chunk(int block, int metadata) { + CompoundTag root = new CompoundTag(); + root.putInt("DataVersion", 1343); + CompoundTag level = new CompoundTag(); + level.putBoolean("TerrainPopulated", true); + level.putBoolean("LightPopulated", true); + level.putByteArray("Biomes", new byte[256]); + level.putIntArray("HeightMap", new int[256]); + CompoundTag section = new CompoundTag(); + byte[] blocks = new byte[4096]; + blocks[0] = (byte) block; + section.putByteArray("Blocks", blocks); + byte[] data = new byte[2048]; + data[0] = (byte) metadata; + section.putByteArray("Data", data); + section.putByte("Y", (byte) 0); + ListTag sections = new ListTag(); + sections.add(section); + level.put("Sections", sections); + ListTag blockEntities = new ListTag(); + if (block == 54) { + CompoundTag chest = new CompoundTag(); + chest.putString("id", "minecraft:chest"); + CompoundTag stack = new CompoundTag(); + stack.putString("id", "example:wrench"); + stack.putByte("Count", (byte) 1); + stack.putByte("Slot", (byte) 4); + stack.putShort("Damage", (short) 3); + ListTag items = new ListTag(); + items.add(stack); + chest.put("Items", items); + blockEntities.add(chest); + } + level.put("TileEntities", blockEntities); + level.put("Entities", new ListTag()); + root.put("Level", level); + return root; + } +} From b8d2d5ae5d2025a08113d836175375b503c3f488 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 9 Nov 2026 15:07:47 +0000 Subject: [PATCH 2/2] Use the declared version for the 1.18.2 CI jars --- .github/workflows/ci.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72ef7c4e4..10dcfd698 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,16 +43,19 @@ jobs: echo "repository=$mirror" >> "$GITHUB_OUTPUT" - name: Build, test, and audit first candidate + id: candidate run: | + version="$(sed -n 's/^mod_version=//p' gradle.properties | tr -d '\r')" + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.118021$ ]] + stem="BaseMetals-$version" + echo "stem=$stem" >> "$GITHUB_OUTPUT" chmod +x ./gradlew ./gradlew clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums \ -PorespawnVerificationRepository="${{ steps.orespawn.outputs.repository }}" \ --no-daemon --stacktrace first="$RUNNER_TEMP/basemetals-first" mkdir -p "$first" - cp build/libs/BaseMetals-3.1.1.118021.jar "$first/" - cp build/libs/BaseMetals-3.1.1.118021-sources.jar "$first/" - cp build/libs/BaseMetals-3.1.1.118021-javadoc.jar "$first/" + cp "build/libs/$stem.jar" "build/libs/$stem-sources.jar" "build/libs/$stem-javadoc.jar" "$first/" cp build/release/SHA256SUMS "$first/" - name: Rebuild and prove byte-identical archives @@ -60,9 +63,10 @@ jobs: ./gradlew clean assemble verifyReleaseArtifacts writeReleaseChecksums \ -PorespawnVerificationRepository="${{ steps.orespawn.outputs.repository }}" \ --no-daemon --stacktrace - cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.1.1.118021.jar" build/libs/BaseMetals-3.1.1.118021.jar - cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.1.1.118021-sources.jar" build/libs/BaseMetals-3.1.1.118021-sources.jar - cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.1.1.118021-javadoc.jar" build/libs/BaseMetals-3.1.1.118021-javadoc.jar + stem="${{ steps.candidate.outputs.stem }}" + cmp "$RUNNER_TEMP/basemetals-first/$stem.jar" "build/libs/$stem.jar" + cmp "$RUNNER_TEMP/basemetals-first/$stem-sources.jar" "build/libs/$stem-sources.jar" + cmp "$RUNNER_TEMP/basemetals-first/$stem-javadoc.jar" "build/libs/$stem-javadoc.jar" cmp "$RUNNER_TEMP/basemetals-first/SHA256SUMS" build/release/SHA256SUMS - name: Run all required GameTests @@ -128,9 +132,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/BaseMetals-3.1.1.118021.jar - build/libs/BaseMetals-3.1.1.118021-sources.jar - build/libs/BaseMetals-3.1.1.118021-javadoc.jar + build/libs/${{ steps.candidate.outputs.stem }}.jar + build/libs/${{ steps.candidate.outputs.stem }}-sources.jar + build/libs/${{ steps.candidate.outputs.stem }}-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt